< 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
35%
Covered lines: 582
Uncovered lines: 1038
Coverable lines: 1620
Total lines: 3804
Line coverage: 35.9%
Branch coverage
31%
Covered branches: 269
Total branches: 866
Branch coverage: 31%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 1/23/2026 - 12:11:06 AM Line coverage: 34.2% (383/1118) Branch coverage: 28% (177/630) Total lines: 33894/19/2026 - 12:14:27 AM Line coverage: 38.5% (554/1438) Branch coverage: 32.4% (242/746) Total lines: 33894/22/2026 - 12:14:04 AM Line coverage: 38.3% (551/1438) Branch coverage: 32.4% (242/746) Total lines: 33894/23/2026 - 12:14:52 AM Line coverage: 38.5% (554/1438) Branch coverage: 32.4% (242/746) Total lines: 33894/26/2026 - 12:12:40 AM Line coverage: 38.3% (551/1438) Branch coverage: 32.4% (242/746) Total lines: 33894/27/2026 - 12:15:04 AM Line coverage: 38.5% (554/1438) Branch coverage: 32.4% (242/746) Total lines: 33895/4/2026 - 12:15:16 AM Line coverage: 35.8% (572/1596) Branch coverage: 30.5% (261/854) Total lines: 37595/5/2026 - 12:15:44 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 3804 1/23/2026 - 12:11:06 AM Line coverage: 34.2% (383/1118) Branch coverage: 28% (177/630) Total lines: 33894/19/2026 - 12:14:27 AM Line coverage: 38.5% (554/1438) Branch coverage: 32.4% (242/746) Total lines: 33894/22/2026 - 12:14:04 AM Line coverage: 38.3% (551/1438) Branch coverage: 32.4% (242/746) Total lines: 33894/23/2026 - 12:14:52 AM Line coverage: 38.5% (554/1438) Branch coverage: 32.4% (242/746) Total lines: 33894/26/2026 - 12:12:40 AM Line coverage: 38.3% (551/1438) Branch coverage: 32.4% (242/746) Total lines: 33894/27/2026 - 12:15:04 AM Line coverage: 38.5% (554/1438) Branch coverage: 32.4% (242/746) Total lines: 33895/4/2026 - 12:15:16 AM Line coverage: 35.8% (572/1596) Branch coverage: 30.5% (261/854) Total lines: 37595/5/2026 - 12:15:44 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 3804

Coverage delta

Coverage delta 5 -5

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%
DeleteItemsUnsafeFast(...)8.33%123128.33%
DeleteItem(...)0%4422660%
DeleteItemPath(...)0%272160%
IsInternalItem(...)0%342180%
GetMetadataPaths(...)0%620%
GetInternalMetadataPaths(...)0%7280%
ResolveItem(...)100%44100%
Resolve(...)100%1140%
GetNewItemId(...)100%11100%
GetNewItemIdInternal(...)100%66100%
ResolvePath(...)100%22100%
ResolveAlternateVersion(...)0%156120%
ResolvePath(...)70%242078.12%
IgnoreFile(...)100%11100%
NormalizeRootPathList(...)50%2291.66%
ResolvePaths(...)100%11100%
ResolvePaths(...)58.33%221258.33%
ResolveFileList()100%4472.72%
CreateRootFolder()71.42%141488.88%
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%
GetArtists(...)100%11100%
GetArtist(...)100%210%
CreateItemByName(...)0%7280%
GetItemByNameId(...)100%11100%
ValidatePeopleAsync(...)100%210%
ValidateMediaLibrary(...)100%11100%
ValidateMediaLibraryInternal()100%11100%
ValidateTopLibraryFolders()100%88100%
ClearIgnoreRuleCache()100%11100%
PerformLibraryValidation()100%11100%
RunPostScanTasks()100%2290.9%
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%88100%
GetItemList(...)100%11100%
GetCount(...)0%7280%
GetItemCounts(...)0%7280%
GetItemCountsForNameItem(...)0%620%
GetChildCountBatch(...)100%210%
GetPlayedAndTotalCountBatch(...)100%210%
GetItemList(...)0%4260%
GetLatestItemList(...)0%4260%
GetNextUpSeriesKeys(...)0%4260%
GetNextUpEpisodesBatch(...)100%210%
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(...)72.22%371861.11%
AddUserToQuery(...)94.44%181894.73%
ConfigureUserAccess(...)100%210%
GetTopParentIdsForQuery(...)8.33%4342410.71%
GetIntros()0%620%
GetIntros()100%210%
ResolveIntro(...)0%110100%
GetLocalAlternateVersionIds(...)0%620%
GetLinkedAlternateVersions(...)0%620%
UpsertLinkedChild(...)100%210%
Sort(...)57.14%341453.33%
Sort(...)0%210140%
GetComparer(...)50%3237.5%
CreateItem(...)100%210%
CreateItems(...)56.66%723063.88%
ImageNeedsRefresh(...)0%156120%
UpdateImagesAsync()25%1001215.09%
UpdateItemsAsync()60%613067.44%
UpdateItemAsync(...)100%11100%
ReattachUserDataAsync()100%11100%
RunMetadataSavers()100%22100%
ReportItemRemoved(...)0%620%
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(...)62.5%9872.72%
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%
FindExtras()88.46%262695.45%
GetPathAfterNetworkSubstitution(...)25%6450%
GetPeople(...)100%210%
GetPeople(...)50%11425%
GetPeopleItems(...)100%210%
GetPeopleNames(...)100%210%
UpdatePeople(...)100%210%
UpdatePeopleAsync()0%2040%
ConvertImageToLocal()0%110100%
AddVirtualFolder()66.66%221877.41%
SavePeopleMetadataAsync()0%272160%
StartScanInBackground()100%11100%
AddMediaPath(...)100%1150%
AddMediaPathInternal(...)33.33%17633.33%
UpdateMediaPath(...)100%210%
SyncLibraryOptionsToLocations(...)0%7280%
RemoveVirtualFolder()62.5%9880%
RemoveContentTypeOverrides(...)0%210140%
RemoveMediaPath(...)25%9433.33%
ItemIsVisible(...)16.66%14640%
CreateShortcut(...)0%620%
RerouteLinkedChildReferencesAsync()0%110100%
GetQueryFiltersLegacy(...)0%620%

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.Entities.Movies;
 34using MediaBrowser.Controller.IO;
 35using MediaBrowser.Controller.Library;
 36using MediaBrowser.Controller.LiveTv;
 37using MediaBrowser.Controller.MediaEncoding;
 38using MediaBrowser.Controller.Persistence;
 39using MediaBrowser.Controller.Playlists;
 40using MediaBrowser.Controller.Providers;
 41using MediaBrowser.Controller.Resolvers;
 42using MediaBrowser.Controller.Sorting;
 43using MediaBrowser.Model.Configuration;
 44using MediaBrowser.Model.Drawing;
 45using MediaBrowser.Model.Dto;
 46using MediaBrowser.Model.Entities;
 47using MediaBrowser.Model.IO;
 48using MediaBrowser.Model.Library;
 49using MediaBrowser.Model.Querying;
 50using MediaBrowser.Model.Tasks;
 51using Microsoft.Extensions.Logging;
 52using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 53using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
 54using Genre = MediaBrowser.Controller.Entities.Genre;
 55using Person = MediaBrowser.Controller.Entities.Person;
 56using VideoResolver = Emby.Naming.Video.VideoResolver;
 57
 58namespace Emby.Server.Implementations.Library
 59{
 60    /// <summary>
 61    /// Class LibraryManager.
 62    /// </summary>
 63    public class LibraryManager : ILibraryManager
 64    {
 65        private const string ShortcutFileExtension = ".mblink";
 66
 67        private readonly ILogger<LibraryManager> _logger;
 68        private readonly ITaskManager _taskManager;
 69        private readonly IUserManager _userManager;
 70        private readonly IUserDataManager _userDataManager;
 71        private readonly IServerConfigurationManager _configurationManager;
 72        private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory;
 73        private readonly Lazy<IProviderManager> _providerManagerFactory;
 74        private readonly Lazy<IUserViewManager> _userViewManagerFactory;
 75        private readonly IServerApplicationHost _appHost;
 76        private readonly IMediaEncoder _mediaEncoder;
 77        private readonly IFileSystem _fileSystem;
 78        private readonly IItemRepository _itemRepository;
 79        private readonly IItemPersistenceService _persistenceService;
 80        private readonly INextUpService _nextUpService;
 81        private readonly IItemCountService _countService;
 82        private readonly ILinkedChildrenService _linkedChildrenService;
 83        private readonly IImageProcessor _imageProcessor;
 84        private readonly NamingOptions _namingOptions;
 85        private readonly IPeopleRepository _peopleRepository;
 86        private readonly ExtraResolver _extraResolver;
 87        private readonly IPathManager _pathManager;
 88        private readonly FastConcurrentLru<Guid, BaseItem> _cache;
 89        private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
 90
 91        /// <summary>
 92        /// The _root folder sync lock.
 93        /// </summary>
 2894        private readonly Lock _rootFolderSyncLock = new();
 2895        private readonly Lock _userRootFolderSyncLock = new();
 96
 2897        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 98
 99        /// <summary>
 100        /// The _root folder.
 101        /// </summary>
 102        private volatile AggregateFolder? _rootFolder;
 103        private volatile UserRootFolder? _userRootFolder;
 104
 105        private bool _wizardCompleted;
 106
 107        /// <summary>
 108        /// Initializes a new instance of the <see cref="LibraryManager" /> class.
 109        /// </summary>
 110        /// <param name="appHost">The application host.</param>
 111        /// <param name="loggerFactory">The logger factory.</param>
 112        /// <param name="taskManager">The task manager.</param>
 113        /// <param name="userManager">The user manager.</param>
 114        /// <param name="configurationManager">The configuration manager.</param>
 115        /// <param name="userDataManager">The user data manager.</param>
 116        /// <param name="libraryMonitorFactory">The library monitor.</param>
 117        /// <param name="fileSystem">The file system.</param>
 118        /// <param name="providerManagerFactory">The provider manager.</param>
 119        /// <param name="userViewManagerFactory">The user view manager.</param>
 120        /// <param name="mediaEncoder">The media encoder.</param>
 121        /// <param name="itemRepository">The item repository.</param>
 122        /// <param name="persistenceService">The item persistence service.</param>
 123        /// <param name="nextUpService">The next up service.</param>
 124        /// <param name="countService">The item count service.</param>
 125        /// <param name="linkedChildrenService">The linked children service.</param>
 126        /// <param name="imageProcessor">The image processor.</param>
 127        /// <param name="namingOptions">The naming options.</param>
 128        /// <param name="directoryService">The directory service.</param>
 129        /// <param name="peopleRepository">The people repository.</param>
 130        /// <param name="pathManager">The path manager.</param>
 131        /// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
 132        public LibraryManager(
 133            IServerApplicationHost appHost,
 134            ILoggerFactory loggerFactory,
 135            ITaskManager taskManager,
 136            IUserManager userManager,
 137            IServerConfigurationManager configurationManager,
 138            IUserDataManager userDataManager,
 139            Lazy<ILibraryMonitor> libraryMonitorFactory,
 140            IFileSystem fileSystem,
 141            Lazy<IProviderManager> providerManagerFactory,
 142            Lazy<IUserViewManager> userViewManagerFactory,
 143            IMediaEncoder mediaEncoder,
 144            IItemRepository itemRepository,
 145            IItemPersistenceService persistenceService,
 146            INextUpService nextUpService,
 147            IItemCountService countService,
 148            ILinkedChildrenService linkedChildrenService,
 149            IImageProcessor imageProcessor,
 150            NamingOptions namingOptions,
 151            IDirectoryService directoryService,
 152            IPeopleRepository peopleRepository,
 153            IPathManager pathManager,
 154            DotIgnoreIgnoreRule dotIgnoreIgnoreRule)
 155        {
 28156            _appHost = appHost;
 28157            _logger = loggerFactory.CreateLogger<LibraryManager>();
 28158            _taskManager = taskManager;
 28159            _userManager = userManager;
 28160            _configurationManager = configurationManager;
 28161            _userDataManager = userDataManager;
 28162            _libraryMonitorFactory = libraryMonitorFactory;
 28163            _fileSystem = fileSystem;
 28164            _providerManagerFactory = providerManagerFactory;
 28165            _userViewManagerFactory = userViewManagerFactory;
 28166            _mediaEncoder = mediaEncoder;
 28167            _itemRepository = itemRepository;
 28168            _persistenceService = persistenceService;
 28169            _nextUpService = nextUpService;
 28170            _countService = countService;
 28171            _linkedChildrenService = linkedChildrenService;
 28172            _imageProcessor = imageProcessor;
 173
 28174            _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
 175
 28176            _namingOptions = namingOptions;
 28177            _peopleRepository = peopleRepository;
 28178            _pathManager = pathManager;
 28179            _dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
 28180            _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryServ
 181
 28182            _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 183
 28184            RecordConfigurationValues(_configurationManager.Configuration);
 28185        }
 186
 187        /// <summary>
 188        /// Occurs when [item added].
 189        /// </summary>
 190        public event EventHandler<ItemChangeEventArgs>? ItemAdded;
 191
 192        /// <summary>
 193        /// Occurs when [item updated].
 194        /// </summary>
 195        public event EventHandler<ItemChangeEventArgs>? ItemUpdated;
 196
 197        /// <summary>
 198        /// Occurs when [item removed].
 199        /// </summary>
 200        public event EventHandler<ItemChangeEventArgs>? ItemRemoved;
 201
 202        /// <summary>
 203        /// Gets the root folder.
 204        /// </summary>
 205        /// <value>The root folder.</value>
 206        public AggregateFolder RootFolder
 207        {
 208            get
 209            {
 179210                if (_rootFolder is null)
 21211                {
 212                    lock (_rootFolderSyncLock)
 213                    {
 21214                        _rootFolder ??= CreateRootFolder();
 21215                    }
 216                }
 217
 179218                return _rootFolder;
 219            }
 220        }
 221
 41222        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
 223
 110224        private IProviderManager ProviderManager => _providerManagerFactory.Value;
 225
 1226        private IUserViewManager UserViewManager => _userViewManagerFactory.Value;
 227
 228        /// <summary>
 229        /// Gets or sets the postscan tasks.
 230        /// </summary>
 231        /// <value>The postscan tasks.</value>
 232        private ILibraryPostScanTask[] PostScanTasks { get; set; } = [];
 233
 234        /// <summary>
 235        /// Gets or sets the intro providers.
 236        /// </summary>
 237        /// <value>The intro providers.</value>
 238        private IIntroProvider[] IntroProviders { get; set; } = [];
 239
 240        /// <summary>
 241        /// Gets or sets the list of entity resolution ignore rules.
 242        /// </summary>
 243        /// <value>The entity resolution ignore rules.</value>
 244        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
 245
 246        /// <summary>
 247        /// Gets or sets the list of currently registered entity resolvers.
 248        /// </summary>
 249        /// <value>The entity resolvers enumerable.</value>
 250        private IItemResolver[] EntityResolvers { get; set; } = [];
 251
 252        private IMultiItemResolver[] MultiItemResolvers { get; set; } = [];
 253
 254        /// <summary>
 255        /// Gets or sets the comparers.
 256        /// </summary>
 257        /// <value>The comparers.</value>
 258        private IBaseItemComparer[] Comparers { get; set; } = [];
 259
 260        public bool IsScanRunning { get; private set; }
 261
 262        /// <summary>
 263        /// Adds the parts.
 264        /// </summary>
 265        /// <param name="rules">The rules.</param>
 266        /// <param name="resolvers">The resolvers.</param>
 267        /// <param name="introProviders">The intro providers.</param>
 268        /// <param name="itemComparers">The item comparers.</param>
 269        /// <param name="postScanTasks">The post scan tasks.</param>
 270        public void AddParts(
 271            IEnumerable<IResolverIgnoreRule> rules,
 272            IEnumerable<IItemResolver> resolvers,
 273            IEnumerable<IIntroProvider> introProviders,
 274            IEnumerable<IBaseItemComparer> itemComparers,
 275            IEnumerable<ILibraryPostScanTask> postScanTasks)
 276        {
 28277            EntityResolutionIgnoreRules = rules.ToArray();
 28278            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
 28279            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
 28280            IntroProviders = introProviders.ToArray();
 28281            Comparers = itemComparers.ToArray();
 28282            PostScanTasks = postScanTasks.ToArray();
 28283        }
 284
 285        /// <summary>
 286        /// Records the configuration values.
 287        /// </summary>
 288        /// <param name="configuration">The configuration.</param>
 289        private void RecordConfigurationValues(ServerConfiguration configuration)
 290        {
 129291            _wizardCompleted = configuration.IsStartupWizardCompleted;
 129292        }
 293
 294        /// <summary>
 295        /// Configurations the updated.
 296        /// </summary>
 297        /// <param name="sender">The sender.</param>
 298        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
 299        private void ConfigurationUpdated(object? sender, EventArgs e)
 300        {
 101301            var config = _configurationManager.Configuration;
 302
 101303            var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted;
 304
 101305            RecordConfigurationValues(config);
 306
 101307            if (wizardChanged)
 308            {
 16309                _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 310            }
 101311        }
 312
 313        public void RegisterItem(BaseItem item)
 314        {
 180315            ArgumentNullException.ThrowIfNull(item);
 316
 180317            if (item is IItemByName)
 318            {
 0319                if (item is not MusicArtist)
 320                {
 0321                    return;
 322                }
 323            }
 180324            else if (!item.IsFolder)
 325            {
 0326                if (item is not Video && item is not LiveTvChannel)
 327                {
 0328                    return;
 329                }
 330            }
 331
 180332            _cache.AddOrUpdate(item.Id, item);
 180333        }
 334
 335        public void DeleteItem(BaseItem item, DeleteOptions options)
 336        {
 0337            DeleteItem(item, options, false);
 0338        }
 339
 340        public void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem)
 341        {
 0342            ArgumentNullException.ThrowIfNull(item);
 343
 0344            var parent = item.GetOwner() ?? item.GetParent();
 345
 0346            DeleteItem(item, options, parent, notifyParentItem);
 0347        }
 348
 349        public void DeleteItemsUnsafeFast(IReadOnlyCollection<BaseItem> items, bool deleteSourceFiles = false)
 350        {
 48351            if (items.Count == 0)
 352            {
 48353                return;
 354            }
 355
 0356            var pathMaps = items.Select(e =>
 0357                (Item: e,
 0358                InternalPath: GetInternalMetadataPaths(e),
 0359                DeletePaths: deleteSourceFiles ? e.GetDeletePaths() : [])).ToArray();
 360
 0361            foreach (var (item, internalPaths, pathsToDelete) in pathMaps)
 362            {
 0363                foreach (var metadataPath in internalPaths)
 364                {
 0365                    if (!Directory.Exists(metadataPath))
 366                    {
 367                        continue;
 368                    }
 369
 0370                    _logger.LogDebug(
 0371                        "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0372                        item.GetType().Name,
 0373                        item.Name ?? "Unknown name",
 0374                        metadataPath,
 0375                        item.Id);
 376
 377                    try
 378                    {
 0379                        Directory.Delete(metadataPath, true);
 0380                    }
 0381                    catch (Exception ex)
 382                    {
 0383                        _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0384                    }
 385                }
 386
 0387                foreach (var fileSystemInfo in pathsToDelete)
 388                {
 0389                    DeleteItemPath(item, false, fileSystemInfo);
 390                }
 391            }
 392
 0393            _persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
 0394        }
 395
 396        public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
 397        {
 0398            ArgumentNullException.ThrowIfNull(item);
 399
 0400            if (item.SourceType == SourceType.Channel)
 401            {
 0402                if (options.DeleteFromExternalProvider)
 403                {
 404                    try
 405                    {
 0406                        BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult();
 0407                    }
 0408                    catch (ArgumentException)
 409                    {
 410                        // channel no longer installed
 0411                    }
 412                }
 413
 0414                options.DeleteFileLocation = false;
 415            }
 416
 0417            if (item is LiveTvProgram)
 418            {
 0419                _logger.LogDebug(
 0420                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0421                    item.GetType().Name,
 0422                    item.Name ?? "Unknown name",
 0423                    item.Path ?? string.Empty,
 0424                    item.Id);
 425            }
 426            else
 427            {
 0428                _logger.LogInformation(
 0429                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0430                    item.GetType().Name,
 0431                    item.Name ?? "Unknown name",
 0432                    item.Path ?? string.Empty,
 0433                    item.Id);
 434            }
 435
 436            // If deleting a primary version video, clear PrimaryVersionId from alternate versions
 437            // OwnerId check: items with OwnerId set are alternate versions or extras, not primaries
 0438            if (item is Video video && !video.PrimaryVersionId.HasValue && video.OwnerId.IsEmpty())
 439            {
 0440                var localAlternateIds = GetLocalAlternateVersionIds(video).ToHashSet();
 0441                var allAlternateVersions = localAlternateIds
 0442                    .Concat(GetLinkedAlternateVersions(video).Select(v => v.Id))
 0443                    .Distinct()
 0444                    .Select(id => GetItemById(id))
 0445                    .OfType<Video>()
 0446                    .ToList();
 447
 448                // Partition alternates by whether their files still exist on disk
 0449                var alternateVersions = new List<Video>();
 0450                var missingAlternates = new List<Video>();
 0451                foreach (var alt in allAlternateVersions)
 452                {
 0453                    if (!string.IsNullOrEmpty(alt.Path) && !_fileSystem.FileExists(alt.Path))
 454                    {
 0455                        missingAlternates.Add(alt);
 456                    }
 457                    else
 458                    {
 0459                        alternateVersions.Add(alt);
 460                    }
 461                }
 462
 463                // Delete alternates whose files no longer exist to avoid ghost items.
 464                // Clear PrimaryVersionId first so DeleteItem doesn't try to update the primary being deleted.
 0465                foreach (var missing in missingAlternates)
 466                {
 0467                    _logger.LogInformation(
 0468                        "Deleting missing alternate version {Name} ({Path})",
 0469                        missing.Name ?? "Unknown name",
 0470                        missing.Path ?? string.Empty);
 0471                    missing.SetPrimaryVersionId(null);
 0472                    missing.OwnerId = Guid.Empty;
 0473                    missing.LocalAlternateVersions = [];
 0474                    missing.LinkedAlternateVersions = [];
 0475                    DeleteItem(missing, new DeleteOptions { DeleteFileLocation = false }, false);
 476                }
 477
 0478                if (alternateVersions.Count > 0)
 479                {
 0480                    _logger.LogInformation(
 0481                        "Clearing PrimaryVersionId from {Count} alternate versions of {Name}",
 0482                        alternateVersions.Count,
 0483                        item.Name ?? "Unknown name");
 484
 485                    // Promote the first alternate version to be the new primary
 0486                    var newPrimary = alternateVersions[0];
 0487                    newPrimary.SetPrimaryVersionId(null);
 0488                    newPrimary.OwnerId = Guid.Empty;
 489
 490                    // Transfer alternate version arrays from old primary to new primary
 491                    // so UpdateToRepositoryAsync creates correct LinkedChildren entries
 0492                    newPrimary.LocalAlternateVersions = video.LocalAlternateVersions
 0493                        .Where(p => !string.Equals(p, newPrimary.Path, StringComparison.OrdinalIgnoreCase))
 0494                        .ToArray();
 0495                    newPrimary.LinkedAlternateVersions = video.LinkedAlternateVersions
 0496                        .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(newPrimary.Id))
 0497                        .ToArray();
 498
 0499                    newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter()
 500
 501                    // Re-route playlist/collection references from deleted primary to new primary
 0502                    RerouteLinkedChildReferencesAsync(video.Id, newPrimary.Id).GetAwaiter().GetResult();
 503
 504                    // Update remaining alternates to point to new primary
 0505                    foreach (var alternate in alternateVersions.Skip(1))
 506                    {
 0507                        alternate.SetPrimaryVersionId(newPrimary.Id);
 508                        // Only set OwnerId for local alternates; linked alternates are independent items
 0509                        alternate.OwnerId = localAlternateIds.Contains(alternate.Id) ? newPrimary.Id : Guid.Empty;
 0510                        alternate.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaite
 511                    }
 512                }
 513            }
 0514            else if (item is Video alternateVideo && alternateVideo.PrimaryVersionId.HasValue)
 515            {
 516                // If deleting an alternate version, re-route references to its primary
 0517                RerouteLinkedChildReferencesAsync(alternateVideo.Id, alternateVideo.PrimaryVersionId.Value).GetAwaiter()
 518
 519                // Remove deleted alternate from primary's LinkedAlternateVersions
 0520                if (GetItemById(alternateVideo.PrimaryVersionId.Value) is Video primaryVideo)
 521                {
 0522                    primaryVideo.LinkedAlternateVersions = primaryVideo.LinkedAlternateVersions
 0523                        .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(alternateVideo.Id))
 0524                        .ToArray();
 0525                    primaryVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter
 526                }
 527            }
 528
 0529            var children = item.IsFolder
 0530                ? ((Folder)item).GetRecursiveChildren(false)
 0531                : [];
 532
 0533            foreach (var metadataPath in GetMetadataPaths(item, children))
 534            {
 0535                if (!Directory.Exists(metadataPath))
 536                {
 537                    continue;
 538                }
 539
 0540                _logger.LogDebug(
 0541                    "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0542                    item.GetType().Name,
 0543                    item.Name ?? "Unknown name",
 0544                    metadataPath,
 0545                    item.Id);
 546
 547                try
 548                {
 0549                    Directory.Delete(metadataPath, true);
 0550                }
 0551                catch (Exception ex)
 552                {
 0553                    _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0554                }
 555            }
 556
 0557            if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item))
 558            {
 559                // Assume only the first is required
 560                // Add this flag to GetDeletePaths if required in the future
 0561                var isRequiredForDelete = true;
 562
 0563                foreach (var fileSystemInfo in item.GetDeletePaths())
 564                {
 0565                    DeleteItemPath(item, isRequiredForDelete, fileSystemInfo);
 566
 0567                    isRequiredForDelete = false;
 568                }
 569            }
 570
 0571            item.SetParent(null);
 572
 0573            _persistenceService.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
 0574            _cache.TryRemove(item.Id, out _);
 0575            foreach (var child in children)
 576            {
 0577                _cache.TryRemove(child.Id, out _);
 578            }
 579
 0580            if (parent is Folder folder)
 581            {
 0582                folder.Children = null;
 0583                folder.UserData = null;
 584            }
 585
 0586            ReportItemRemoved(item, parent);
 0587        }
 588
 589        private void DeleteItemPath(BaseItem item, bool isRequiredForDelete, FileSystemMetadata fileSystemInfo)
 590        {
 0591            if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName))
 592            {
 593                try
 594                {
 0595                    _logger.LogInformation(
 0596                        "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0597                        item.GetType().Name,
 0598                        item.Name ?? "Unknown name",
 0599                        fileSystemInfo.FullName,
 0600                        item.Id);
 601
 0602                    if (fileSystemInfo.IsDirectory)
 603                    {
 0604                        Directory.Delete(fileSystemInfo.FullName, true);
 605                    }
 606                    else
 607                    {
 0608                        File.Delete(fileSystemInfo.FullName);
 609                    }
 0610                }
 0611                catch (DirectoryNotFoundException)
 612                {
 0613                    _logger.LogInformation(
 0614                        "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id:
 0615                        item.GetType().Name,
 0616                        item.Name ?? "Unknown name",
 0617                        fileSystemInfo.FullName,
 0618                        item.Id);
 0619                }
 0620                catch (FileNotFoundException)
 621                {
 0622                    _logger.LogInformation(
 0623                        "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}
 0624                        item.GetType().Name,
 0625                        item.Name ?? "Unknown name",
 0626                        fileSystemInfo.FullName,
 0627                        item.Id);
 0628                }
 0629                catch (IOException)
 630                {
 0631                    if (isRequiredForDelete)
 632                    {
 0633                        throw;
 634                    }
 0635                }
 0636                catch (UnauthorizedAccessException)
 637                {
 0638                    if (isRequiredForDelete)
 639                    {
 0640                        throw;
 641                    }
 0642                }
 643            }
 0644        }
 645
 646        private bool IsInternalItem(BaseItem item)
 647        {
 0648            if (!item.IsFileProtocol)
 649            {
 0650                return false;
 651            }
 652
 0653            var pathToCheck = item switch
 0654            {
 0655                Genre => _configurationManager.ApplicationPaths.GenrePath,
 0656                MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
 0657                MusicGenre => _configurationManager.ApplicationPaths.MusicGenrePath,
 0658                Person => _configurationManager.ApplicationPaths.PeoplePath,
 0659                Studio => _configurationManager.ApplicationPaths.StudioPath,
 0660                Year => _configurationManager.ApplicationPaths.YearPath,
 0661                _ => null
 0662            };
 663
 0664            var itemPath = item.Path;
 0665            if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath))
 666            {
 0667                var cleanPath = _fileSystem.GetValidFilename(itemPath);
 0668                var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck);
 669
 0670                return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal);
 671            }
 672
 0673            return false;
 674        }
 675
 676        private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
 677        {
 0678            var list = GetInternalMetadataPaths(item);
 0679            foreach (var child in children)
 680            {
 0681                list.AddRange(GetInternalMetadataPaths(child));
 682            }
 683
 0684            return list;
 685        }
 686
 687        private List<string> GetInternalMetadataPaths(BaseItem item)
 688        {
 0689            var list = new List<string>
 0690            {
 0691                item.GetInternalMetadataPath()
 0692            };
 693
 0694            if (item is Video video)
 695            {
 696                // Trickplay
 0697                list.Add(_pathManager.GetTrickplayDirectory(video));
 698
 699                // Chapter Images
 0700                list.Add(_pathManager.GetChapterImageFolderPath(video));
 701
 702                // Subtitles and attachments
 0703                foreach (var mediaSource in item.GetMediaSources(false))
 704                {
 0705                    var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
 0706                    if (subtitleFolder is not null)
 707                    {
 0708                        list.Add(subtitleFolder);
 709                    }
 710
 0711                    var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
 0712                    if (attachmentFolder is not null)
 713                    {
 0714                        list.Add(attachmentFolder);
 715                    }
 716                }
 717            }
 718
 0719            return list;
 720        }
 721
 722        /// <summary>
 723        /// Resolves the item.
 724        /// </summary>
 725        /// <param name="args">The args.</param>
 726        /// <param name="resolvers">The resolvers.</param>
 727        /// <returns>BaseItem.</returns>
 728        private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers)
 729        {
 76730            var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
 76731                .FirstOrDefault(i => i is not null);
 732
 76733            if (item is not null)
 734            {
 66735                ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this);
 736            }
 737
 76738            return item;
 739        }
 740
 741        private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver)
 742        {
 743            try
 744            {
 304745                return resolver.ResolvePath(args);
 746            }
 0747            catch (Exception ex)
 748            {
 0749                _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path);
 0750                return null;
 751            }
 304752        }
 753
 754        public Guid GetNewItemId(string key, Type type)
 755        {
 129756            return GetNewItemIdInternal(key, type, false);
 757        }
 758
 759        private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive)
 760        {
 130761            ArgumentException.ThrowIfNullOrEmpty(key);
 130762            ArgumentNullException.ThrowIfNull(type);
 763
 130764            string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
 130765            if (key.StartsWith(programDataPath, StringComparison.Ordinal))
 766            {
 767                // Try to normalize paths located underneath program-data in an attempt to make them more portable
 113768                key = key.Substring(programDataPath.Length)
 113769                    .TrimStart('/', '\\')
 113770                    .Replace('/', '\\');
 771            }
 772
 130773            if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
 774            {
 1775                key = key.ToLowerInvariant();
 776            }
 777
 130778            key = type.FullName + key;
 779
 130780            return key.GetMD5();
 781        }
 782
 783        public BaseItem? ResolvePath(
 784            FileSystemMetadata fileInfo,
 785            Folder? parent = null,
 786            IDirectoryService? directoryService = null,
 787            CollectionType? collectionType = null)
 42788            => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType
 789
 790        /// <inheritdoc />
 791        public Video? ResolveAlternateVersion(string path, Type expectedVideoType, Folder? parent, CollectionType? colle
 792        {
 793            // Clean up any existing item saved with wrong type (e.g. Video instead of Movie).
 794            // This happens when items were previously resolved without proper type context
 795            // in mixed-content libraries where collectionType is null.
 0796            var expectedId = GetNewItemId(path, expectedVideoType);
 0797            if (expectedVideoType != typeof(Video))
 798            {
 0799                var wrongTypeId = GetNewItemId(path, typeof(Video));
 0800                if (!wrongTypeId.Equals(expectedId) && GetItemById(wrongTypeId) is Video wrongTypeItem)
 801                {
 0802                    _logger.LogInformation(
 0803                        "Removing alternate version with wrong type {WrongType}, expected {ExpectedType}: {Path}",
 0804                        wrongTypeItem.GetType().Name,
 0805                        expectedVideoType.Name,
 0806                        path);
 0807                    DeleteItem(wrongTypeItem, new DeleteOptions { DeleteFileLocation = false });
 808                }
 809            }
 810
 0811            var resolved = ResolvePath(
 0812                _fileSystem.GetFileSystemInfo(path),
 0813                parent,
 0814                collectionType: collectionType) as Video;
 815
 0816            if (resolved is null)
 817            {
 0818                return null;
 819            }
 820
 821            // Ensure the alternate version has the same concrete type as the primary video.
 822            // ResolvePath may return a generic Video for files in mixed-content libraries
 823            // where collectionType is null, even though the primary is a Movie/Episode/etc.
 0824            if (resolved.GetType() != expectedVideoType)
 825            {
 0826                if (Activator.CreateInstance(expectedVideoType) is Video correctVideo)
 827                {
 0828                    correctVideo.Path = resolved.Path;
 0829                    correctVideo.Name = resolved.Name;
 0830                    correctVideo.VideoType = resolved.VideoType;
 0831                    correctVideo.ProductionYear = resolved.ProductionYear;
 0832                    correctVideo.ExtraType = resolved.ExtraType;
 0833                    resolved = correctVideo;
 834                }
 835            }
 836
 0837            resolved.Id = expectedId;
 0838            return resolved;
 839        }
 840
 841        private BaseItem? ResolvePath(
 842            FileSystemMetadata fileInfo,
 843            IDirectoryService directoryService,
 844            IItemResolver[]? resolvers,
 845            Folder? parent = null,
 846            CollectionType? collectionType = null,
 847            LibraryOptions? libraryOptions = null)
 848        {
 76849            ArgumentNullException.ThrowIfNull(fileInfo);
 850
 76851            var fullPath = fileInfo.FullName;
 852
 76853            if (collectionType is null && parent is not null)
 854            {
 17855                collectionType = GetContentTypeOverride(fullPath, true);
 856            }
 857
 76858            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
 76859            {
 76860                Parent = parent,
 76861                FileInfo = fileInfo,
 76862                CollectionType = collectionType,
 76863                LibraryOptions = libraryOptions
 76864            };
 865
 866            // Return null if ignore rules deem that we should do so
 76867            if (IgnoreFile(args.FileInfo, args.Parent))
 868            {
 0869                return null;
 870            }
 871
 872            // Gather child folder and files
 76873            if (args.IsDirectory)
 874            {
 49875                var isPhysicalRoot = args.IsPhysicalRoot;
 876
 877                // When resolving the root, we need it's grandchildren (children of user views)
 49878                var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
 879
 880                FileSystemMetadata[] files;
 49881                var isVf = args.IsVf;
 882
 883                try
 884                {
 49885                    files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _l
 49886                }
 0887                catch (Exception ex)
 888                {
 0889                    if (parent is not null && parent.IsPhysicalRoot)
 890                    {
 0891                        _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPh
 892
 0893                        files = [];
 894                    }
 895                    else
 896                    {
 0897                        throw;
 898                    }
 0899                }
 900
 901                // Need to remove sub-paths that may have been resolved from shortcuts
 902                // Example: if \\server\movies exists, then strip out \\server\movies\action
 49903                if (isPhysicalRoot)
 904                {
 21905                    files = NormalizeRootPathList(files).ToArray();
 906                }
 907
 49908                args.FileSystemChildren = files;
 909            }
 910
 911            // Filter content based on ignore rules
 76912            if (args.IsDirectory)
 913            {
 49914                var filtered = args.GetActualFileSystemChildren().ToArray();
 49915                args.FileSystemChildren = filtered ?? [];
 916            }
 917
 76918            return ResolveItem(args, resolvers);
 919        }
 920
 921        public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent)
 100922            => EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent));
 923
 924        public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
 925        {
 80926            var originalList = paths.ToList();
 927
 80928            var list = originalList.Where(i => i.IsDirectory)
 80929                .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
 80930                .Distinct()
 80931                .ToList();
 932
 80933            var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.Ordinal) && list.Any(i => _fileS
 80934                .ToList();
 935
 160936            foreach (var dupe in dupes)
 937            {
 0938                _logger.LogInformation("Found duplicate path: {0}", dupe);
 939            }
 940
 80941            var newList = list.Except(dupes, StringComparer.Ordinal).Select(_fileSystem.GetDirectoryInfo).ToList();
 80942            newList.AddRange(originalList.Where(i => !i.IsDirectory));
 80943            return newList;
 944        }
 945
 946        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryServ
 947        {
 58948            return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
 949        }
 950
 951        public IEnumerable<BaseItem> ResolvePaths(
 952            IEnumerable<FileSystemMetadata> files,
 953            IDirectoryService directoryService,
 954            Folder parent,
 955            LibraryOptions libraryOptions,
 956            CollectionType? collectionType,
 957            IItemResolver[] resolvers)
 958        {
 58959            var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
 960
 58961            if (parent is not null)
 962            {
 58963                var multiItemResolvers = resolvers is null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>()
 964
 348965                foreach (var resolver in multiItemResolvers)
 966                {
 116967                    var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
 968
 116969                    if (result?.Items.Count > 0)
 970                    {
 0971                        var items = result.Items;
 0972                        items.RemoveAll(item => !ResolverHelper.SetInitialItemValues(item, parent, this, directoryServic
 0973                        items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, reso
 0974                        return items;
 975                    }
 976                }
 977            }
 978
 58979            return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions);
 0980        }
 981
 982        private IEnumerable<BaseItem> ResolveFileList(
 983            IReadOnlyList<FileSystemMetadata> fileList,
 984            IDirectoryService directoryService,
 985            Folder? parent,
 986            CollectionType? collectionType,
 987            IItemResolver[]? resolvers,
 988            LibraryOptions libraryOptions)
 989        {
 990            // Given that fileList is a list we can save enumerator allocations by indexing
 150991            for (var i = 0; i < fileList.Count; i++)
 992            {
 17993                var file = fileList[i];
 17994                BaseItem? result = null;
 995                try
 996                {
 17997                    result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
 17998                }
 0999                catch (Exception ex)
 1000                {
 01001                    _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
 01002                }
 1003
 171004                if (result is not null)
 1005                {
 71006                    yield return result;
 1007                }
 1008            }
 581009        }
 1010
 1011        /// <summary>
 1012        /// Creates the root media folder.
 1013        /// </summary>
 1014        /// <returns>AggregateFolder.</returns>
 1015        /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</except
 1016        public AggregateFolder CreateRootFolder()
 1017        {
 211018            var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath;
 1019
 211020            var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
 211021                             (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOp
 211022                             .DeepCopy<Folder, AggregateFolder>();
 1023
 1024            // In case program data folder was moved
 211025            if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal))
 1026            {
 01027                _logger.LogInformation("Resetting root folder path to {0}", rootFolderPath);
 01028                rootFolder.Path = rootFolderPath;
 1029            }
 1030
 1031            // Add in the plug-in folders
 211032            var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
 1033
 211034            var info = Directory.CreateDirectory(path);
 211035            Folder folder = new PlaylistsFolder
 211036            {
 211037                Path = path,
 211038                DateCreated = info.CreationTimeUtc,
 211039                DateModified = info.LastWriteTimeUtc,
 211040            };
 1041
 211042            if (folder.Id.IsEmpty())
 1043            {
 211044                folder.Id = GetNewItemId(folder.Path, folder.GetType());
 1045            }
 1046
 211047            var dbItem = GetItemById(folder.Id) as BasePluginFolder;
 1048
 211049            if (dbItem is not null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
 1050            {
 01051                folder = dbItem;
 1052            }
 1053
 211054            if (!folder.ParentId.Equals(rootFolder.Id))
 1055            {
 211056                rootFolder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().G
 211057                folder.ParentId = rootFolder.Id;
 211058                folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetRe
 1059            }
 1060
 211061            rootFolder.AddVirtualChild(folder);
 1062
 211063            RegisterItem(folder);
 1064
 211065            return rootFolder;
 1066        }
 1067
 1068        public Folder GetUserRootFolder()
 1069        {
 8511070            if (_userRootFolder is null)
 211071            {
 1072                lock (_userRootFolderSyncLock)
 1073                {
 211074                    if (_userRootFolder is null)
 1075                    {
 211076                        var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 1077
 211078                        _logger.LogDebug("Creating userRootPath at {Path}", userRootPath);
 211079                        Directory.CreateDirectory(userRootPath);
 1080
 211081                        var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
 211082                        UserRootFolder? tmpItem = null;
 1083                        try
 1084                        {
 211085                            tmpItem = GetItemById(newItemId) as UserRootFolder;
 211086                        }
 01087                        catch (Exception ex)
 1088                        {
 01089                            _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId);
 01090                        }
 1091
 211092                        if (tmpItem is null)
 1093                        {
 211094                            _logger.LogDebug("Creating new userRootFolder with DeepCopy");
 211095                            tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new In
 211096                                        .DeepCopy<Folder, UserRootFolder>();
 1097                        }
 1098
 1099                        // In case program data folder was moved
 211100                        if (!string.Equals(tmpItem.Path, userRootPath, StringComparison.Ordinal))
 1101                        {
 01102                            _logger.LogInformation("Resetting user root folder path to {0}", userRootPath);
 01103                            tmpItem.Path = userRootPath;
 1104                        }
 1105
 211106                        _userRootFolder = tmpItem;
 211107                        _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder);
 1108                    }
 211109                }
 1110            }
 1111
 8511112            return _userRootFolder;
 1113        }
 1114
 1115        /// <inheritdoc />
 1116        public BaseItem? FindByPath(string path, bool? isFolder)
 1117        {
 1118            // If this returns multiple items it could be tricky figuring out which one is correct.
 1119            // In most cases, the newest one will be and the others obsolete but not yet cleaned up
 01120            ArgumentException.ThrowIfNullOrEmpty(path);
 1121
 01122            var query = new InternalItemsQuery
 01123            {
 01124                Path = path,
 01125                IsFolder = isFolder,
 01126                OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)],
 01127                Limit = 1,
 01128                DtoOptions = new DtoOptions(true)
 01129            };
 1130
 01131            return GetItemList(query)
 01132                .FirstOrDefault();
 1133        }
 1134
 1135        /// <inheritdoc />
 1136        public Person? GetPerson(string name)
 1137        {
 11138            var path = Person.GetPath(name);
 11139            var id = GetItemByNameId<Person>(path);
 11140            if (GetItemById(id) is Person item)
 1141            {
 01142                return item;
 1143            }
 1144
 11145            return null;
 1146        }
 1147
 1148        /// <summary>
 1149        /// Gets the studio.
 1150        /// </summary>
 1151        /// <param name="name">The name.</param>
 1152        /// <returns>Task{Studio}.</returns>
 1153        public Studio GetStudio(string name)
 1154        {
 01155            return CreateItemByName<Studio>(Studio.GetPath, name, new DtoOptions(true));
 1156        }
 1157
 1158        public Guid GetStudioId(string name)
 1159        {
 01160            return GetItemByNameId<Studio>(Studio.GetPath(name));
 1161        }
 1162
 1163        public Guid GetGenreId(string name)
 1164        {
 01165            return GetItemByNameId<Genre>(Genre.GetPath(name));
 1166        }
 1167
 1168        public Guid GetMusicGenreId(string name)
 1169        {
 01170            return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
 1171        }
 1172
 1173        /// <summary>
 1174        /// Gets the genre.
 1175        /// </summary>
 1176        /// <param name="name">The name.</param>
 1177        /// <returns>Task{Genre}.</returns>
 1178        public Genre GetGenre(string name)
 1179        {
 01180            return CreateItemByName<Genre>(Genre.GetPath, name, new DtoOptions(true));
 1181        }
 1182
 1183        /// <summary>
 1184        /// Gets the music genre.
 1185        /// </summary>
 1186        /// <param name="name">The name.</param>
 1187        /// <returns>Task{MusicGenre}.</returns>
 1188        public MusicGenre GetMusicGenre(string name)
 1189        {
 01190            return CreateItemByName<MusicGenre>(MusicGenre.GetPath, name, new DtoOptions(true));
 1191        }
 1192
 1193        /// <summary>
 1194        /// Gets the year.
 1195        /// </summary>
 1196        /// <param name="value">The value.</param>
 1197        /// <returns>Task{Year}.</returns>
 1198        public Year GetYear(int value)
 1199        {
 01200            if (value <= 0)
 1201            {
 01202                throw new ArgumentOutOfRangeException(nameof(value), "Years less than or equal to 0 are invalid.");
 1203            }
 1204
 01205            var name = value.ToString(CultureInfo.InvariantCulture);
 1206
 01207            return CreateItemByName<Year>(Year.GetPath, name, new DtoOptions(true));
 1208        }
 1209
 1210        /// <summary>
 1211        /// Gets a Genre.
 1212        /// </summary>
 1213        /// <param name="name">The name.</param>
 1214        /// <returns>Task{Genre}.</returns>
 1215        public MusicArtist GetArtist(string name)
 1216        {
 01217            return GetArtist(name, new DtoOptions(true));
 1218        }
 1219
 1220        public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
 1221        {
 161222            return _linkedChildrenService.FindArtists(names);
 1223        }
 1224
 1225        public MusicArtist GetArtist(string name, DtoOptions options)
 1226        {
 01227            return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options);
 1228        }
 1229
 1230        private T CreateItemByName<T>(Func<string, string> getPathFn, string name, DtoOptions options)
 1231            where T : BaseItem, new()
 1232        {
 01233            if (typeof(T) == typeof(MusicArtist))
 1234            {
 01235                var existing = GetItemList(new InternalItemsQuery
 01236                {
 01237                    IncludeItemTypes = [BaseItemKind.MusicArtist],
 01238                    Name = name,
 01239                    UseRawName = true,
 01240                    DtoOptions = options
 01241                }).Cast<MusicArtist>()
 01242                .OrderBy(i => i.IsAccessedByName ? 1 : 0)
 01243                .Cast<T>()
 01244                .FirstOrDefault();
 1245
 01246                if (existing is not null)
 1247                {
 01248                    return existing;
 1249                }
 1250            }
 1251
 01252            var path = getPathFn(name);
 01253            var id = GetItemByNameId<T>(path);
 01254            var item = GetItemById(id) as T;
 01255            if (item is null)
 1256            {
 01257                var info = Directory.CreateDirectory(path);
 01258                item = new T
 01259                {
 01260                    Name = name,
 01261                    Id = id,
 01262                    DateCreated = info.CreationTimeUtc,
 01263                    DateModified = info.LastWriteTimeUtc,
 01264                    Path = path
 01265                };
 1266
 01267                CreateItem(item, null);
 1268            }
 1269
 01270            return item;
 1271        }
 1272
 1273        private Guid GetItemByNameId<T>(string path)
 1274              where T : BaseItem, new()
 1275        {
 11276            var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
 11277            return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
 1278        }
 1279
 1280        /// <inheritdoc />
 1281        public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
 1282        {
 1283            // Ensure the location is available.
 01284            Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
 1285
 01286            return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
 1287        }
 1288
 1289        /// <summary>
 1290        /// Reloads the root media folder.
 1291        /// </summary>
 1292        /// <param name="progress">The progress.</param>
 1293        /// <param name="cancellationToken">The cancellation token.</param>
 1294        /// <returns>Task.</returns>
 1295        public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
 1296        {
 1297            // Just run the scheduled task so that the user can see it
 31298            _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 1299
 31300            return Task.CompletedTask;
 1301        }
 1302
 1303        /// <summary>
 1304        /// Validates the media library internal.
 1305        /// </summary>
 1306        /// <param name="progress">The progress.</param>
 1307        /// <param name="cancellationToken">The cancellation token.</param>
 1308        /// <returns>Task.</returns>
 1309        public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
 1310        {
 191311            IsScanRunning = true;
 191312            ClearIgnoreRuleCache();
 191313            LibraryMonitor.Stop();
 1314
 1315            try
 1316            {
 191317                await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false);
 141318            }
 1319            finally
 1320            {
 191321                ClearIgnoreRuleCache();
 191322                LibraryMonitor.Start();
 191323                IsScanRunning = false;
 1324            }
 141325        }
 1326
 1327        public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
 1328        {
 221329            ClearIgnoreRuleCache();
 221330            RootFolder.Children = null;
 221331            await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1332
 1333            // Start by just validating the children of the root, but go no further
 221334            await RootFolder.ValidateChildren(
 221335                new Progress<double>(),
 221336                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 221337                recursive: false,
 221338                allowRemoveRoot: removeRoot,
 221339                cancellationToken: cancellationToken).ConfigureAwait(false);
 1340
 211341            var rootFolder = GetUserRootFolder();
 211342            rootFolder.Children = null;
 1343
 211344            await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1345
 201346            await rootFolder.ValidateChildren(
 201347                new Progress<double>(),
 201348                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 201349                recursive: false,
 201350                allowRemoveRoot: removeRoot,
 201351                cancellationToken: cancellationToken).ConfigureAwait(false);
 1352
 1353            // Quickly scan CollectionFolders for changes
 201354            var toDelete = new List<Guid>();
 561355            foreach (var child in rootFolder.Children!.OfType<Folder>())
 1356            {
 1357                // If the user has somehow deleted the collection directory, remove the metadata from the database.
 81358                if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
 1359                {
 11360                    toDelete.Add(collectionFolder.Id);
 1361                }
 1362                else
 1363                {
 71364                    await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1365                }
 1366            }
 1367
 201368            if (toDelete.Count > 0)
 1369            {
 11370                _persistenceService.DeleteItem(toDelete.ToArray());
 1371            }
 1372
 201373            ClearIgnoreRuleCache();
 201374        }
 1375
 1376        /// <inheritdoc />
 1377        public void ClearIgnoreRuleCache()
 1378        {
 801379            _dotIgnoreIgnoreRule.ClearDirectoryCache();
 801380        }
 1381
 1382        private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
 1383        {
 191384            _logger.LogInformation("Validating media library");
 1385
 191386            await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false);
 1387
 171388            var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96));
 1389
 1390            // Validate the entire media library
 171391            await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem
 1392
 161393            progress.Report(96);
 1394
 161395            innerProgress = new Progress<double>(pct => progress.Report(96 + (pct * .04)));
 1396
 161397            await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
 1398
 141399            progress.Report(100);
 141400        }
 1401
 1402        /// <summary>
 1403        /// Runs the post scan tasks.
 1404        /// </summary>
 1405        /// <param name="progress">The progress.</param>
 1406        /// <param name="cancellationToken">The cancellation token.</param>
 1407        /// <returns>Task.</returns>
 1408        private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
 1409        {
 161410            var tasks = PostScanTasks.ToList();
 1411
 161412            var numComplete = 0;
 161413            var numTasks = tasks.Count;
 1414
 2541415            foreach (var task in tasks)
 1416            {
 1417                // Prevent access to modified closure
 1121418                var currentNumComplete = numComplete;
 1419
 1121420                var innerProgress = new Progress<double>(pct =>
 1121421                {
 1121422                    double innerPercent = pct;
 1121423                    innerPercent /= 100;
 1121424                    innerPercent += currentNumComplete;
 1121425
 1121426                    innerPercent /= numTasks;
 1121427                    innerPercent *= 100;
 1121428
 1121429                    progress.Report(innerPercent);
 1121430                });
 1431
 1121432                _logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
 1433
 1434                try
 1435                {
 1121436                    await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
 1101437                }
 21438                catch (OperationCanceledException)
 1439                {
 21440                    _logger.LogInformation("Post-scan task cancelled: {0}", task.GetType().Name);
 21441                    throw;
 1442                }
 01443                catch (Exception ex)
 1444                {
 01445                    _logger.LogError(ex, "Error running post-scan task");
 01446                }
 1447
 1101448                numComplete++;
 1101449                double percent = numComplete;
 1101450                percent /= numTasks;
 1101451                progress.Report(percent * 100);
 1101452            }
 1453
 141454            _persistenceService.UpdateInheritedValues();
 1455
 141456            progress.Report(100);
 141457        }
 1458
 1459        /// <summary>
 1460        /// Gets the default view.
 1461        /// </summary>
 1462        /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
 1463        public List<VirtualFolderInfo> GetVirtualFolders()
 1464        {
 231465            return GetVirtualFolders(false);
 1466        }
 1467
 1468        public List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState)
 1469        {
 241470            _logger.LogDebug("Getting topLibraryFolders");
 241471            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 1472
 241473            _logger.LogDebug("Getting refreshQueue");
 241474            var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null;
 1475
 241476            return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath)
 241477                .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
 241478                .ToList();
 1479        }
 1480
 1481        private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid>? r
 1482        {
 11483            var info = new VirtualFolderInfo
 11484            {
 11485                Name = Path.GetFileName(dir),
 11486
 11487                Locations = _fileSystem.GetFilePaths(dir, false)
 11488                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 11489                    .Select(i =>
 11490                    {
 11491                        try
 11492                        {
 11493                            return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i));
 11494                        }
 11495                        catch (Exception ex)
 11496                        {
 11497                            _logger.LogError(ex, "Error resolving shortcut file {File}", i);
 11498                            return null;
 11499                        }
 11500                    })
 11501                    .Where(i => i is not null)
 11502                    .Order()
 11503                    .ToArray(),
 11504
 11505                CollectionType = GetCollectionType(dir)
 11506            };
 1507
 11508            var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.Ord
 11509            if (libraryFolder is not null)
 1510            {
 11511                var libraryFolderId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture);
 11512                info.ItemId = libraryFolderId;
 11513                if (libraryFolder.HasImage(ImageType.Primary))
 1514                {
 01515                    info.PrimaryImageItemId = libraryFolderId;
 1516                }
 1517
 11518                info.LibraryOptions = GetLibraryOptions(libraryFolder);
 1519
 11520                if (refreshQueue is not null)
 1521                {
 11522                    info.RefreshProgress = libraryFolder.GetRefreshProgress();
 1523
 11524                    info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.Contains(libraryFolder.
 1525                }
 1526            }
 1527
 11528            return info;
 1529        }
 1530
 1531        private CollectionTypeOptions? GetCollectionType(string path)
 1532        {
 11533            var files = _fileSystem.GetFilePaths(path, [".collection"], true, false);
 21534            foreach (ReadOnlySpan<char> file in files)
 1535            {
 01536                if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
 1537                {
 01538                    return res;
 1539                }
 1540            }
 1541
 11542            return null;
 01543        }
 1544
 1545        /// <inheritdoc />
 1546        public BaseItem? GetItemById(Guid id)
 1547        {
 4801548            if (id.IsEmpty())
 1549            {
 01550                throw new ArgumentException("Guid can't be empty", nameof(id));
 1551            }
 1552
 4801553            if (_cache.TryGet(id, out var item))
 1554            {
 3771555                return item;
 1556            }
 1557
 1031558            item = RetrieveItem(id);
 1559
 1031560            if (item is not null)
 1561            {
 01562                RegisterItem(item);
 1563            }
 1564
 1031565            return item;
 1566        }
 1567
 1568        /// <inheritdoc />
 1569        public T? GetItemById<T>(Guid id)
 1570            where T : BaseItem
 1571        {
 231572            var item = GetItemById(id);
 231573            if (item is T typedItem)
 1574            {
 11575                return typedItem;
 1576            }
 1577
 221578            return null;
 1579        }
 1580
 1581        /// <inheritdoc />
 1582        public T? GetItemById<T>(Guid id, Guid userId)
 1583            where T : BaseItem
 1584        {
 11585            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 11586            return GetItemById<T>(id, user);
 1587        }
 1588
 1589        /// <inheritdoc />
 1590        public T? GetItemById<T>(Guid id, User? user)
 1591            where T : BaseItem
 1592        {
 211593            var item = GetItemById<T>(id);
 211594            return ItemIsVisible(item, user) ? item : null;
 1595        }
 1596
 1597        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
 1598        {
 1671599            if (query.Recursive && !query.ParentId.IsEmpty())
 1600            {
 431601                var parent = GetItemById(query.ParentId);
 431602                if (parent is not null)
 1603                {
 431604                    SetTopParentIdsOrAncestors(query, [parent]);
 1605                }
 1606            }
 1607
 1671608            if (query.User is not null)
 1609            {
 11610                AddUserToQuery(query, query.User, allowExternalContent);
 1611            }
 1612
 1671613            return _itemRepository.GetItemList(query);
 1614        }
 1615
 1616        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 1617        {
 1671618            return GetItemList(query, true);
 1619        }
 1620
 1621        public int GetCount(InternalItemsQuery query)
 1622        {
 01623            if (query.Recursive && !query.ParentId.IsEmpty())
 1624            {
 01625                var parent = GetItemById(query.ParentId);
 01626                if (parent is not null)
 1627                {
 01628                    SetTopParentIdsOrAncestors(query, [parent]);
 1629                }
 1630            }
 1631
 01632            if (query.User is not null)
 1633            {
 01634                AddUserToQuery(query, query.User);
 1635            }
 1636
 01637            return _countService.GetCount(query);
 1638        }
 1639
 1640        public ItemCounts GetItemCounts(InternalItemsQuery query)
 1641        {
 01642            if (query.Recursive && !query.ParentId.IsEmpty())
 1643            {
 01644                var parent = GetItemById(query.ParentId);
 01645                if (parent is not null)
 1646                {
 01647                    SetTopParentIdsOrAncestors(query, [parent]);
 1648                }
 1649            }
 1650
 01651            if (query.User is not null)
 1652            {
 01653                AddUserToQuery(query, query.User);
 1654            }
 1655
 01656            return _countService.GetItemCounts(query);
 1657        }
 1658
 1659        /// <inheritdoc/>
 1660        public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? us
 1661        {
 01662            var query = new InternalItemsQuery(user);
 01663            if (user is not null)
 1664            {
 01665                AddUserToQuery(query, user);
 1666            }
 1667
 01668            return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
 1669        }
 1670
 1671        public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
 1672        {
 01673            return _countService.GetChildCountBatch(parentIds, userId);
 1674        }
 1675
 1676        /// <inheritdoc/>
 1677        public Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User
 1678        {
 01679            return _countService.GetPlayedAndTotalCountBatch(folderIds, user);
 1680        }
 1681
 1682        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
 1683        {
 01684            SetTopParentIdsOrAncestors(query, parents);
 1685
 01686            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1687            {
 01688                if (query.User is not null)
 1689                {
 01690                    AddUserToQuery(query, query.User);
 1691                }
 1692            }
 1693
 01694            return _itemRepository.GetItemList(query);
 1695        }
 1696
 1697        public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, Coll
 1698        {
 01699            SetTopParentIdsOrAncestors(query, parents);
 1700
 01701            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1702            {
 01703                if (query.User is not null)
 1704                {
 01705                    AddUserToQuery(query, query.User);
 1706                }
 1707            }
 1708
 01709            return _itemRepository.GetLatestItemList(query, collectionType);
 1710        }
 1711
 1712        public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents
 1713        {
 01714            SetTopParentIdsOrAncestors(query, parents);
 1715
 01716            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1717            {
 01718                if (query.User is not null)
 1719                {
 01720                    AddUserToQuery(query, query.User);
 1721                }
 1722            }
 1723
 01724            return _nextUpService.GetNextUpSeriesKeys(query, dateCutoff);
 1725        }
 1726
 1727        /// <inheritdoc />
 1728        public IReadOnlyDictionary<string, MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult> GetNextUpEpisod
 1729            InternalItemsQuery query,
 1730            IReadOnlyList<string> seriesKeys,
 1731            bool includeSpecials,
 1732            bool includeWatchedForRewatching)
 1733        {
 01734            return _nextUpService.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeWatchedForRewatching
 1735        }
 1736
 1737        public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
 1738        {
 01739            if (query.User is not null)
 1740            {
 01741                AddUserToQuery(query, query.User);
 1742            }
 1743
 01744            if (query.EnableTotalRecordCount)
 1745            {
 01746                return _itemRepository.GetItems(query);
 1747            }
 1748
 01749            return new QueryResult<BaseItem>(
 01750                query.StartIndex,
 01751                null,
 01752                _itemRepository.GetItemList(query));
 1753        }
 1754
 1755        public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
 1756        {
 801757            if (query.User is not null)
 1758            {
 01759                AddUserToQuery(query, query.User);
 1760            }
 1761
 801762            return _itemRepository.GetItemIdsList(query);
 1763        }
 1764
 1765        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
 1766        {
 01767            if (query.User is not null)
 1768            {
 01769                AddUserToQuery(query, query.User);
 1770            }
 1771
 01772            SetTopParentOrAncestorIds(query);
 01773            return _itemRepository.GetStudios(query);
 1774        }
 1775
 1776        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
 1777        {
 01778            if (query.User is not null)
 1779            {
 01780                AddUserToQuery(query, query.User);
 1781            }
 1782
 01783            SetTopParentOrAncestorIds(query);
 01784            return _itemRepository.GetGenres(query);
 1785        }
 1786
 1787        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
 1788        {
 01789            if (query.User is not null)
 1790            {
 01791                AddUserToQuery(query, query.User);
 1792            }
 1793
 01794            SetTopParentOrAncestorIds(query);
 01795            return _itemRepository.GetMusicGenres(query);
 1796        }
 1797
 1798        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
 1799        {
 01800            if (query.User is not null)
 1801            {
 01802                AddUserToQuery(query, query.User);
 1803            }
 1804
 01805            SetTopParentOrAncestorIds(query);
 01806            return _itemRepository.GetAllArtists(query);
 1807        }
 1808
 1809        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
 1810        {
 01811            if (query.User is not null)
 1812            {
 01813                AddUserToQuery(query, query.User);
 1814            }
 1815
 01816            SetTopParentOrAncestorIds(query);
 01817            return _itemRepository.GetArtists(query);
 1818        }
 1819
 1820        private void SetTopParentOrAncestorIds(InternalItemsQuery query)
 1821        {
 01822            var ancestorIds = query.AncestorIds;
 01823            int len = ancestorIds.Length;
 01824            if (len == 0)
 1825            {
 01826                return;
 1827            }
 1828
 01829            var parents = new BaseItem[len];
 01830            for (int i = 0; i < len; i++)
 1831            {
 01832                parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id:
 01833                if (parents[i] is not (ICollectionFolder or UserView))
 1834                {
 01835                    return;
 1836                }
 1837            }
 1838
 1839            // Optimize by querying against top level views
 01840            query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 01841            query.AncestorIds = [];
 1842
 1843            // Prevent searching in all libraries due to empty filter
 01844            if (query.TopParentIds.Length == 0)
 1845            {
 01846                query.TopParentIds = [Guid.NewGuid()];
 1847            }
 01848        }
 1849
 1850        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
 1851        {
 01852            if (query.User is not null)
 1853            {
 01854                AddUserToQuery(query, query.User);
 1855            }
 1856
 01857            SetTopParentOrAncestorIds(query);
 01858            return _itemRepository.GetAlbumArtists(query);
 1859        }
 1860
 1861        public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
 1862        {
 151863            if (query.Recursive && !query.ParentId.IsEmpty())
 1864            {
 141865                var parent = GetItemById(query.ParentId);
 141866                if (parent is not null)
 1867                {
 141868                    SetTopParentIdsOrAncestors(query, [parent]);
 1869                }
 1870            }
 1871
 151872            if (query.User is not null)
 1873            {
 11874                AddUserToQuery(query, query.User);
 1875            }
 1876
 151877            if (query.EnableTotalRecordCount)
 1878            {
 11879                return _itemRepository.GetItems(query);
 1880            }
 1881
 141882            return new QueryResult<BaseItem>(
 141883                query.StartIndex,
 141884                null,
 141885                _itemRepository.GetItemList(query));
 1886        }
 1887
 1888        private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
 1889        {
 571890            if (parents.All(i => i is ICollectionFolder || i is UserView))
 1891            {
 1892                // Optimize by querying against top level views
 141893                query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 1894
 1895                // Prevent searching in all libraries due to empty filter
 141896                if (query.TopParentIds.Length == 0)
 1897                {
 141898                    query.TopParentIds = [Guid.NewGuid()];
 1899                }
 1900            }
 431901            else if (parents.Count == 1 && parents.First() is Folder folder
 431902                && (folder is Playlist || folder is BoxSet)
 431903                && folder.LinkedChildren.Length > 0)
 1904            {
 1905                // Playlists and BoxSets store their contents in LinkedChildren and never
 1906                // populate AncestorIds for those items, so a recursive AncestorIds query
 1907                // would return zero rows. Resolve to the linked child IDs up front and
 1908                // route through the existing indexed ItemIds filter.
 01909                query.ItemIds = folder.LinkedChildren
 01910                    .Where(lc => lc.ItemId.HasValue && !lc.ItemId.Value.IsEmpty())
 01911                    .Select(lc => lc.ItemId!.Value)
 01912                    .ToArray();
 1913
 1914                // Empty linked-children should still return empty rather than scanning everything.
 01915                if (query.ItemIds.Length == 0)
 1916                {
 01917                    query.ItemIds = [Guid.NewGuid()];
 1918                }
 1919            }
 1920            else
 1921            {
 1922                // We need to be able to query from any arbitrary ancestor up the tree
 431923                query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
 1924
 1925                // Prevent searching in all libraries due to empty filter
 431926                if (query.AncestorIds.Length == 0)
 1927                {
 01928                    query.AncestorIds = [Guid.NewGuid()];
 1929                }
 1930            }
 1931
 571932            query.Parent = null;
 571933        }
 1934
 1935        private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
 1936        {
 21937            if (query.User is null)
 1938            {
 01939                query.SetUser(user);
 1940            }
 1941
 21942            if (query.AncestorIds.Length == 0 &&
 21943                query.ParentId.IsEmpty() &&
 21944                query.ChannelIds.Count == 0 &&
 21945                query.TopParentIds.Length == 0 &&
 21946                string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
 21947                string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
 21948                query.ItemIds.Length == 0)
 1949            {
 11950                var userViews = UserViewManager.GetUserViews(new UserViewQuery
 11951                {
 11952                    User = user,
 11953                    IncludeHidden = true,
 11954                    IncludeExternalContent = allowExternalContent
 11955                });
 1956
 11957                query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
 1958
 1959                // Prevent searching in all libraries due to empty filter
 11960                if (query.TopParentIds.Length == 0)
 1961                {
 11962                    query.TopParentIds = [Guid.NewGuid()];
 1963                }
 1964            }
 21965        }
 1966
 1967        /// <inheritdoc/>
 1968        public void ConfigureUserAccess(InternalItemsQuery query, User user)
 1969        {
 01970            ArgumentNullException.ThrowIfNull(query);
 01971            ArgumentNullException.ThrowIfNull(user);
 1972
 01973            AddUserToQuery(query, user);
 01974        }
 1975
 1976        private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
 1977        {
 141978            if (item is UserView view)
 1979            {
 01980                if (view.ViewType == CollectionType.livetv)
 1981                {
 01982                    return [view.Id];
 1983                }
 1984
 1985                // Translate view into folders
 01986                if (!view.DisplayParentId.IsEmpty())
 1987                {
 01988                    var displayParent = GetItemById(view.DisplayParentId);
 01989                    if (displayParent is not null)
 1990                    {
 01991                        return GetTopParentIdsForQuery(displayParent, user);
 1992                    }
 1993
 01994                    return [];
 1995                }
 1996
 01997                if (!view.ParentId.IsEmpty())
 1998                {
 01999                    var displayParent = GetItemById(view.ParentId);
 02000                    if (displayParent is not null)
 2001                    {
 02002                        return GetTopParentIdsForQuery(displayParent, user);
 2003                    }
 2004
 02005                    return [];
 2006                }
 2007
 2008                // Handle grouping
 02009                if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.V
 02010                    && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
 2011                {
 02012                    return GetUserRootFolder()
 02013                        .GetChildren(user, true)
 02014                        .OfType<CollectionFolder>()
 02015                        .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
 02016                        .Where(i => user.IsFolderGrouped(i.Id))
 02017                        .SelectMany(i => GetTopParentIdsForQuery(i, user));
 2018                }
 2019
 02020                return [];
 2021            }
 2022
 142023            if (item is CollectionFolder collectionFolder)
 2024            {
 142025                return collectionFolder.PhysicalFolderIds;
 2026            }
 2027
 02028            var topParent = item.GetTopParent();
 02029            if (topParent is not null)
 2030            {
 02031                return [topParent.Id];
 2032            }
 2033
 02034            return [];
 2035        }
 2036
 2037        /// <summary>
 2038        /// Gets the intros.
 2039        /// </summary>
 2040        /// <param name="item">The item.</param>
 2041        /// <param name="user">The user.</param>
 2042        /// <returns>IEnumerable{System.String}.</returns>
 2043        public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
 2044        {
 02045            if (IntroProviders.Length == 0)
 2046            {
 02047                return [];
 2048            }
 2049
 02050            var tasks = IntroProviders
 02051                .Select(i => GetIntros(i, item, user));
 2052
 02053            var items = await Task.WhenAll(tasks).ConfigureAwait(false);
 2054
 02055            return items
 02056                .SelectMany(i => i)
 02057                .Select(ResolveIntro)
 02058                .Where(i => i is not null)!; // null values got filtered out
 02059        }
 2060
 2061        /// <summary>
 2062        /// Gets the intros.
 2063        /// </summary>
 2064        /// <param name="provider">The provider.</param>
 2065        /// <param name="item">The item.</param>
 2066        /// <param name="user">The user.</param>
 2067        /// <returns>Task&lt;IEnumerable&lt;IntroInfo&gt;&gt;.</returns>
 2068        private async Task<IEnumerable<IntroInfo>> GetIntros(IIntroProvider provider, BaseItem item, User user)
 2069        {
 2070            try
 2071            {
 02072                return await provider.GetIntros(item, user).ConfigureAwait(false);
 2073            }
 02074            catch (Exception ex)
 2075            {
 02076                _logger.LogError(ex, "Error getting intros");
 2077
 02078                return [];
 2079            }
 02080        }
 2081
 2082        /// <summary>
 2083        /// Resolves the intro.
 2084        /// </summary>
 2085        /// <param name="info">The info.</param>
 2086        /// <returns>Video.</returns>
 2087        private Video? ResolveIntro(IntroInfo info)
 2088        {
 02089            Video? video = null;
 2090
 02091            if (info.ItemId.HasValue)
 2092            {
 2093                // Get an existing item by Id
 02094                video = GetItemById(info.ItemId.Value) as Video;
 2095
 02096                if (video is null)
 2097                {
 02098                    _logger.LogError("Unable to locate item with Id {ID}.", info.ItemId.Value);
 2099                }
 2100            }
 02101            else if (!string.IsNullOrEmpty(info.Path))
 2102            {
 2103                try
 2104                {
 2105                    // Try to resolve the path into a video
 02106                    video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video;
 2107
 02108                    if (video is null)
 2109                    {
 02110                        _logger.LogError("Intro resolver returned null for {Path}.", info.Path);
 2111                    }
 2112                    else
 2113                    {
 2114                        // Pull the saved db item that will include metadata
 02115                        var dbItem = GetItemById(video.Id) as Video;
 2116
 02117                        if (dbItem is not null)
 2118                        {
 02119                            video = dbItem;
 2120                        }
 2121                        else
 2122                        {
 02123                            return null;
 2124                        }
 2125                    }
 02126                }
 02127                catch (Exception ex)
 2128                {
 02129                    _logger.LogError(ex, "Error resolving path {Path}.", info.Path);
 02130                }
 2131            }
 2132            else
 2133            {
 02134                _logger.LogError("IntroProvider returned an IntroInfo with null Path and ItemId.");
 2135            }
 2136
 02137            return video;
 02138        }
 2139
 2140        /// <inheritdoc />
 2141        public IEnumerable<Guid> GetLocalAlternateVersionIds(Video video)
 2142        {
 02143            ArgumentNullException.ThrowIfNull(video);
 2144
 02145            var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.
 02146            if (linkedIds.Count > 0)
 2147            {
 02148                return linkedIds;
 2149            }
 2150
 02151            return [];
 2152        }
 2153
 2154        /// <inheritdoc />
 2155        public IEnumerable<Video> GetLinkedAlternateVersions(Video video)
 2156        {
 02157            ArgumentNullException.ThrowIfNull(video);
 2158
 02159            var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.
 02160            if (linkedIds.Count > 0)
 2161            {
 02162                return linkedIds
 02163                    .Select(id => GetItemById(id))
 02164                    .Where(i => i is not null)
 02165                    .OfType<Video>()
 02166                    .OrderBy(i => i.SortName);
 2167            }
 2168
 02169            return [];
 2170        }
 2171
 2172        /// <inheritdoc />
 2173        public void UpsertLinkedChild(Guid parentId, Guid childId, MediaBrowser.Controller.Entities.LinkedChildType chil
 2174        {
 02175            _linkedChildrenService.UpsertLinkedChild(parentId, childId, childType);
 02176        }
 2177
 2178        /// <inheritdoc />
 2179        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortO
 2180        {
 12181            IOrderedEnumerable<BaseItem>? orderedItems = null;
 2182
 42183            foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null))
 2184            {
 12185                if (orderBy is RandomComparer)
 2186                {
 02187                    var randomItems = items.ToArray();
 02188                    Random.Shared.Shuffle(randomItems);
 02189                    items = randomItems;
 2190                    // Items are no longer ordered at this point, so set orderedItems back to null
 02191                    orderedItems = null;
 2192                }
 12193                else if (orderedItems is null)
 2194                {
 12195                    orderedItems = sortOrder == SortOrder.Descending
 12196                        ? items.OrderByDescending(i => i, orderBy)
 12197                        : items.OrderBy(i => i, orderBy);
 2198                }
 2199                else
 2200                {
 02201                    orderedItems = sortOrder == SortOrder.Descending
 02202                        ? orderedItems!.ThenByDescending(i => i, orderBy)
 02203                        : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration
 2204                }
 2205            }
 2206
 12207            return orderedItems ?? items;
 2208        }
 2209
 2210        /// <inheritdoc />
 2211        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, Sort
 2212        {
 02213            IOrderedEnumerable<BaseItem>? orderedItems = null;
 2214
 02215            foreach (var (name, sortOrder) in orderBy)
 2216            {
 02217                var comparer = GetComparer(name, user);
 02218                if (comparer is null)
 2219                {
 2220                    continue;
 2221                }
 2222
 02223                if (comparer is RandomComparer)
 2224                {
 02225                    var randomItems = items.ToArray();
 02226                    Random.Shared.Shuffle(randomItems);
 02227                    items = randomItems;
 2228                    // Items are no longer ordered at this point, so set orderedItems back to null
 02229                    orderedItems = null;
 2230                }
 02231                else if (orderedItems is null)
 2232                {
 02233                    orderedItems = sortOrder == SortOrder.Descending
 02234                        ? items.OrderByDescending(i => i, comparer)
 02235                        : items.OrderBy(i => i, comparer);
 2236                }
 2237                else
 2238                {
 02239                    orderedItems = sortOrder == SortOrder.Descending
 02240                        ? orderedItems!.ThenByDescending(i => i, comparer)
 02241                        : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration
 2242                }
 2243            }
 2244
 02245            return orderedItems ?? items;
 2246        }
 2247
 2248        /// <summary>
 2249        /// Gets the comparer.
 2250        /// </summary>
 2251        /// <param name="name">The name.</param>
 2252        /// <param name="user">The user.</param>
 2253        /// <returns>IBaseItemComparer.</returns>
 2254        private IBaseItemComparer? GetComparer(ItemSortBy name, User? user)
 2255        {
 12256            var comparer = Comparers.FirstOrDefault(c => name == c.Type);
 2257
 2258            // If it requires a user, create a new one, and assign the user
 12259            if (comparer is IUserBaseItemComparer)
 2260            {
 02261                var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null fo
 2262
 02263                userComparer.User = user;
 02264                userComparer.UserManager = _userManager;
 02265                userComparer.UserDataManager = _userDataManager;
 2266
 02267                return userComparer;
 2268            }
 2269
 12270            return comparer;
 2271        }
 2272
 2273        /// <inheritdoc />
 2274        public void CreateItem(BaseItem item, BaseItem? parent)
 2275        {
 02276            CreateItems([item], parent, CancellationToken.None);
 02277        }
 2278
 2279        /// <inheritdoc />
 2280        public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
 2281        {
 2282            // Resolve and add any local alternate version items that don't exist yet
 2283            // This ensures they exist in the database when LinkedChildren are processed
 22284            var allItems = new List<BaseItem>(items);
 22285            var parentFolder = parent as Folder;
 22286            var parentCollectionType = parent is not null ? GetTopFolderContentType(parent) : null;
 82287            foreach (var item in items)
 2288            {
 22289                if (item is Video video && video.LocalAlternateVersions.Length > 0)
 2290                {
 02291                    var videoType = video.GetType();
 02292                    foreach (var path in video.LocalAlternateVersions)
 2293                    {
 02294                        if (string.IsNullOrEmpty(path))
 2295                        {
 2296                            continue;
 2297                        }
 2298
 2299                        // Use the primary video's type for ID calculation to ensure consistency
 02300                        var altId = GetNewItemId(path, videoType);
 02301                        if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
 2302                        {
 2303                            // Alternate version doesn't exist, resolve and create it
 2304                            // ensuring it has the same type as the primary video
 02305                            var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
 02306                            if (altVideo is not null)
 2307                            {
 02308                                altVideo.OwnerId = video.Id;
 02309                                altVideo.SetPrimaryVersionId(video.Id);
 02310                                allItems.Add(altVideo);
 2311                            }
 2312                        }
 2313                    }
 2314                }
 2315            }
 2316
 22317            _persistenceService.SaveItems(allItems, cancellationToken);
 2318
 82319            foreach (var item in allItems)
 2320            {
 22321                RegisterItem(item);
 2322            }
 2323
 22324            if (parent is Folder folder)
 2325            {
 22326                folder.Children = null;
 22327                folder.UserData = null;
 2328            }
 2329
 22330            if (ItemAdded is not null)
 2331            {
 82332                foreach (var item in items)
 2333                {
 2334                    // With the live tv guide this just creates too much noise
 22335                    if (item.SourceType != SourceType.Library)
 2336                    {
 2337                        continue;
 2338                    }
 2339
 2340                    try
 2341                    {
 22342                        ItemAdded(
 22343                            this,
 22344                            new ItemChangeEventArgs
 22345                            {
 22346                                Item = item,
 22347                                Parent = parent ?? item.GetParent()
 22348                            });
 22349                    }
 02350                    catch (Exception ex)
 2351                    {
 02352                        _logger.LogError(ex, "Error in ItemAdded event handler");
 02353                    }
 2354                }
 2355            }
 22356        }
 2357
 2358        private bool ImageNeedsRefresh(ItemImageInfo image)
 2359        {
 02360            if (image.Path is not null && image.IsLocalFile)
 2361            {
 02362                if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash))
 2363                {
 02364                    return true;
 2365                }
 2366
 2367                try
 2368                {
 02369                    return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeco
 2370                }
 02371                catch (Exception ex)
 2372                {
 02373                    _logger.LogError(ex, "Cannot get file info for {0}", image.Path);
 02374                    return false;
 2375                }
 2376            }
 2377
 02378            return image.Path is not null && !image.IsLocalFile;
 02379        }
 2380
 2381        /// <inheritdoc />
 2382        public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false)
 2383        {
 1492384            ArgumentNullException.ThrowIfNull(item);
 2385
 1492386            var outdated = forceUpdate
 1492387                ? item.ImageInfos.Where(i => i.Path is not null).ToArray()
 1492388                : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
 2389            // Skip image processing if current or live tv source
 1492390            if (outdated.Length == 0 || item.SourceType != SourceType.Library)
 2391            {
 1492392                RegisterItem(item);
 1492393                return;
 2394            }
 2395
 02396            foreach (var img in outdated)
 2397            {
 02398                var image = img;
 02399                if (!img.IsLocalFile)
 2400                {
 2401                    try
 2402                    {
 02403                        var index = item.GetImageIndex(img);
 02404                        image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
 02405                    }
 02406                    catch (ArgumentException)
 2407                    {
 02408                        _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
 02409                        continue;
 2410                    }
 02411                    catch (Exception ex) when (ex is InvalidOperationException or IOException)
 2412                    {
 02413                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
 02414                        continue;
 2415                    }
 02416                    catch (HttpRequestException ex)
 2417                    {
 02418                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", im
 02419                        continue;
 2420                    }
 2421                }
 2422
 02423                if (!File.Exists(image.Path))
 2424                {
 02425                    _logger.LogWarning("Image not found at {ImagePath}", image.Path);
 02426                    continue;
 2427                }
 2428
 2429                ImageDimensions size;
 2430                try
 2431                {
 02432                    size = _imageProcessor.GetImageDimensions(item, image);
 02433                    image.Width = size.Width;
 02434                    image.Height = size.Height;
 02435                }
 02436                catch (Exception ex)
 2437                {
 02438                    _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
 02439                    size = default;
 02440                    image.Width = 0;
 02441                    image.Height = 0;
 02442                }
 2443
 2444                try
 2445                {
 02446                    var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size);
 02447                    image.BlurHash = blurhash;
 02448                }
 02449                catch (Exception ex)
 2450                {
 02451                    _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
 02452                    image.BlurHash = string.Empty;
 02453                }
 2454
 2455                try
 2456                {
 02457                    var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path);
 02458                    image.DateModified = modifiedDate;
 02459                }
 02460                catch (Exception ex)
 2461                {
 02462                    _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
 02463                }
 02464            }
 2465
 02466            item.ValidateImages();
 2467
 02468            await _persistenceService.SaveImagesAsync(item).ConfigureAwait(false);
 2469
 02470            RegisterItem(item);
 1492471        }
 2472
 2473        /// <inheritdoc />
 2474        public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, 
 2475        {
 4362476            foreach (var item in items)
 2477            {
 1092478                item.DateLastSaved = DateTime.UtcNow;
 1092479                await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
 2480
 2481                // Modify again, so saved value is after write time of externally saved metadata
 1092482                item.DateLastSaved = DateTime.UtcNow;
 1092483            }
 2484
 2485            // Resolve and add any local alternate version items that don't exist yet
 2486            // This ensures they exist in the database when LinkedChildren are processed
 1092487            var allItems = new List<BaseItem>(items);
 1092488            var parentFolder = parent as Folder;
 1092489            var parentCollectionType = GetTopFolderContentType(parent);
 4362490            foreach (var item in items)
 2491            {
 1092492                if (item is Video video && video.LocalAlternateVersions.Length > 0)
 2493                {
 02494                    var videoType = video.GetType();
 02495                    foreach (var path in video.LocalAlternateVersions)
 2496                    {
 02497                        if (string.IsNullOrEmpty(path))
 2498                        {
 2499                            continue;
 2500                        }
 2501
 2502                        // Use the primary video's type for ID calculation to ensure consistency
 02503                        var altId = GetNewItemId(path, videoType);
 02504                        if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
 2505                        {
 2506                            // Alternate version doesn't exist, resolve and create it
 2507                            // ensuring it has the same type as the primary video
 02508                            var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
 02509                            if (altVideo is not null)
 2510                            {
 02511                                altVideo.OwnerId = video.Id;
 02512                                altVideo.SetPrimaryVersionId(video.Id);
 02513                                allItems.Add(altVideo);
 2514                            }
 2515                        }
 2516                    }
 2517                }
 2518            }
 2519
 1092520            _persistenceService.SaveItems(allItems, cancellationToken);
 2521
 4362522            foreach (var item in allItems)
 2523            {
 1092524                if (!items.Contains(item))
 2525                {
 02526                    RegisterItem(item);
 2527                }
 2528            }
 2529
 1092530            if (parent is Folder folder)
 2531            {
 252532                folder.Children = null;
 252533                folder.UserData = null;
 2534            }
 2535
 1092536            if (ItemUpdated is not null)
 2537            {
 4362538                foreach (var item in items)
 2539                {
 2540                    // With the live tv guide this just creates too much noise
 1092541                    if (item.SourceType != SourceType.Library)
 2542                    {
 2543                        continue;
 2544                    }
 2545
 2546                    try
 2547                    {
 1092548                        ItemUpdated(
 1092549                            this,
 1092550                            new ItemChangeEventArgs
 1092551                            {
 1092552                                Item = item,
 1092553                                Parent = parent,
 1092554                                UpdateReason = updateReason
 1092555                            });
 1092556                    }
 02557                    catch (Exception ex)
 2558                    {
 02559                        _logger.LogError(ex, "Error in ItemUpdated event handler");
 02560                    }
 2561                }
 2562            }
 1092563        }
 2564
 2565        /// <inheritdoc />
 2566        public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cance
 1092567            => UpdateItemsAsync([item], parent, updateReason, cancellationToken);
 2568
 2569        /// <inheritdoc />
 2570        public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
 2571        {
 332572            await _persistenceService.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
 332573        }
 2574
 2575        public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
 2576        {
 1092577            if (item.IsFileProtocol)
 2578            {
 1092579                await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
 2580            }
 2581
 1092582            await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
 1092583        }
 2584
 2585        /// <summary>
 2586        /// Reports the item removed.
 2587        /// </summary>
 2588        /// <param name="item">The item.</param>
 2589        /// <param name="parent">The parent item.</param>
 2590        public void ReportItemRemoved(BaseItem item, BaseItem parent)
 2591        {
 02592            if (ItemRemoved is not null)
 2593            {
 2594                try
 2595                {
 02596                    ItemRemoved(
 02597                        this,
 02598                        new ItemChangeEventArgs
 02599                        {
 02600                            Item = item,
 02601                            Parent = parent
 02602                        });
 02603                }
 02604                catch (Exception ex)
 2605                {
 02606                    _logger.LogError(ex, "Error in ItemRemoved event handler");
 02607                }
 2608            }
 02609        }
 2610
 2611        /// <summary>
 2612        /// Retrieves the item.
 2613        /// </summary>
 2614        /// <param name="id">The id.</param>
 2615        /// <returns>BaseItem.</returns>
 2616        public BaseItem RetrieveItem(Guid id)
 2617        {
 1032618            return _itemRepository.RetrieveItem(id);
 2619        }
 2620
 2621        public List<Folder> GetCollectionFolders(BaseItem item)
 2622        {
 7052623            return GetCollectionFolders(item, GetUserRootFolder().Children.OfType<Folder>());
 2624        }
 2625
 2626        public List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2627        {
 7212628            while (item is not null)
 2629            {
 7212630                var parent = item.GetParent();
 2631
 7212632                if (parent is AggregateFolder)
 2633                {
 2634                    break;
 2635                }
 2636
 6422637                if (parent is null)
 2638                {
 6262639                    var owner = item.GetOwner();
 2640
 6262641                    if (owner is null)
 2642                    {
 2643                        break;
 2644                    }
 2645
 02646                    item = owner;
 2647                }
 2648                else
 2649                {
 162650                    item = parent;
 2651                }
 2652            }
 2653
 7052654            if (item is null)
 2655            {
 02656                return [];
 2657            }
 2658
 7052659            return GetCollectionFoldersInternal(item, allUserRootChildren);
 2660        }
 2661
 2662        private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2663        {
 7052664            return allUserRootChildren
 7052665                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 7052666                .ToList();
 2667        }
 2668
 2669        public LibraryOptions GetLibraryOptions(BaseItem item)
 2670        {
 4522671            if (item is CollectionFolder collectionFolder)
 2672            {
 372673                return collectionFolder.GetLibraryOptions();
 2674            }
 2675
 2676            // List.Find is more performant than FirstOrDefault due to enumerator allocation
 4152677            return GetCollectionFolders(item)
 4152678                .Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2
 4152679                ? collectionFolder2.GetLibraryOptions()
 4152680                : new LibraryOptions();
 2681        }
 2682
 2683        public CollectionType? GetContentType(BaseItem item)
 2684        {
 582685            var configuredContentType = GetConfiguredContentType(item, false);
 582686            if (configuredContentType is not null)
 2687            {
 02688                return configuredContentType;
 2689            }
 2690
 582691            configuredContentType = GetConfiguredContentType(item, true);
 582692            if (configuredContentType is not null)
 2693            {
 02694                return configuredContentType;
 2695            }
 2696
 582697            return GetInheritedContentType(item);
 2698        }
 2699
 2700        public CollectionType? GetInheritedContentType(BaseItem item)
 2701        {
 582702            var type = GetTopFolderContentType(item);
 2703
 582704            if (type is not null)
 2705            {
 02706                return type;
 2707            }
 2708
 582709            return item.GetParents()
 582710                .Select(GetConfiguredContentType)
 582711                .LastOrDefault(i => i is not null);
 2712        }
 2713
 2714        public CollectionType? GetConfiguredContentType(BaseItem item)
 2715        {
 02716            return GetConfiguredContentType(item, false);
 2717        }
 2718
 2719        public CollectionType? GetConfiguredContentType(string path)
 2720        {
 02721            return GetContentTypeOverride(path, false);
 2722        }
 2723
 2724        public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
 2725        {
 1162726            if (item is ICollectionFolder collectionFolder)
 2727            {
 02728                return collectionFolder.CollectionType;
 2729            }
 2730
 1162731            return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
 2732        }
 2733
 2734        private CollectionType? GetContentTypeOverride(string path, bool inherit)
 2735        {
 1332736            var nameValuePair = _configurationManager.Configuration.ContentTypes
 1332737                                    .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
 1332738                                                         || (inherit && !string.IsNullOrEmpty(i.Name)
 1332739                                                                     && _fileSystem.ContainsSubPath(i.Name, path)));
 1332740            if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType))
 2741            {
 02742                return collectionType;
 2743            }
 2744
 1332745            return null;
 2746        }
 2747
 2748        private CollectionType? GetTopFolderContentType(BaseItem item)
 2749        {
 1692750            if (item is null)
 2751            {
 842752                return null;
 2753            }
 2754
 852755            while (!item.ParentId.IsEmpty())
 2756            {
 02757                var parent = item.GetParent();
 02758                if (parent is null || parent is AggregateFolder)
 2759                {
 2760                    break;
 2761                }
 2762
 02763                item = parent;
 2764            }
 2765
 852766            return GetUserRootFolder().Children
 852767                .OfType<ICollectionFolder>()
 852768                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 852769                .Select(i => i.CollectionType)
 852770                .FirstOrDefault(i => i is not null);
 2771        }
 2772
 2773        public UserView GetNamedView(
 2774            User user,
 2775            string name,
 2776            CollectionType? viewType,
 2777            string sortName)
 2778        {
 02779            return GetNamedView(user, name, Guid.Empty, viewType, sortName);
 2780        }
 2781
 2782        public UserView GetNamedView(
 2783            string name,
 2784            CollectionType viewType,
 2785            string sortName)
 2786        {
 02787            var path = Path.Combine(
 02788                _configurationManager.ApplicationPaths.InternalMetadataPath,
 02789                "views",
 02790                _fileSystem.GetValidFilename(viewType.ToString()));
 2791
 02792            var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
 2793
 02794            var item = GetItemById(id) as UserView;
 2795
 02796            var refresh = false;
 2797
 02798            if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
 2799            {
 02800                var info = Directory.CreateDirectory(path);
 02801                item = new UserView
 02802                {
 02803                    Path = path,
 02804                    Id = id,
 02805                    DateCreated = info.CreationTimeUtc,
 02806                    DateModified = info.LastWriteTimeUtc,
 02807                    Name = name,
 02808                    ViewType = viewType,
 02809                    ForcedSortName = sortName
 02810                };
 2811
 02812                CreateItem(item, null);
 2813
 02814                refresh = true;
 2815            }
 2816
 02817            if (refresh)
 2818            {
 02819                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResu
 02820                ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), Ref
 2821            }
 2822
 02823            return item;
 2824        }
 2825
 2826        public UserView GetNamedView(
 2827            User user,
 2828            string name,
 2829            Guid parentId,
 2830            CollectionType? viewType,
 2831            string sortName)
 2832        {
 02833            var parentIdString = parentId.IsEmpty()
 02834                ? null
 02835                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02836            var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdStrin
 2837
 02838            var id = GetNewItemId(idValues, typeof(UserView));
 2839
 02840            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2841
 02842            var item = GetItemById(id) as UserView;
 2843
 02844            var isNew = false;
 2845
 02846            if (item is null)
 2847            {
 02848                var info = Directory.CreateDirectory(path);
 02849                item = new UserView
 02850                {
 02851                    Path = path,
 02852                    Id = id,
 02853                    DateCreated = info.CreationTimeUtc,
 02854                    DateModified = info.LastWriteTimeUtc,
 02855                    Name = name,
 02856                    ViewType = viewType,
 02857                    ForcedSortName = sortName,
 02858                    UserId = user.Id,
 02859                    DisplayParentId = parentId
 02860                };
 2861
 02862                CreateItem(item, null);
 2863
 02864                isNew = true;
 2865            }
 2866
 02867            var lastRefreshedUtc = item.DateLastRefreshed;
 02868            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2869
 02870            if (!refresh && !item.DisplayParentId.IsEmpty())
 2871            {
 02872                var displayParent = GetItemById(item.DisplayParentId);
 02873                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 2874            }
 2875
 02876            if (refresh)
 2877            {
 02878                ProviderManager.QueueRefresh(
 02879                    item.Id,
 02880                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02881                    {
 02882                        // Need to force save to increment DateLastSaved
 02883                        ForceSave = true
 02884                    },
 02885                    RefreshPriority.Normal);
 2886            }
 2887
 02888            return item;
 2889        }
 2890
 2891        public UserView GetShadowView(
 2892            BaseItem parent,
 2893            CollectionType? viewType,
 2894            string sortName)
 2895        {
 02896            ArgumentNullException.ThrowIfNull(parent);
 2897
 02898            var name = parent.Name;
 02899            var parentId = parent.Id;
 2900
 02901            var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
 2902
 02903            var id = GetNewItemId(idValues, typeof(UserView));
 2904
 02905            var path = parent.Path;
 2906
 02907            var item = GetItemById(id) as UserView;
 2908
 02909            var isNew = false;
 2910
 02911            if (item is null)
 2912            {
 02913                var info = Directory.CreateDirectory(path);
 02914                item = new UserView
 02915                {
 02916                    Path = path,
 02917                    Id = id,
 02918                    DateCreated = info.CreationTimeUtc,
 02919                    DateModified = info.LastWriteTimeUtc,
 02920                    Name = name,
 02921                    ViewType = viewType,
 02922                    ForcedSortName = sortName,
 02923                    DisplayParentId = parentId
 02924                };
 2925
 02926                CreateItem(item, null);
 2927
 02928                isNew = true;
 2929            }
 2930
 02931            var lastRefreshedUtc = item.DateLastRefreshed;
 02932            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2933
 02934            if (!refresh && !item.DisplayParentId.IsEmpty())
 2935            {
 02936                var displayParent = GetItemById(item.DisplayParentId);
 02937                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 2938            }
 2939
 02940            if (refresh)
 2941            {
 02942                ProviderManager.QueueRefresh(
 02943                    item.Id,
 02944                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02945                    {
 02946                        // Need to force save to increment DateLastSaved
 02947                        ForceSave = true
 02948                    },
 02949                    RefreshPriority.Normal);
 2950            }
 2951
 02952            return item;
 2953        }
 2954
 2955        public UserView GetNamedView(
 2956            string name,
 2957            Guid parentId,
 2958            CollectionType? viewType,
 2959            string sortName,
 2960            string uniqueId)
 2961        {
 02962            ArgumentException.ThrowIfNullOrEmpty(name);
 2963
 02964            var parentIdString = parentId.IsEmpty()
 02965                ? null
 02966                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02967            var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.E
 02968            if (!string.IsNullOrEmpty(uniqueId))
 2969            {
 02970                idValues += uniqueId;
 2971            }
 2972
 02973            var id = GetNewItemId(idValues, typeof(UserView));
 2974
 02975            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2976
 02977            var item = GetItemById(id) as UserView;
 2978
 02979            var isNew = false;
 2980
 02981            if (item is null)
 2982            {
 02983                var info = Directory.CreateDirectory(path);
 02984                item = new UserView
 02985                {
 02986                    Path = path,
 02987                    Id = id,
 02988                    DateCreated = info.CreationTimeUtc,
 02989                    DateModified = info.LastWriteTimeUtc,
 02990                    Name = name,
 02991                    ViewType = viewType,
 02992                    ForcedSortName = sortName,
 02993                    DisplayParentId = parentId
 02994                };
 2995
 02996                CreateItem(item, null);
 2997
 02998                isNew = true;
 2999            }
 3000
 03001            if (viewType != item.ViewType)
 3002            {
 03003                item.ViewType = viewType;
 03004                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult
 3005            }
 3006
 03007            var lastRefreshedUtc = item.DateLastRefreshed;
 03008            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 3009
 03010            if (!refresh && !item.DisplayParentId.IsEmpty())
 3011            {
 03012                var displayParent = GetItemById(item.DisplayParentId);
 03013                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 3014            }
 3015
 03016            if (refresh)
 3017            {
 03018                ProviderManager.QueueRefresh(
 03019                    item.Id,
 03020                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 03021                    {
 03022                        // Need to force save to increment DateLastSaved
 03023                        ForceSave = true
 03024                    },
 03025                    RefreshPriority.Normal);
 3026            }
 3027
 03028            return item;
 3029        }
 3030
 3031        public BaseItem GetParentItem(Guid? parentId, Guid? userId)
 3032        {
 33033            if (parentId.HasValue)
 3034            {
 03035                return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}"
 3036            }
 3037
 33038            if (!userId.IsNullOrEmpty())
 3039            {
 33040                return GetUserRootFolder();
 3041            }
 3042
 03043            return RootFolder;
 3044        }
 3045
 3046        /// <inheritdoc />
 3047        public void QueueLibraryScan()
 3048        {
 03049            _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
 03050        }
 3051
 3052        /// <inheritdoc />
 3053        public int? GetSeasonNumberFromPath(string path, Guid? parentId)
 3054        {
 03055            var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
 03056            return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
 3057        }
 3058
 3059        /// <inheritdoc />
 3060        public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
 3061        {
 03062            var series = episode.Series;
 03063            bool? isAbsoluteNaming = series is not null && string.Equals(series.DisplayOrder, "absolute", StringComparis
 03064            if (!isAbsoluteNaming.Value)
 3065            {
 3066                // In other words, no filter applied
 03067                isAbsoluteNaming = null;
 3068            }
 3069
 03070            var resolver = new EpisodeResolver(_namingOptions);
 3071
 03072            var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 3073
 03074            EpisodeInfo? episodeInfo = null;
 03075            if (episode.IsFileProtocol)
 3076            {
 03077                episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
 3078                // Resolve from parent folder if it's not the Season folder
 03079                var parent = episode.GetParent();
 03080                if (episodeInfo is null && parent.GetType() == typeof(Folder))
 3081                {
 03082                    episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming);
 03083                    if (episodeInfo is not null)
 3084                    {
 3085                        // add the container
 03086                        episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
 3087                    }
 3088                }
 3089            }
 3090
 03091            var changed = false;
 03092            if (episodeInfo is null)
 3093            {
 03094                return changed;
 3095            }
 3096
 03097            if (episodeInfo.IsByDate)
 3098            {
 03099                if (episode.IndexNumber.HasValue)
 3100                {
 03101                    episode.IndexNumber = null;
 03102                    changed = true;
 3103                }
 3104
 03105                if (episode.IndexNumberEnd.HasValue)
 3106                {
 03107                    episode.IndexNumberEnd = null;
 03108                    changed = true;
 3109                }
 3110
 03111                if (!episode.PremiereDate.HasValue)
 3112                {
 03113                    if (episodeInfo.Year.HasValue && episodeInfo.Month.HasValue && episodeInfo.Day.HasValue)
 3114                    {
 03115                        episode.PremiereDate = new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo
 3116                    }
 3117
 03118                    if (episode.PremiereDate.HasValue)
 3119                    {
 03120                        changed = true;
 3121                    }
 3122                }
 3123
 03124                if (!episode.ProductionYear.HasValue)
 3125                {
 03126                    episode.ProductionYear = episodeInfo.Year;
 3127
 03128                    if (episode.ProductionYear.HasValue)
 3129                    {
 03130                        changed = true;
 3131                    }
 3132                }
 3133            }
 3134            else
 3135            {
 03136                if (!episode.IndexNumber.HasValue || forceRefresh)
 3137                {
 03138                    if (episode.IndexNumber != episodeInfo.EpisodeNumber)
 3139                    {
 03140                        changed = true;
 3141                    }
 3142
 03143                    episode.IndexNumber = episodeInfo.EpisodeNumber;
 3144                }
 3145
 03146                if (!episode.IndexNumberEnd.HasValue || forceRefresh)
 3147                {
 03148                    if (episode.IndexNumberEnd != episodeInfo.EndingEpisodeNumber)
 3149                    {
 03150                        changed = true;
 3151                    }
 3152
 03153                    episode.IndexNumberEnd = episodeInfo.EndingEpisodeNumber;
 3154                }
 3155
 03156                if (!episode.ParentIndexNumber.HasValue || forceRefresh)
 3157                {
 03158                    if (episode.ParentIndexNumber != episodeInfo.SeasonNumber)
 3159                    {
 03160                        changed = true;
 3161                    }
 3162
 03163                    episode.ParentIndexNumber = episodeInfo.SeasonNumber;
 3164                }
 3165            }
 3166
 03167            if (!episode.ParentIndexNumber.HasValue)
 3168            {
 03169                var season = episode.Season;
 3170
 03171                if (season is not null)
 3172                {
 03173                    episode.ParentIndexNumber = season.IndexNumber;
 3174                }
 3175
 03176                if (episode.ParentIndexNumber.HasValue)
 3177                {
 03178                    changed = true;
 3179                }
 3180            }
 3181
 03182            return changed;
 3183        }
 3184
 3185        public ItemLookupInfo ParseName(string name)
 3186        {
 03187            var namingOptions = _namingOptions;
 03188            var result = VideoResolver.CleanDateTime(name, namingOptions);
 3189
 03190            return new ItemLookupInfo
 03191            {
 03192                Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName : result.Name
 03193                Year = result.Year
 03194            };
 3195        }
 3196
 3197        public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, ID
 3198        {
 3199            // Apply .ignore rules
 73200            var filtered = fileSystemChildren.Where(c => !_dotIgnoreIgnoreRule.ShouldIgnore(c, owner)).ToList();
 73201            var isFolder = owner.IsFolder || (owner is Video video && (video.VideoType == VideoType.BluRay || video.Vide
 73202            var ownerVideoInfo = VideoResolver.Resolve(owner.Path, isFolder, _namingOptions, libraryRoot: owner.Containi
 73203            if (ownerVideoInfo is null)
 3204            {
 03205                yield break;
 3206            }
 3207
 73208            var count = filtered.Count;
 823209            for (var i = 0; i < count; i++)
 3210            {
 343211                var current = filtered[i];
 343212                if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
 3213                {
 53214                    var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
 53215                    var filesInSubFolderList = filesInSubFolder.ToList();
 3216
 53217                    bool subFolderIsMixedFolder = filesInSubFolderList.Count > 1;
 3218
 203219                    foreach (var file in filesInSubFolderList)
 3220                    {
 53221                        if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType))
 3222                        {
 3223                            continue;
 3224                        }
 3225
 43226                        var extra = GetExtra(file, extraType.Value, subFolderIsMixedFolder);
 43227                        if (extra is not null)
 3228                        {
 43229                            yield return extra;
 3230                        }
 3231                    }
 3232                }
 293233                else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo
 3234                {
 133235                    var extra = GetExtra(current, extraType.Value, false);
 133236                    if (extra is not null)
 3237                    {
 133238                        yield return extra;
 3239                    }
 3240                }
 3241            }
 3242
 3243            BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType, bool isInMixedFolder)
 3244            {
 3245                var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetReso
 3246                if (extra is not Video && extra is not Audio)
 3247                {
 3248                    return null;
 3249                }
 3250
 3251                // Try to retrieve it from the db. If we don't find it, use the resolved version
 3252                var itemById = GetItemById(extra.Id);
 3253                if (itemById is not null)
 3254                {
 3255                    extra = itemById;
 3256                }
 3257
 3258                // Only update extra type if it is more specific then the currently known extra type
 3259                if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown)
 3260                {
 3261                    extra.ExtraType = extraType;
 3262                }
 3263
 3264                // Only return items that are actual extras (have ExtraType set)
 3265                // Note: OwnerId and ParentId are set by RefreshExtras, not here,
 3266                // so that RefreshExtras can detect when they need updating and set ForceSave.
 3267                if (extra.ExtraType is not null)
 3268                {
 3269                    extra.IsInMixedFolder = isInMixedFolder;
 3270                    return extra;
 3271                }
 3272
 3273                return null;
 3274            }
 73275        }
 3276
 3277        public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
 3278        {
 123279            foreach (var map in _configurationManager.Configuration.PathSubstitutions)
 3280            {
 03281                if (path.TryReplaceSubPath(map.From, map.To, out var newPath))
 3282                {
 03283                    return newPath;
 3284                }
 3285            }
 3286
 63287            return path;
 3288        }
 3289
 3290        public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
 3291        {
 03292            return _peopleRepository.GetPeople(query).Items;
 3293        }
 3294
 3295        public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
 3296        {
 63297            if (item.SupportsPeople)
 3298            {
 03299                var people = GetPeople(new InternalPeopleQuery
 03300                {
 03301                    ItemId = item.Id
 03302                });
 3303
 03304                if (people.Count > 0)
 3305                {
 03306                    return people;
 3307                }
 3308            }
 3309
 63310            return [];
 3311        }
 3312
 3313        public QueryResult<BaseItem> GetPeopleItems(InternalPeopleQuery query)
 3314        {
 03315            var queryResult = _peopleRepository.GetPeople(query);
 03316            var baseItems = queryResult.Items.Select(i =>
 03317                {
 03318                    try
 03319                    {
 03320                        return GetPerson(i.Name);
 03321                    }
 03322                    catch (Exception ex)
 03323                    {
 03324                        _logger.LogError(ex, "error retrieving BaseItem for person: {0}", i.Name);
 03325                        return null;
 03326                    }
 03327                })
 03328                .Where(i => i is not null)
 03329                .Where(i => query.User is null || i!.IsVisible(query.User))
 03330                .OfType<BaseItem>()
 03331                .ToList()
 03332                .AsReadOnly();
 3333
 03334            return new QueryResult<BaseItem>
 03335            {
 03336                StartIndex = queryResult.StartIndex,
 03337                TotalRecordCount = queryResult.TotalRecordCount,
 03338                Items = baseItems,
 03339            };
 3340        }
 3341
 3342        public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
 3343        {
 03344            return _peopleRepository.GetPeopleNames(query);
 3345        }
 3346
 3347        public void UpdatePeople(BaseItem item, List<PersonInfo> people)
 3348        {
 03349            UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
 03350        }
 3351
 3352        /// <inheritdoc />
 3353        public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellat
 3354        {
 03355            if (!item.SupportsPeople)
 3356            {
 03357                return;
 3358            }
 3359
 03360            if (people is not null)
 3361            {
 03362                people = people.Where(e => e is not null).ToArray();
 03363                _peopleRepository.UpdatePeople(item.Id, people);
 03364                await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
 3365            }
 03366        }
 3367
 3368        public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool re
 3369        {
 03370            foreach (var url in image.Path.Split('|'))
 3371            {
 3372                try
 3373                {
 03374                    _logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
 3375
 03376                    await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).Configure
 3377
 03378                    await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwai
 3379
 03380                    return item.GetImageInfo(image.Type, imageIndex);
 3381                }
 03382                catch (HttpRequestException ex)
 3383                {
 03384                    if (ex.StatusCode.HasValue
 03385                        && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forb
 3386                    {
 03387                        _logger.LogDebug(ex, "Error downloading image {Url}", url);
 03388                        continue;
 3389                    }
 3390
 03391                    throw;
 3392                }
 3393            }
 3394
 03395            if (removeOnFailure)
 3396            {
 3397                // Remove this image to prevent it from retrying over and over
 03398                item.RemoveImage(image);
 03399                await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(fa
 3400            }
 3401
 03402            throw new InvalidOperationException("Unable to convert any images to local");
 03403        }
 3404
 3405        public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, b
 3406        {
 23407            if (string.IsNullOrWhiteSpace(name))
 3408            {
 03409                throw new ArgumentNullException(nameof(name));
 3410            }
 3411
 23412            name = _fileSystem.GetValidFilename(name.Trim());
 3413
 23414            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3415
 23416            var existingNameCount = 1; // first numbered name will be 2
 23417            var virtualFolderPath = Path.Combine(rootFolderPath, name);
 23418            var originalName = name;
 33419            while (Directory.Exists(virtualFolderPath))
 3420            {
 13421                existingNameCount++;
 13422                name = originalName + existingNameCount;
 13423                virtualFolderPath = Path.Combine(rootFolderPath, name);
 3424            }
 3425
 23426            var mediaPathInfos = options.PathInfos;
 23427            if (mediaPathInfos is not null)
 3428            {
 23429                var invalidpath = mediaPathInfos.FirstOrDefault(i => !Directory.Exists(i.Path));
 23430                if (invalidpath is not null)
 3431                {
 03432                    throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + ".");
 3433                }
 3434            }
 3435
 23436            LibraryMonitor.Stop();
 3437
 3438            try
 3439            {
 23440                Directory.CreateDirectory(virtualFolderPath);
 3441
 23442                if (collectionType is not null)
 3443                {
 03444                    var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collect
 3445
 03446                    FileHelper.CreateEmpty(path);
 3447                }
 3448
 23449                CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
 3450
 23451                if (mediaPathInfos is not null)
 3452                {
 43453                    foreach (var path in mediaPathInfos)
 3454                    {
 03455                        AddMediaPathInternal(name, path, false);
 3456                    }
 3457                }
 3458            }
 3459            finally
 3460            {
 23461                await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
 3462
 23463                if (refreshLibrary)
 3464                {
 23465                    StartScanInBackground();
 3466                }
 3467                else
 3468                {
 3469                    // Need to add a delay here or directory watchers may still pick up the changes
 03470                    await Task.Delay(1000).ConfigureAwait(false);
 03471                    LibraryMonitor.Start();
 3472                }
 3473            }
 23474        }
 3475
 3476        private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
 3477        {
 03478            foreach (var person in people)
 3479            {
 03480                cancellationToken.ThrowIfCancellationRequested();
 3481
 03482                var itemUpdateType = ItemUpdateType.MetadataDownload;
 03483                var saveEntity = false;
 03484                var createEntity = false;
 03485                var personEntity = GetPerson(person.Name);
 3486
 03487                if (personEntity is null)
 3488                {
 3489                    try
 3490                    {
 03491                        var path = Person.GetPath(person.Name);
 03492                        var info = Directory.CreateDirectory(path);
 03493                        personEntity = new Person()
 03494                        {
 03495                            Name = person.Name,
 03496                            Id = GetItemByNameId<Person>(path),
 03497                            DateCreated = info.CreationTimeUtc,
 03498                            DateModified = info.LastWriteTimeUtc,
 03499                            Path = path
 03500                        };
 3501
 03502                        personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
 03503                        saveEntity = true;
 03504                        createEntity = true;
 03505                    }
 03506                    catch (Exception ex)
 3507                    {
 03508                        _logger.LogWarning(ex, "Failed to create person {Name}", person.Name);
 03509                        continue;
 3510                    }
 3511                }
 3512
 03513                foreach (var id in person.ProviderIds)
 3514                {
 03515                    if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)
 3516                    {
 03517                        personEntity.SetProviderId(id.Key, id.Value);
 03518                        saveEntity = true;
 3519                    }
 3520                }
 3521
 03522                if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
 3523                {
 03524                    personEntity.SetImage(
 03525                        new ItemImageInfo
 03526                        {
 03527                            Path = person.ImageUrl,
 03528                            Type = ImageType.Primary
 03529                        },
 03530                        0);
 3531
 03532                    saveEntity = true;
 03533                    itemUpdateType = ItemUpdateType.ImageUpdate;
 3534                }
 3535
 03536                if (saveEntity)
 3537                {
 03538                    if (createEntity)
 3539                    {
 03540                        CreateItems([personEntity], null, CancellationToken.None);
 3541                    }
 3542
 03543                    await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
 03544                    personEntity.DateLastSaved = DateTime.UtcNow;
 3545
 03546                    CreateItems([personEntity], null, CancellationToken.None);
 3547                }
 03548            }
 03549        }
 3550
 3551        private void StartScanInBackground()
 3552        {
 33553            Task.Run(() =>
 33554            {
 33555                // No need to start if scanning the library because it will handle it
 33556                ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
 33557            });
 33558        }
 3559
 3560        public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3561        {
 13562            AddMediaPathInternal(virtualFolderName, mediaPath, true);
 03563        }
 3564
 3565        private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
 3566        {
 13567            ArgumentNullException.ThrowIfNull(pathInfo);
 3568
 13569            var path = pathInfo.Path;
 3570
 13571            if (string.IsNullOrWhiteSpace(path))
 3572            {
 03573                throw new ArgumentException(nameof(path));
 3574            }
 3575
 13576            if (!Directory.Exists(path))
 3577            {
 13578                throw new FileNotFoundException("The path does not exist.");
 3579            }
 3580
 03581            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03582            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3583
 03584            CreateShortcut(virtualFolderPath, pathInfo);
 3585
 03586            if (saveLibraryOptions)
 3587            {
 03588                var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3589
 03590                libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
 3591
 03592                SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3593
 03594                CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 3595            }
 03596        }
 3597
 3598        public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3599        {
 03600            ArgumentNullException.ThrowIfNull(mediaPath);
 3601
 03602            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03603            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3604
 03605            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3606
 03607            SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3608
 03609            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03610        }
 3611
 3612        private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options)
 3613        {
 03614            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 03615            var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders, null);
 3616
 03617            if (info.Locations.Length > 0 && info.Locations.Length != options.PathInfos.Length)
 3618            {
 03619                var list = options.PathInfos.ToList();
 3620
 03621                foreach (var location in info.Locations)
 3622                {
 03623                    if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
 3624                    {
 03625                        list.Add(new MediaPathInfo(location));
 3626                    }
 3627                }
 3628
 03629                options.PathInfos = list.ToArray();
 3630            }
 03631        }
 3632
 3633        public async Task RemoveVirtualFolder(string name, bool refreshLibrary)
 3634        {
 23635            if (string.IsNullOrWhiteSpace(name))
 3636            {
 03637                throw new ArgumentNullException(nameof(name));
 3638            }
 3639
 23640            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3641
 23642            var path = Path.Combine(rootFolderPath, name);
 3643
 23644            if (!Directory.Exists(path))
 3645            {
 13646                throw new FileNotFoundException("The media folder does not exist");
 3647            }
 3648
 13649            LibraryMonitor.Stop();
 3650
 3651            try
 3652            {
 13653                Directory.Delete(path, true);
 3654            }
 3655            finally
 3656            {
 13657                CollectionFolder.OnCollectionFolderChange();
 3658
 13659                if (refreshLibrary)
 3660                {
 13661                    await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
 3662
 13663                    StartScanInBackground();
 3664                }
 3665                else
 3666                {
 3667                    // Need to add a delay here or directory watchers may still pick up the changes
 03668                    await Task.Delay(1000).ConfigureAwait(false);
 03669                    LibraryMonitor.Start();
 3670                }
 3671            }
 13672        }
 3673
 3674        private void RemoveContentTypeOverrides(string path)
 3675        {
 03676            if (string.IsNullOrWhiteSpace(path))
 3677            {
 03678                throw new ArgumentNullException(nameof(path));
 3679            }
 3680
 03681            List<NameValuePair>? removeList = null;
 3682
 03683            foreach (var contentType in _configurationManager.Configuration.ContentTypes)
 3684            {
 03685                if (string.IsNullOrWhiteSpace(contentType.Name)
 03686                    || _fileSystem.AreEqual(path, contentType.Name)
 03687                    || _fileSystem.ContainsSubPath(path, contentType.Name))
 3688                {
 03689                    (removeList ??= new()).Add(contentType);
 3690                }
 3691            }
 3692
 03693            if (removeList is not null)
 3694            {
 03695                _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
 03696                    .Except(removeList)
 03697                    .ToArray();
 3698
 03699                _configurationManager.SaveConfiguration();
 3700            }
 03701        }
 3702
 3703        public void RemoveMediaPath(string virtualFolderName, string mediaPath)
 3704        {
 13705            ArgumentException.ThrowIfNullOrEmpty(mediaPath);
 3706
 13707            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 13708            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3709
 13710            if (!Directory.Exists(virtualFolderPath))
 3711            {
 13712                throw new FileNotFoundException(
 13713                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolder
 3714            }
 3715
 03716            var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
 03717                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 03718                .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, String
 3719
 03720            if (!string.IsNullOrEmpty(shortcut))
 3721            {
 03722                _fileSystem.DeleteFile(shortcut);
 3723            }
 3724
 03725            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3726
 03727            libraryOptions.PathInfos = libraryOptions
 03728                .PathInfos
 03729                .Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal))
 03730                .ToArray();
 3731
 03732            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03733        }
 3734
 3735        private static bool ItemIsVisible(BaseItem? item, User? user)
 3736        {
 213737            if (item is null)
 3738            {
 213739                return false;
 3740            }
 3741
 03742            if (user is null)
 3743            {
 03744                return true;
 3745            }
 3746
 03747            return item is UserRootFolder || item.IsVisibleStandalone(user);
 3748        }
 3749
 3750        public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
 3751        {
 03752            var path = pathInfo.Path;
 03753            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3754
 03755            var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 3756
 03757            var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3758
 03759            while (File.Exists(lnk))
 3760            {
 03761                shortcutFilename += "1";
 03762                lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3763            }
 3764
 03765            _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
 03766            RemoveContentTypeOverrides(path);
 03767        }
 3768
 3769        /// <inheritdoc />
 3770        public async Task RerouteLinkedChildReferencesAsync(Guid fromChildId, Guid toChildId)
 3771        {
 03772            var affectedParentIds = _linkedChildrenService.RerouteLinkedChildren(fromChildId, toChildId);
 3773
 3774            // Update in-memory LinkedChildren and re-save metadata (NFO) for affected parents
 03775            foreach (var parentId in affectedParentIds)
 3776            {
 03777                if (GetItemById(parentId) is Folder parent)
 3778                {
 03779                    foreach (var lc in parent.LinkedChildren)
 3780                    {
 03781                        if (lc.ItemId.HasValue && lc.ItemId.Value.Equals(fromChildId))
 3782                        {
 03783                            lc.ItemId = toChildId;
 3784                        }
 3785                    }
 3786
 03787                    await RunMetadataSavers(parent, ItemUpdateType.MetadataEdit).ConfigureAwait(false);
 3788                }
 3789            }
 03790        }
 3791
 3792        /// <inheritdoc />
 3793        public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query)
 3794        {
 03795            if (query.User is not null)
 3796            {
 03797                AddUserToQuery(query, query.User);
 3798            }
 3799
 03800            SetTopParentOrAncestorIds(query);
 03801            return _itemRepository.GetQueryFiltersLegacy(query);
 3802        }
 3803    }
 3804}

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.Persistence.IItemPersistenceService,MediaBrowser.Controller.Persistence.INextUpService,MediaBrowser.Controller.Persistence.IItemCountService,MediaBrowser.Controller.Persistence.ILinkedChildrenService,MediaBrowser.Controller.Drawing.IImageProcessor,Emby.Naming.Common.NamingOptions,MediaBrowser.Controller.Providers.IDirectoryService,MediaBrowser.Controller.Persistence.IPeopleRepository,MediaBrowser.Controller.IO.IPathManager,Emby.Server.Implementations.Library.DotIgnoreIgnoreRule)
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)
DeleteItemsUnsafeFast(System.Collections.Generic.IReadOnlyCollection`1<MediaBrowser.Controller.Entities.BaseItem>,System.Boolean)
DeleteItem(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Library.DeleteOptions,MediaBrowser.Controller.Entities.BaseItem,System.Boolean)
DeleteItemPath(MediaBrowser.Controller.Entities.BaseItem,System.Boolean,MediaBrowser.Model.IO.FileSystemMetadata)
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,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>)
ResolveAlternateVersion(System.String,System.Type,MediaBrowser.Controller.Entities.Folder,System.Nullable`1<Jellyfin.Data.Enums.CollectionType>)
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[])
ResolveFileList()
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)
GetArtists(System.Collections.Generic.IReadOnlyList`1<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)
ValidateMediaLibraryInternal()
ValidateTopLibraryFolders()
ClearIgnoreRuleCache()
PerformLibraryValidation()
RunPostScanTasks()
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)
GetItemCountsForNameItem(Jellyfin.Data.Enums.BaseItemKind,System.Guid,Jellyfin.Data.Enums.BaseItemKind[],Jellyfin.Database.Implementations.Entities.User)
GetChildCountBatch(System.Collections.Generic.IReadOnlyList`1<System.Guid>,System.Nullable`1<System.Guid>)
GetPlayedAndTotalCountBatch(System.Collections.Generic.IReadOnlyList`1<System.Guid>,Jellyfin.Database.Implementations.Entities.User)
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)
GetNextUpEpisodesBatch(MediaBrowser.Controller.Entities.InternalItemsQuery,System.Collections.Generic.IReadOnlyList`1<System.String>,System.Boolean,System.Boolean)
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)
ConfigureUserAccess(MediaBrowser.Controller.Entities.InternalItemsQuery,Jellyfin.Database.Implementations.Entities.User)
GetTopParentIdsForQuery(MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Database.Implementations.Entities.User)
GetIntros()
GetIntros()
ResolveIntro(MediaBrowser.Controller.Library.IntroInfo)
GetLocalAlternateVersionIds(MediaBrowser.Controller.Entities.Video)
GetLinkedAlternateVersions(MediaBrowser.Controller.Entities.Video)
UpsertLinkedChild(System.Guid,System.Guid,MediaBrowser.Controller.Entities.LinkedChildType)
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)
UpdateImagesAsync()
UpdateItemsAsync()
UpdateItemAsync(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Library.ItemUpdateType,System.Threading.CancellationToken)
ReattachUserDataAsync()
RunMetadataSavers()
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)
FindExtras()
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>)
UpdatePeopleAsync()
ConvertImageToLocal()
AddVirtualFolder()
SavePeopleMetadataAsync()
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)
RemoveVirtualFolder()
RemoveContentTypeOverrides(System.String)
RemoveMediaPath(System.String,System.String)
ItemIsVisible(MediaBrowser.Controller.Entities.BaseItem,Jellyfin.Database.Implementations.Entities.User)
CreateShortcut(System.String,MediaBrowser.Model.Configuration.MediaPathInfo)
RerouteLinkedChildReferencesAsync()
GetQueryFiltersLegacy(MediaBrowser.Controller.Entities.InternalItemsQuery)