< 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: 586
Uncovered lines: 1069
Coverable lines: 1655
Total lines: 3890
Line coverage: 35.4%
Branch coverage
27%
Covered branches: 246
Total branches: 886
Branch coverage: 27.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 3/6/2026 - 12:14:09 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: 38045/7/2026 - 12:15:44 AM Line coverage: 35.7% (579/1620) Branch coverage: 31% (269/866) Total lines: 38045/8/2026 - 12:15:13 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/9/2026 - 12:15:41 AM Line coverage: 35.7% (579/1620) Branch coverage: 31% (269/866) Total lines: 38045/11/2026 - 12:15:59 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/14/2026 - 12:15:54 AM Line coverage: 35.7% (579/1620) Branch coverage: 31% (269/866) Total lines: 38045/15/2026 - 12:15:55 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/16/2026 - 12:15:55 AM Line coverage: 35.5% (583/1641) Branch coverage: 30.6% (269/878) Total lines: 38605/19/2026 - 12:15:13 AM Line coverage: 35.3% (580/1641) Branch coverage: 30.6% (269/878) Total lines: 38605/20/2026 - 12:15:44 AM Line coverage: 35.5% (583/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/21/2026 - 12:15:22 AM Line coverage: 35.3% (580/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/22/2026 - 12:15:17 AM Line coverage: 35.5% (583/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/26/2026 - 12:15:12 AM Line coverage: 35.3% (580/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/27/2026 - 12:15:38 AM Line coverage: 35.5% (583/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/28/2026 - 12:15:50 AM Line coverage: 35.5% (583/1642) Branch coverage: 27.7% (244/878) Total lines: 38665/29/2026 - 12:15:07 AM Line coverage: 35.3% (580/1642) Branch coverage: 27.7% (244/878) Total lines: 38665/31/2026 - 12:15:44 AM Line coverage: 35.5% (583/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/3/2026 - 12:16:02 AM Line coverage: 35.3% (580/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/4/2026 - 12:15:59 AM Line coverage: 35.5% (583/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/5/2026 - 12:16:38 AM Line coverage: 35.3% (580/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/6/2026 - 12:15:50 AM Line coverage: 35.3% (581/1643) Branch coverage: 27.8% (245/880) Total lines: 38676/7/2026 - 12:16:09 AM Line coverage: 35.5% (584/1643) Branch coverage: 27.8% (245/880) Total lines: 38676/8/2026 - 12:16:15 AM Line coverage: 35.4% (585/1651) Branch coverage: 27.7% (245/884) Total lines: 38846/9/2026 - 12:16:23 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 3890 3/6/2026 - 12:14:09 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: 38045/7/2026 - 12:15:44 AM Line coverage: 35.7% (579/1620) Branch coverage: 31% (269/866) Total lines: 38045/8/2026 - 12:15:13 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/9/2026 - 12:15:41 AM Line coverage: 35.7% (579/1620) Branch coverage: 31% (269/866) Total lines: 38045/11/2026 - 12:15:59 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/14/2026 - 12:15:54 AM Line coverage: 35.7% (579/1620) Branch coverage: 31% (269/866) Total lines: 38045/15/2026 - 12:15:55 AM Line coverage: 35.9% (582/1620) Branch coverage: 31% (269/866) Total lines: 38045/16/2026 - 12:15:55 AM Line coverage: 35.5% (583/1641) Branch coverage: 30.6% (269/878) Total lines: 38605/19/2026 - 12:15:13 AM Line coverage: 35.3% (580/1641) Branch coverage: 30.6% (269/878) Total lines: 38605/20/2026 - 12:15:44 AM Line coverage: 35.5% (583/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/21/2026 - 12:15:22 AM Line coverage: 35.3% (580/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/22/2026 - 12:15:17 AM Line coverage: 35.5% (583/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/26/2026 - 12:15:12 AM Line coverage: 35.3% (580/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/27/2026 - 12:15:38 AM Line coverage: 35.5% (583/1641) Branch coverage: 27.7% (244/878) Total lines: 38605/28/2026 - 12:15:50 AM Line coverage: 35.5% (583/1642) Branch coverage: 27.7% (244/878) Total lines: 38665/29/2026 - 12:15:07 AM Line coverage: 35.3% (580/1642) Branch coverage: 27.7% (244/878) Total lines: 38665/31/2026 - 12:15:44 AM Line coverage: 35.5% (583/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/3/2026 - 12:16:02 AM Line coverage: 35.3% (580/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/4/2026 - 12:15:59 AM Line coverage: 35.5% (583/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/5/2026 - 12:16:38 AM Line coverage: 35.3% (580/1642) Branch coverage: 27.7% (244/878) Total lines: 38666/6/2026 - 12:15:50 AM Line coverage: 35.3% (581/1643) Branch coverage: 27.8% (245/880) Total lines: 38676/7/2026 - 12:16:09 AM Line coverage: 35.5% (584/1643) Branch coverage: 27.8% (245/880) Total lines: 38676/8/2026 - 12:16:15 AM Line coverage: 35.4% (585/1651) Branch coverage: 27.7% (245/884) Total lines: 38846/9/2026 - 12:16:23 AM Line coverage: 35.4% (586/1655) Branch coverage: 27.7% (246/886) Total lines: 3890

Coverage delta

Coverage delta 3 -3

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(...)7.14%170147.4%
DeleteItem(...)0%4692680%
DeleteItemPath(...)0%272160%
IsInternalItem(...)0%342180%
GetMetadataPaths(...)0%620%
GetInternalMetadataPaths(...)0%7280%
ResolveItem(...)100%44100%
Resolve(...)100%1140%
GetNewItemId(...)100%11100%
GetNewItemIdInternal(...)83.33%66100%
ResolvePath(...)50%22100%
SetAdditionalPartsFromStack(...)0%156120%
ResolveAlternateVersion(...)0%156120%
ResolvePath(...)65%242078.12%
IgnoreFile(...)100%11100%
NormalizeRootPathList(...)50%2291.66%
ResolvePaths(...)100%11100%
ResolvePaths(...)58.33%221258.33%
ResolveFileList()100%4472.72%
CreateRootFolder()42.85%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%2281.81%
GetVirtualFolders()100%11100%
GetVirtualFolders(...)100%22100%
GetVirtualFolderInfo(...)50%101097.14%
GetCollectionType(...)50%6450%
GetItemById(...)66.66%7675%
GetItemById(...)100%22100%
GetItemById(...)50%22100%
GetItemById(...)50%22100%
GetItemList(...)87.5%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(...)80%1010100%
SetTopParentIdsOrAncestors(...)61.11%371861.11%
AddUserToQuery(...)55%202095%
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(...)50%341453.33%
Sort(...)0%210140%
GetComparer(...)50%3237.5%
CreateItem(...)100%210%
CreateItems(...)53.33%793062.16%
ImageNeedsRefresh(...)0%156120%
UpdateImagesAsync()28.57%1101421.05%
UpdateItemsAsync()60%663065.9%
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(...)37.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%
GetPeopleNamesByItems(...)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%
GetMediaStreamLanguages(...)100%210%

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.Naming.Video;
 17using Emby.Server.Implementations.Library.Resolvers;
 18using Emby.Server.Implementations.Library.Validators;
 19using Emby.Server.Implementations.Playlists;
 20using Emby.Server.Implementations.ScheduledTasks.Tasks;
 21using Emby.Server.Implementations.Sorting;
 22using Jellyfin.Data;
 23using Jellyfin.Data.Enums;
 24using Jellyfin.Database.Implementations.Entities;
 25using Jellyfin.Database.Implementations.Enums;
 26using Jellyfin.Extensions;
 27using MediaBrowser.Common.Extensions;
 28using MediaBrowser.Controller;
 29using MediaBrowser.Controller.Configuration;
 30using MediaBrowser.Controller.Drawing;
 31using MediaBrowser.Controller.Dto;
 32using MediaBrowser.Controller.Entities;
 33using MediaBrowser.Controller.Entities.Audio;
 34using MediaBrowser.Controller.Entities.Movies;
 35using MediaBrowser.Controller.IO;
 36using MediaBrowser.Controller.Library;
 37using MediaBrowser.Controller.LiveTv;
 38using MediaBrowser.Controller.MediaEncoding;
 39using MediaBrowser.Controller.Persistence;
 40using MediaBrowser.Controller.Playlists;
 41using MediaBrowser.Controller.Providers;
 42using MediaBrowser.Controller.Resolvers;
 43using MediaBrowser.Controller.Sorting;
 44using MediaBrowser.Model.Configuration;
 45using MediaBrowser.Model.Drawing;
 46using MediaBrowser.Model.Dto;
 47using MediaBrowser.Model.Entities;
 48using MediaBrowser.Model.IO;
 49using MediaBrowser.Model.Library;
 50using MediaBrowser.Model.Querying;
 51using MediaBrowser.Model.Tasks;
 52using Microsoft.Extensions.Logging;
 53using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 54using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
 55using Genre = MediaBrowser.Controller.Entities.Genre;
 56using Person = MediaBrowser.Controller.Entities.Person;
 57using VideoResolver = Emby.Naming.Video.VideoResolver;
 58
 59namespace Emby.Server.Implementations.Library
 60{
 61    /// <summary>
 62    /// Class LibraryManager.
 63    /// </summary>
 64    public class LibraryManager : ILibraryManager
 65    {
 66        private const string ShortcutFileExtension = ".mblink";
 67
 68        private readonly ILogger<LibraryManager> _logger;
 69        private readonly ITaskManager _taskManager;
 70        private readonly IUserManager _userManager;
 71        private readonly IUserDataManager _userDataManager;
 72        private readonly IServerConfigurationManager _configurationManager;
 73        private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory;
 74        private readonly Lazy<IProviderManager> _providerManagerFactory;
 75        private readonly Lazy<IUserViewManager> _userViewManagerFactory;
 76        private readonly IServerApplicationHost _appHost;
 77        private readonly IMediaEncoder _mediaEncoder;
 78        private readonly IFileSystem _fileSystem;
 79        private readonly IItemRepository _itemRepository;
 80        private readonly IItemPersistenceService _persistenceService;
 81        private readonly INextUpService _nextUpService;
 82        private readonly IItemCountService _countService;
 83        private readonly ILinkedChildrenService _linkedChildrenService;
 84        private readonly IImageProcessor _imageProcessor;
 85        private readonly NamingOptions _namingOptions;
 86        private readonly IPeopleRepository _peopleRepository;
 87        private readonly ExtraResolver _extraResolver;
 88        private readonly IPathManager _pathManager;
 89        private readonly FastConcurrentLru<Guid, BaseItem> _cache;
 90        private readonly DotIgnoreIgnoreRule _dotIgnoreIgnoreRule;
 91        private readonly IMediaStreamRepository _mediaStreamRepository;
 92        private readonly Lazy<IExternalDataManager> _externalDataManagerFactory;
 93
 94        /// <summary>
 95        /// The _root folder sync lock.
 96        /// </summary>
 2897        private readonly Lock _rootFolderSyncLock = new();
 2898        private readonly Lock _userRootFolderSyncLock = new();
 99
 28100        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 101
 102        /// <summary>
 103        /// The _root folder.
 104        /// </summary>
 105        private volatile AggregateFolder? _rootFolder;
 106        private volatile UserRootFolder? _userRootFolder;
 107
 108        private bool _wizardCompleted;
 109
 110        /// <summary>
 111        /// Initializes a new instance of the <see cref="LibraryManager" /> class.
 112        /// </summary>
 113        /// <param name="appHost">The application host.</param>
 114        /// <param name="loggerFactory">The logger factory.</param>
 115        /// <param name="taskManager">The task manager.</param>
 116        /// <param name="userManager">The user manager.</param>
 117        /// <param name="configurationManager">The configuration manager.</param>
 118        /// <param name="userDataManager">The user data manager.</param>
 119        /// <param name="libraryMonitorFactory">The library monitor.</param>
 120        /// <param name="fileSystem">The file system.</param>
 121        /// <param name="providerManagerFactory">The provider manager.</param>
 122        /// <param name="userViewManagerFactory">The user view manager.</param>
 123        /// <param name="mediaEncoder">The media encoder.</param>
 124        /// <param name="itemRepository">The item repository.</param>
 125        /// <param name="persistenceService">The item persistence service.</param>
 126        /// <param name="nextUpService">The next up service.</param>
 127        /// <param name="countService">The item count service.</param>
 128        /// <param name="linkedChildrenService">The linked children service.</param>
 129        /// <param name="imageProcessor">The image processor.</param>
 130        /// <param name="namingOptions">The naming options.</param>
 131        /// <param name="directoryService">The directory service.</param>
 132        /// <param name="peopleRepository">The people repository.</param>
 133        /// <param name="pathManager">The path manager.</param>
 134        /// <param name="dotIgnoreIgnoreRule">The .ignore rule handler.</param>
 135        /// <param name="mediaStreamRepository">The media stream repository.</param>
 136        /// <param name="externalDataManagerFactory">The external data manager (lazy, to break the DI cycle through Chap
 137        public LibraryManager(
 138            IServerApplicationHost appHost,
 139            ILoggerFactory loggerFactory,
 140            ITaskManager taskManager,
 141            IUserManager userManager,
 142            IServerConfigurationManager configurationManager,
 143            IUserDataManager userDataManager,
 144            Lazy<ILibraryMonitor> libraryMonitorFactory,
 145            IFileSystem fileSystem,
 146            Lazy<IProviderManager> providerManagerFactory,
 147            Lazy<IUserViewManager> userViewManagerFactory,
 148            IMediaEncoder mediaEncoder,
 149            IItemRepository itemRepository,
 150            IItemPersistenceService persistenceService,
 151            INextUpService nextUpService,
 152            IItemCountService countService,
 153            ILinkedChildrenService linkedChildrenService,
 154            IImageProcessor imageProcessor,
 155            NamingOptions namingOptions,
 156            IDirectoryService directoryService,
 157            IPeopleRepository peopleRepository,
 158            IPathManager pathManager,
 159            DotIgnoreIgnoreRule dotIgnoreIgnoreRule,
 160            IMediaStreamRepository mediaStreamRepository,
 161            Lazy<IExternalDataManager> externalDataManagerFactory)
 162        {
 28163            _appHost = appHost;
 28164            _logger = loggerFactory.CreateLogger<LibraryManager>();
 28165            _taskManager = taskManager;
 28166            _userManager = userManager;
 28167            _configurationManager = configurationManager;
 28168            _userDataManager = userDataManager;
 28169            _libraryMonitorFactory = libraryMonitorFactory;
 28170            _fileSystem = fileSystem;
 28171            _providerManagerFactory = providerManagerFactory;
 28172            _userViewManagerFactory = userViewManagerFactory;
 28173            _mediaEncoder = mediaEncoder;
 28174            _itemRepository = itemRepository;
 28175            _persistenceService = persistenceService;
 28176            _nextUpService = nextUpService;
 28177            _countService = countService;
 28178            _linkedChildrenService = linkedChildrenService;
 28179            _imageProcessor = imageProcessor;
 180
 28181            _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
 182
 28183            _namingOptions = namingOptions;
 28184            _peopleRepository = peopleRepository;
 28185            _pathManager = pathManager;
 28186            _dotIgnoreIgnoreRule = dotIgnoreIgnoreRule;
 28187            _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryServ
 188
 28189            _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 190
 28191            _mediaStreamRepository = mediaStreamRepository;
 28192            _externalDataManagerFactory = externalDataManagerFactory;
 193
 28194            RecordConfigurationValues(_configurationManager.Configuration);
 28195        }
 196
 197        /// <summary>
 198        /// Occurs when [item added].
 199        /// </summary>
 200        public event EventHandler<ItemChangeEventArgs>? ItemAdded;
 201
 202        /// <summary>
 203        /// Occurs when [item updated].
 204        /// </summary>
 205        public event EventHandler<ItemChangeEventArgs>? ItemUpdated;
 206
 207        /// <summary>
 208        /// Occurs when [item removed].
 209        /// </summary>
 210        public event EventHandler<ItemChangeEventArgs>? ItemRemoved;
 211
 212        /// <summary>
 213        /// Gets the root folder.
 214        /// </summary>
 215        /// <value>The root folder.</value>
 216        public AggregateFolder RootFolder
 217        {
 218            get
 219            {
 179220                if (_rootFolder is null)
 21221                {
 222                    lock (_rootFolderSyncLock)
 223                    {
 21224                        _rootFolder ??= CreateRootFolder();
 21225                    }
 226                }
 227
 179228                return _rootFolder;
 229            }
 230        }
 231
 41232        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
 233
 110234        private IProviderManager ProviderManager => _providerManagerFactory.Value;
 235
 1236        private IUserViewManager UserViewManager => _userViewManagerFactory.Value;
 237
 238        /// <summary>
 239        /// Gets or sets the postscan tasks.
 240        /// </summary>
 241        /// <value>The postscan tasks.</value>
 242        private ILibraryPostScanTask[] PostScanTasks { get; set; } = [];
 243
 244        /// <summary>
 245        /// Gets or sets the intro providers.
 246        /// </summary>
 247        /// <value>The intro providers.</value>
 248        private IIntroProvider[] IntroProviders { get; set; } = [];
 249
 250        /// <summary>
 251        /// Gets or sets the list of entity resolution ignore rules.
 252        /// </summary>
 253        /// <value>The entity resolution ignore rules.</value>
 254        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
 255
 256        /// <summary>
 257        /// Gets or sets the list of currently registered entity resolvers.
 258        /// </summary>
 259        /// <value>The entity resolvers enumerable.</value>
 260        private IItemResolver[] EntityResolvers { get; set; } = [];
 261
 262        private IMultiItemResolver[] MultiItemResolvers { get; set; } = [];
 263
 264        /// <summary>
 265        /// Gets or sets the comparers.
 266        /// </summary>
 267        /// <value>The comparers.</value>
 268        private IBaseItemComparer[] Comparers { get; set; } = [];
 269
 270        public bool IsScanRunning { get; private set; }
 271
 272        /// <summary>
 273        /// Adds the parts.
 274        /// </summary>
 275        /// <param name="rules">The rules.</param>
 276        /// <param name="resolvers">The resolvers.</param>
 277        /// <param name="introProviders">The intro providers.</param>
 278        /// <param name="itemComparers">The item comparers.</param>
 279        /// <param name="postScanTasks">The post scan tasks.</param>
 280        public void AddParts(
 281            IEnumerable<IResolverIgnoreRule> rules,
 282            IEnumerable<IItemResolver> resolvers,
 283            IEnumerable<IIntroProvider> introProviders,
 284            IEnumerable<IBaseItemComparer> itemComparers,
 285            IEnumerable<ILibraryPostScanTask> postScanTasks)
 286        {
 28287            EntityResolutionIgnoreRules = rules.ToArray();
 28288            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
 28289            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
 28290            IntroProviders = introProviders.ToArray();
 28291            Comparers = itemComparers.ToArray();
 28292            PostScanTasks = postScanTasks.ToArray();
 28293        }
 294
 295        /// <summary>
 296        /// Records the configuration values.
 297        /// </summary>
 298        /// <param name="configuration">The configuration.</param>
 299        private void RecordConfigurationValues(ServerConfiguration configuration)
 300        {
 129301            _wizardCompleted = configuration.IsStartupWizardCompleted;
 129302        }
 303
 304        /// <summary>
 305        /// Configurations the updated.
 306        /// </summary>
 307        /// <param name="sender">The sender.</param>
 308        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
 309        private void ConfigurationUpdated(object? sender, EventArgs e)
 310        {
 101311            var config = _configurationManager.Configuration;
 312
 101313            var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted;
 314
 101315            RecordConfigurationValues(config);
 316
 101317            if (wizardChanged)
 318            {
 16319                _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 320            }
 101321        }
 322
 323        public void RegisterItem(BaseItem item)
 324        {
 179325            ArgumentNullException.ThrowIfNull(item);
 326
 179327            if (item is IItemByName)
 328            {
 0329                if (item is not MusicArtist)
 330                {
 0331                    return;
 332                }
 333            }
 179334            else if (!item.IsFolder)
 335            {
 0336                if (item is not Video && item is not LiveTvChannel)
 337                {
 0338                    return;
 339                }
 340            }
 341
 179342            _cache.AddOrUpdate(item.Id, item);
 179343        }
 344
 345        public void DeleteItem(BaseItem item, DeleteOptions options)
 346        {
 0347            DeleteItem(item, options, false);
 0348        }
 349
 350        public void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem)
 351        {
 0352            ArgumentNullException.ThrowIfNull(item);
 353
 0354            var parent = item.GetOwner() ?? item.GetParent();
 355
 0356            DeleteItem(item, options, parent, notifyParentItem);
 0357        }
 358
 359        public void DeleteItemsUnsafeFast(IReadOnlyCollection<BaseItem> items, bool deleteSourceFiles = false)
 360        {
 51361            if (items.Count == 0)
 362            {
 51363                return;
 364            }
 365
 0366            var pathMaps = items.Select(e =>
 0367                (Item: e,
 0368                InternalPath: GetInternalMetadataPaths(e),
 0369                DeletePaths: deleteSourceFiles ? e.GetDeletePaths() : [])).ToArray();
 370
 0371            foreach (var (item, internalPaths, pathsToDelete) in pathMaps)
 372            {
 0373                foreach (var metadataPath in internalPaths)
 374                {
 0375                    if (!Directory.Exists(metadataPath))
 376                    {
 377                        continue;
 378                    }
 379
 0380                    _logger.LogDebug(
 0381                        "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0382                        item.GetType().Name,
 0383                        item.Name ?? "Unknown name",
 0384                        metadataPath,
 0385                        item.Id);
 386
 387                    try
 388                    {
 0389                        Directory.Delete(metadataPath, true);
 0390                    }
 0391                    catch (Exception ex)
 392                    {
 0393                        _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0394                    }
 395                }
 396
 0397                foreach (var fileSystemInfo in pathsToDelete)
 398                {
 0399                    DeleteItemPath(item, false, fileSystemInfo);
 400                }
 401            }
 402
 0403            var externalDataManager = _externalDataManagerFactory.Value;
 0404            foreach (var (item, _, _) in pathMaps)
 405            {
 0406                externalDataManager.DeleteExternalItemFiles(item);
 407            }
 408
 0409            _persistenceService.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
 0410        }
 411
 412        public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
 413        {
 0414            ArgumentNullException.ThrowIfNull(item);
 415
 0416            if (item.SourceType == SourceType.Channel)
 417            {
 0418                if (options.DeleteFromExternalProvider)
 419                {
 420                    try
 421                    {
 0422                        BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult();
 0423                    }
 0424                    catch (ArgumentException)
 425                    {
 426                        // channel no longer installed
 0427                    }
 428                }
 429
 0430                options.DeleteFileLocation = false;
 431            }
 432
 0433            if (item is LiveTvProgram)
 434            {
 0435                _logger.LogDebug(
 0436                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0437                    item.GetType().Name,
 0438                    item.Name ?? "Unknown name",
 0439                    item.Path ?? string.Empty,
 0440                    item.Id);
 441            }
 442            else
 443            {
 0444                _logger.LogInformation(
 0445                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0446                    item.GetType().Name,
 0447                    item.Name ?? "Unknown name",
 0448                    item.Path ?? string.Empty,
 0449                    item.Id);
 450            }
 451
 452            // If deleting a primary version video, clear PrimaryVersionId from alternate versions
 453            // OwnerId check: items with OwnerId set are alternate versions or extras, not primaries
 0454            if (item is Video video && !video.PrimaryVersionId.HasValue && video.OwnerId.IsEmpty())
 455            {
 0456                var localAlternateIds = GetLocalAlternateVersionIds(video).ToHashSet();
 0457                var allAlternateVersions = localAlternateIds
 0458                    .Concat(GetLinkedAlternateVersions(video).Select(v => v.Id))
 0459                    .Distinct()
 0460                    .Select(id => GetItemById(id))
 0461                    .OfType<Video>()
 0462                    .ToList();
 463
 464                // Partition alternates by whether their files still exist on disk
 0465                var alternateVersions = new List<Video>();
 0466                var missingAlternates = new List<Video>();
 0467                foreach (var alt in allAlternateVersions)
 468                {
 0469                    if (!string.IsNullOrEmpty(alt.Path) && !_fileSystem.FileExists(alt.Path))
 470                    {
 0471                        missingAlternates.Add(alt);
 472                    }
 473                    else
 474                    {
 0475                        alternateVersions.Add(alt);
 476                    }
 477                }
 478
 479                // Delete alternates whose files no longer exist to avoid ghost items.
 480                // Clear PrimaryVersionId first so DeleteItem doesn't try to update the primary being deleted.
 0481                foreach (var missing in missingAlternates)
 482                {
 0483                    _logger.LogInformation(
 0484                        "Deleting missing alternate version {Name} ({Path})",
 0485                        missing.Name ?? "Unknown name",
 0486                        missing.Path ?? string.Empty);
 0487                    missing.SetPrimaryVersionId(null);
 0488                    missing.OwnerId = Guid.Empty;
 0489                    missing.LocalAlternateVersions = [];
 0490                    missing.LinkedAlternateVersions = [];
 0491                    DeleteItem(missing, new DeleteOptions { DeleteFileLocation = false }, false);
 492                }
 493
 0494                if (alternateVersions.Count > 0)
 495                {
 0496                    _logger.LogInformation(
 0497                        "Clearing PrimaryVersionId from {Count} alternate versions of {Name}",
 0498                        alternateVersions.Count,
 0499                        item.Name ?? "Unknown name");
 500
 501                    // Promote the first alternate version to be the new primary
 0502                    var newPrimary = alternateVersions[0];
 0503                    newPrimary.SetPrimaryVersionId(null);
 0504                    newPrimary.OwnerId = Guid.Empty;
 505
 506                    // Transfer alternate version arrays from old primary to new primary
 507                    // so UpdateToRepositoryAsync creates correct LinkedChildren entries
 0508                    newPrimary.LocalAlternateVersions = video.LocalAlternateVersions
 0509                        .Where(p => !string.Equals(p, newPrimary.Path, StringComparison.OrdinalIgnoreCase))
 0510                        .ToArray();
 0511                    newPrimary.LinkedAlternateVersions = video.LinkedAlternateVersions
 0512                        .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(newPrimary.Id))
 0513                        .ToArray();
 514
 0515                    newPrimary.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter()
 516
 517                    // Re-route playlist/collection references from deleted primary to new primary
 0518                    RerouteLinkedChildReferencesAsync(video.Id, newPrimary.Id).GetAwaiter().GetResult();
 519
 520                    // Update remaining alternates to point to new primary
 0521                    foreach (var alternate in alternateVersions.Skip(1))
 522                    {
 0523                        alternate.SetPrimaryVersionId(newPrimary.Id);
 524                        // Only set OwnerId for local alternates; linked alternates are independent items
 0525                        alternate.OwnerId = localAlternateIds.Contains(alternate.Id) ? newPrimary.Id : Guid.Empty;
 0526                        alternate.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaite
 527                    }
 528                }
 529            }
 0530            else if (item is Video alternateVideo && alternateVideo.PrimaryVersionId.HasValue)
 531            {
 532                // If deleting an alternate version, re-route references to its primary
 0533                RerouteLinkedChildReferencesAsync(alternateVideo.Id, alternateVideo.PrimaryVersionId.Value).GetAwaiter()
 534
 535                // Remove deleted alternate from primary's LinkedAlternateVersions
 0536                if (GetItemById(alternateVideo.PrimaryVersionId.Value) is Video primaryVideo)
 537                {
 0538                    primaryVideo.LinkedAlternateVersions = primaryVideo.LinkedAlternateVersions
 0539                        .Where(lc => !lc.ItemId.HasValue || !lc.ItemId.Value.Equals(alternateVideo.Id))
 0540                        .ToArray();
 0541                    primaryVideo.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter
 542                }
 543            }
 544
 0545            var children = item.IsFolder
 0546                ? ((Folder)item).GetRecursiveChildren(false)
 0547                : [];
 548
 0549            foreach (var metadataPath in GetMetadataPaths(item, children))
 550            {
 0551                if (!Directory.Exists(metadataPath))
 552                {
 553                    continue;
 554                }
 555
 0556                _logger.LogDebug(
 0557                    "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0558                    item.GetType().Name,
 0559                    item.Name ?? "Unknown name",
 0560                    metadataPath,
 0561                    item.Id);
 562
 563                try
 564                {
 0565                    Directory.Delete(metadataPath, true);
 0566                }
 0567                catch (Exception ex)
 568                {
 0569                    _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0570                }
 571            }
 572
 0573            if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item))
 574            {
 575                // Assume only the first is required
 576                // Add this flag to GetDeletePaths if required in the future
 0577                var isRequiredForDelete = true;
 578
 0579                foreach (var fileSystemInfo in item.GetDeletePaths())
 580                {
 0581                    DeleteItemPath(item, isRequiredForDelete, fileSystemInfo);
 582
 0583                    isRequiredForDelete = false;
 584                }
 585            }
 586
 0587            item.SetParent(null);
 588
 0589            var externalDataManager = _externalDataManagerFactory.Value;
 0590            externalDataManager.DeleteExternalItemFiles(item);
 0591            foreach (var child in children)
 592            {
 0593                externalDataManager.DeleteExternalItemFiles(child);
 594            }
 595
 0596            _persistenceService.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
 0597            _cache.TryRemove(item.Id, out _);
 0598            foreach (var child in children)
 599            {
 0600                _cache.TryRemove(child.Id, out _);
 601            }
 602
 0603            if (parent is Folder folder)
 604            {
 0605                folder.Children = null;
 0606                folder.UserData = null;
 607            }
 608
 0609            ReportItemRemoved(item, parent);
 0610        }
 611
 612        private void DeleteItemPath(BaseItem item, bool isRequiredForDelete, FileSystemMetadata fileSystemInfo)
 613        {
 0614            if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName))
 615            {
 616                try
 617                {
 0618                    _logger.LogInformation(
 0619                        "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0620                        item.GetType().Name,
 0621                        item.Name ?? "Unknown name",
 0622                        fileSystemInfo.FullName,
 0623                        item.Id);
 624
 0625                    if (fileSystemInfo.IsDirectory)
 626                    {
 0627                        Directory.Delete(fileSystemInfo.FullName, true);
 628                    }
 629                    else
 630                    {
 0631                        File.Delete(fileSystemInfo.FullName);
 632                    }
 0633                }
 0634                catch (DirectoryNotFoundException)
 635                {
 0636                    _logger.LogInformation(
 0637                        "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id:
 0638                        item.GetType().Name,
 0639                        item.Name ?? "Unknown name",
 0640                        fileSystemInfo.FullName,
 0641                        item.Id);
 0642                }
 0643                catch (FileNotFoundException)
 644                {
 0645                    _logger.LogInformation(
 0646                        "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}
 0647                        item.GetType().Name,
 0648                        item.Name ?? "Unknown name",
 0649                        fileSystemInfo.FullName,
 0650                        item.Id);
 0651                }
 0652                catch (IOException)
 653                {
 0654                    if (isRequiredForDelete)
 655                    {
 0656                        throw;
 657                    }
 0658                }
 0659                catch (UnauthorizedAccessException)
 660                {
 0661                    if (isRequiredForDelete)
 662                    {
 0663                        throw;
 664                    }
 0665                }
 666            }
 0667        }
 668
 669        private bool IsInternalItem(BaseItem item)
 670        {
 0671            if (!item.IsFileProtocol)
 672            {
 0673                return false;
 674            }
 675
 0676            var pathToCheck = item switch
 0677            {
 0678                Genre => _configurationManager.ApplicationPaths.GenrePath,
 0679                MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
 0680                MusicGenre => _configurationManager.ApplicationPaths.MusicGenrePath,
 0681                Person => _configurationManager.ApplicationPaths.PeoplePath,
 0682                Studio => _configurationManager.ApplicationPaths.StudioPath,
 0683                Year => _configurationManager.ApplicationPaths.YearPath,
 0684                _ => null
 0685            };
 686
 0687            var itemPath = item.Path;
 0688            if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath))
 689            {
 0690                var cleanPath = _fileSystem.GetValidFilename(itemPath);
 0691                var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck);
 692
 0693                return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal);
 694            }
 695
 0696            return false;
 697        }
 698
 699        private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
 700        {
 0701            var list = GetInternalMetadataPaths(item);
 0702            foreach (var child in children)
 703            {
 0704                list.AddRange(GetInternalMetadataPaths(child));
 705            }
 706
 0707            return list;
 708        }
 709
 710        private List<string> GetInternalMetadataPaths(BaseItem item)
 711        {
 0712            var list = new List<string>
 0713            {
 0714                item.GetInternalMetadataPath()
 0715            };
 716
 0717            if (item is Video video)
 718            {
 719                // Trickplay
 0720                list.Add(_pathManager.GetTrickplayDirectory(video));
 721
 722                // Chapter Images
 0723                list.Add(_pathManager.GetChapterImageFolderPath(video));
 724
 725                // Subtitles and attachments
 0726                foreach (var mediaSource in item.GetMediaSources(false))
 727                {
 0728                    var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
 0729                    if (subtitleFolder is not null)
 730                    {
 0731                        list.Add(subtitleFolder);
 732                    }
 733
 0734                    var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
 0735                    if (attachmentFolder is not null)
 736                    {
 0737                        list.Add(attachmentFolder);
 738                    }
 739                }
 740            }
 741
 0742            return list;
 743        }
 744
 745        /// <summary>
 746        /// Resolves the item.
 747        /// </summary>
 748        /// <param name="args">The args.</param>
 749        /// <param name="resolvers">The resolvers.</param>
 750        /// <returns>BaseItem.</returns>
 751        private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers)
 752        {
 76753            var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
 76754                .FirstOrDefault(i => i is not null);
 755
 76756            if (item is not null)
 757            {
 66758                ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this);
 759            }
 760
 76761            return item;
 762        }
 763
 764        private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver)
 765        {
 766            try
 767            {
 304768                return resolver.ResolvePath(args);
 769            }
 0770            catch (Exception ex)
 771            {
 0772                _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path);
 0773                return null;
 774            }
 304775        }
 776
 777        public Guid GetNewItemId(string key, Type type)
 778        {
 129779            return GetNewItemIdInternal(key, type, false);
 780        }
 781
 782        private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive)
 783        {
 130784            ArgumentException.ThrowIfNullOrEmpty(key);
 130785            ArgumentNullException.ThrowIfNull(type);
 786
 130787            string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
 130788            if (key.StartsWith(programDataPath, StringComparison.Ordinal))
 789            {
 790                // Try to normalize paths located underneath program-data in an attempt to make them more portable
 113791                key = key.Substring(programDataPath.Length)
 113792                    .TrimStart('/', '\\')
 113793                    .Replace('/', '\\');
 794            }
 795
 130796            if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
 797            {
 1798                key = key.ToLowerInvariant();
 799            }
 800
 130801            key = type.FullName + key;
 802
 130803            return key.GetMD5();
 804        }
 805
 806        public BaseItem? ResolvePath(
 807            FileSystemMetadata fileInfo,
 808            Folder? parent = null,
 809            IDirectoryService? directoryService = null,
 810            CollectionType? collectionType = null)
 42811            => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent, collectionType
 812
 813        private void SetAdditionalPartsFromStack(Video altVideo, string path)
 814        {
 0815            if (altVideo.AdditionalParts is { Length: > 0 })
 816            {
 0817                return;
 818            }
 819
 0820            var directory = Path.GetDirectoryName(path);
 0821            if (string.IsNullOrEmpty(directory))
 822            {
 0823                return;
 824            }
 825
 826            IEnumerable<FileSystemMetadata> siblings;
 827            try
 828            {
 0829                siblings = _fileSystem.GetFiles(directory);
 0830            }
 0831            catch (Exception ex)
 832            {
 0833                _logger.LogWarning(ex, "Failed to enumerate siblings to detect stack for {Path}", path);
 0834                return;
 835            }
 836
 0837            var stacks = StackResolver.Resolve(siblings, _namingOptions);
 0838            foreach (var stack in stacks)
 839            {
 0840                if (stack.Files.Count > 1
 0841                    && string.Equals(stack.Files[0], path, StringComparison.OrdinalIgnoreCase))
 842                {
 0843                    altVideo.AdditionalParts = stack.Files.Skip(1).ToArray();
 0844                    return;
 845                }
 846            }
 0847        }
 848
 849        /// <inheritdoc />
 850        public Video? ResolveAlternateVersion(string path, Type expectedVideoType, Folder? parent, CollectionType? colle
 851        {
 852            // Clean up any existing item saved with wrong type (e.g. Video instead of Movie).
 853            // This happens when items were previously resolved without proper type context
 854            // in mixed-content libraries where collectionType is null.
 0855            var expectedId = GetNewItemId(path, expectedVideoType);
 0856            if (expectedVideoType != typeof(Video))
 857            {
 0858                var wrongTypeId = GetNewItemId(path, typeof(Video));
 0859                if (!wrongTypeId.Equals(expectedId) && GetItemById(wrongTypeId) is Video wrongTypeItem)
 860                {
 0861                    _logger.LogInformation(
 0862                        "Removing alternate version with wrong type {WrongType}, expected {ExpectedType}: {Path}",
 0863                        wrongTypeItem.GetType().Name,
 0864                        expectedVideoType.Name,
 0865                        path);
 0866                    DeleteItem(wrongTypeItem, new DeleteOptions { DeleteFileLocation = false });
 867                }
 868            }
 869
 0870            var resolved = ResolvePath(
 0871                _fileSystem.GetFileSystemInfo(path),
 0872                parent,
 0873                collectionType: collectionType) as Video;
 874
 0875            if (resolved is null)
 876            {
 0877                return null;
 878            }
 879
 880            // Ensure the alternate version has the same concrete type as the primary video.
 881            // ResolvePath may return a generic Video for files in mixed-content libraries
 882            // where collectionType is null, even though the primary is a Movie/Episode/etc.
 0883            if (resolved.GetType() != expectedVideoType)
 884            {
 0885                if (Activator.CreateInstance(expectedVideoType) is Video correctVideo)
 886                {
 0887                    correctVideo.Path = resolved.Path;
 0888                    correctVideo.Name = resolved.Name;
 0889                    correctVideo.VideoType = resolved.VideoType;
 0890                    correctVideo.ProductionYear = resolved.ProductionYear;
 0891                    correctVideo.ExtraType = resolved.ExtraType;
 0892                    resolved = correctVideo;
 893                }
 894            }
 895
 0896            resolved.Id = expectedId;
 0897            return resolved;
 898        }
 899
 900        private BaseItem? ResolvePath(
 901            FileSystemMetadata fileInfo,
 902            IDirectoryService directoryService,
 903            IItemResolver[]? resolvers,
 904            Folder? parent = null,
 905            CollectionType? collectionType = null,
 906            LibraryOptions? libraryOptions = null)
 907        {
 76908            ArgumentNullException.ThrowIfNull(fileInfo);
 909
 76910            var fullPath = fileInfo.FullName;
 911
 76912            if (collectionType is null && parent is not null)
 913            {
 17914                collectionType = GetContentTypeOverride(fullPath, true);
 915            }
 916
 76917            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
 76918            {
 76919                Parent = parent,
 76920                FileInfo = fileInfo,
 76921                CollectionType = collectionType,
 76922                LibraryOptions = libraryOptions
 76923            };
 924
 925            // Return null if ignore rules deem that we should do so
 76926            if (IgnoreFile(args.FileInfo, args.Parent))
 927            {
 0928                return null;
 929            }
 930
 931            // Gather child folder and files
 76932            if (args.IsDirectory)
 933            {
 49934                var isPhysicalRoot = args.IsPhysicalRoot;
 935
 936                // When resolving the root, we need it's grandchildren (children of user views)
 49937                var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
 938
 939                FileSystemMetadata[] files;
 49940                var isVf = args.IsVf;
 941
 942                try
 943                {
 49944                    files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _l
 49945                }
 0946                catch (Exception ex)
 947                {
 0948                    if (parent is not null && parent.IsPhysicalRoot)
 949                    {
 0950                        _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPh
 951
 0952                        files = [];
 953                    }
 954                    else
 955                    {
 0956                        throw;
 957                    }
 0958                }
 959
 960                // Need to remove sub-paths that may have been resolved from shortcuts
 961                // Example: if \\server\movies exists, then strip out \\server\movies\action
 49962                if (isPhysicalRoot)
 963                {
 21964                    files = NormalizeRootPathList(files).ToArray();
 965                }
 966
 49967                args.FileSystemChildren = files;
 968            }
 969
 970            // Filter content based on ignore rules
 76971            if (args.IsDirectory)
 972            {
 49973                var filtered = args.GetActualFileSystemChildren().ToArray();
 49974                args.FileSystemChildren = filtered ?? [];
 975            }
 976
 76977            return ResolveItem(args, resolvers);
 978        }
 979
 980        public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent)
 101981            => EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent));
 982
 983        public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
 984        {
 80985            var originalList = paths.ToList();
 986
 80987            var list = originalList.Where(i => i.IsDirectory)
 80988                .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
 80989                .Distinct()
 80990                .ToList();
 991
 80992            var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.Ordinal) && list.Any(i => _fileS
 80993                .ToList();
 994
 160995            foreach (var dupe in dupes)
 996            {
 0997                _logger.LogInformation("Found duplicate path: {0}", dupe);
 998            }
 999
 801000            var newList = list.Except(dupes, StringComparer.Ordinal).Select(_fileSystem.GetDirectoryInfo).ToList();
 801001            newList.AddRange(originalList.Where(i => !i.IsDirectory));
 801002            return newList;
 1003        }
 1004
 1005        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryServ
 1006        {
 581007            return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
 1008        }
 1009
 1010        public IEnumerable<BaseItem> ResolvePaths(
 1011            IEnumerable<FileSystemMetadata> files,
 1012            IDirectoryService directoryService,
 1013            Folder parent,
 1014            LibraryOptions libraryOptions,
 1015            CollectionType? collectionType,
 1016            IItemResolver[] resolvers)
 1017        {
 581018            var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
 1019
 581020            if (parent is not null)
 1021            {
 581022                var multiItemResolvers = resolvers is null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>()
 1023
 3481024                foreach (var resolver in multiItemResolvers)
 1025                {
 1161026                    var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
 1027
 1161028                    if (result?.Items.Count > 0)
 1029                    {
 01030                        var items = result.Items;
 01031                        items.RemoveAll(item => !ResolverHelper.SetInitialItemValues(item, parent, this, directoryServic
 01032                        items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, reso
 01033                        return items;
 1034                    }
 1035                }
 1036            }
 1037
 581038            return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions);
 01039        }
 1040
 1041        private IEnumerable<BaseItem> ResolveFileList(
 1042            IReadOnlyList<FileSystemMetadata> fileList,
 1043            IDirectoryService directoryService,
 1044            Folder? parent,
 1045            CollectionType? collectionType,
 1046            IItemResolver[]? resolvers,
 1047            LibraryOptions libraryOptions)
 1048        {
 1049            // Given that fileList is a list we can save enumerator allocations by indexing
 1481050            for (var i = 0; i < fileList.Count; i++)
 1051            {
 171052                var file = fileList[i];
 171053                BaseItem? result = null;
 1054                try
 1055                {
 171056                    result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
 171057                }
 01058                catch (Exception ex)
 1059                {
 01060                    _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
 01061                }
 1062
 171063                if (result is not null)
 1064                {
 71065                    yield return result;
 1066                }
 1067            }
 571068        }
 1069
 1070        /// <summary>
 1071        /// Creates the root media folder.
 1072        /// </summary>
 1073        /// <returns>AggregateFolder.</returns>
 1074        /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</except
 1075        public AggregateFolder CreateRootFolder()
 1076        {
 211077            var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath;
 1078
 211079            var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
 211080                             (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOp
 211081                             .DeepCopy<Folder, AggregateFolder>();
 1082
 1083            // In case program data folder was moved
 211084            if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal))
 1085            {
 01086                _logger.LogInformation("Resetting root folder path to {0}", rootFolderPath);
 01087                rootFolder.Path = rootFolderPath;
 1088            }
 1089
 1090            // Add in the plug-in folders
 211091            var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
 1092
 211093            var info = Directory.CreateDirectory(path);
 211094            Folder folder = new PlaylistsFolder
 211095            {
 211096                Path = path,
 211097                DateCreated = info.CreationTimeUtc,
 211098                DateModified = info.LastWriteTimeUtc,
 211099            };
 1100
 211101            if (folder.Id.IsEmpty())
 1102            {
 211103                folder.Id = GetNewItemId(folder.Path, folder.GetType());
 1104            }
 1105
 211106            var dbItem = GetItemById(folder.Id) as BasePluginFolder;
 1107
 211108            if (dbItem is not null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
 1109            {
 01110                folder = dbItem;
 1111            }
 1112
 211113            if (!folder.ParentId.Equals(rootFolder.Id))
 1114            {
 211115                rootFolder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().G
 211116                folder.ParentId = rootFolder.Id;
 211117                folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetRe
 1118            }
 1119
 211120            rootFolder.AddVirtualChild(folder);
 1121
 211122            RegisterItem(folder);
 1123
 211124            return rootFolder;
 1125        }
 1126
 1127        public Folder GetUserRootFolder()
 1128        {
 8451129            if (_userRootFolder is null)
 211130            {
 1131                lock (_userRootFolderSyncLock)
 1132                {
 211133                    if (_userRootFolder is null)
 1134                    {
 211135                        var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 1136
 211137                        _logger.LogDebug("Creating userRootPath at {Path}", userRootPath);
 211138                        Directory.CreateDirectory(userRootPath);
 1139
 211140                        var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
 211141                        UserRootFolder? tmpItem = null;
 1142                        try
 1143                        {
 211144                            tmpItem = GetItemById(newItemId) as UserRootFolder;
 211145                        }
 01146                        catch (Exception ex)
 1147                        {
 01148                            _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId);
 01149                        }
 1150
 211151                        if (tmpItem is null)
 1152                        {
 211153                            _logger.LogDebug("Creating new userRootFolder with DeepCopy");
 211154                            tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new In
 211155                                        .DeepCopy<Folder, UserRootFolder>();
 1156                        }
 1157
 1158                        // In case program data folder was moved
 211159                        if (!string.Equals(tmpItem.Path, userRootPath, StringComparison.Ordinal))
 1160                        {
 01161                            _logger.LogInformation("Resetting user root folder path to {0}", userRootPath);
 01162                            tmpItem.Path = userRootPath;
 1163                        }
 1164
 211165                        _userRootFolder = tmpItem;
 211166                        _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder);
 1167                    }
 211168                }
 1169            }
 1170
 8451171            return _userRootFolder;
 1172        }
 1173
 1174        /// <inheritdoc />
 1175        public BaseItem? FindByPath(string path, bool? isFolder)
 1176        {
 1177            // If this returns multiple items it could be tricky figuring out which one is correct.
 1178            // In most cases, the newest one will be and the others obsolete but not yet cleaned up
 01179            ArgumentException.ThrowIfNullOrEmpty(path);
 1180
 01181            var query = new InternalItemsQuery
 01182            {
 01183                Path = path,
 01184                IsFolder = isFolder,
 01185                OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)],
 01186                Limit = 1,
 01187                DtoOptions = new DtoOptions(true)
 01188            };
 1189
 01190            return GetItemList(query)
 01191                .FirstOrDefault();
 1192        }
 1193
 1194        /// <inheritdoc />
 1195        public Person? GetPerson(string name)
 1196        {
 11197            var path = Person.GetPath(name);
 11198            var id = GetItemByNameId<Person>(path);
 11199            if (GetItemById(id) is Person item)
 1200            {
 01201                return item;
 1202            }
 1203
 11204            return null;
 1205        }
 1206
 1207        /// <summary>
 1208        /// Gets the studio.
 1209        /// </summary>
 1210        /// <param name="name">The name.</param>
 1211        /// <returns>Task{Studio}.</returns>
 1212        public Studio GetStudio(string name)
 1213        {
 01214            return CreateItemByName<Studio>(Studio.GetPath, name, new DtoOptions(true));
 1215        }
 1216
 1217        public Guid GetStudioId(string name)
 1218        {
 01219            return GetItemByNameId<Studio>(Studio.GetPath(name));
 1220        }
 1221
 1222        public Guid GetGenreId(string name)
 1223        {
 01224            return GetItemByNameId<Genre>(Genre.GetPath(name));
 1225        }
 1226
 1227        public Guid GetMusicGenreId(string name)
 1228        {
 01229            return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
 1230        }
 1231
 1232        /// <summary>
 1233        /// Gets the genre.
 1234        /// </summary>
 1235        /// <param name="name">The name.</param>
 1236        /// <returns>Task{Genre}.</returns>
 1237        public Genre GetGenre(string name)
 1238        {
 01239            return CreateItemByName<Genre>(Genre.GetPath, name, new DtoOptions(true));
 1240        }
 1241
 1242        /// <summary>
 1243        /// Gets the music genre.
 1244        /// </summary>
 1245        /// <param name="name">The name.</param>
 1246        /// <returns>Task{MusicGenre}.</returns>
 1247        public MusicGenre GetMusicGenre(string name)
 1248        {
 01249            return CreateItemByName<MusicGenre>(MusicGenre.GetPath, name, new DtoOptions(true));
 1250        }
 1251
 1252        /// <summary>
 1253        /// Gets the year.
 1254        /// </summary>
 1255        /// <param name="value">The value.</param>
 1256        /// <returns>Task{Year}.</returns>
 1257        public Year GetYear(int value)
 1258        {
 01259            if (value <= 0)
 1260            {
 01261                throw new ArgumentOutOfRangeException(nameof(value), "Years less than or equal to 0 are invalid.");
 1262            }
 1263
 01264            var name = value.ToString(CultureInfo.InvariantCulture);
 1265
 01266            return CreateItemByName<Year>(Year.GetPath, name, new DtoOptions(true));
 1267        }
 1268
 1269        /// <summary>
 1270        /// Gets a Genre.
 1271        /// </summary>
 1272        /// <param name="name">The name.</param>
 1273        /// <returns>Task{Genre}.</returns>
 1274        public MusicArtist GetArtist(string name)
 1275        {
 01276            return GetArtist(name, new DtoOptions(true));
 1277        }
 1278
 1279        public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
 1280        {
 171281            return _linkedChildrenService.FindArtists(names);
 1282        }
 1283
 1284        public MusicArtist GetArtist(string name, DtoOptions options)
 1285        {
 01286            return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options);
 1287        }
 1288
 1289        private T CreateItemByName<T>(Func<string, string> getPathFn, string name, DtoOptions options)
 1290            where T : BaseItem, new()
 1291        {
 01292            if (typeof(T) == typeof(MusicArtist))
 1293            {
 01294                var existing = GetItemList(new InternalItemsQuery
 01295                {
 01296                    IncludeItemTypes = [BaseItemKind.MusicArtist],
 01297                    Name = name,
 01298                    UseRawName = true,
 01299                    DtoOptions = options
 01300                }).Cast<MusicArtist>()
 01301                .OrderBy(i => i.IsAccessedByName ? 1 : 0)
 01302                .Cast<T>()
 01303                .FirstOrDefault();
 1304
 01305                if (existing is not null)
 1306                {
 01307                    return existing;
 1308                }
 1309            }
 1310
 01311            var path = getPathFn(name);
 01312            var id = GetItemByNameId<T>(path);
 01313            var item = GetItemById(id) as T;
 01314            if (item is null)
 1315            {
 01316                var info = Directory.CreateDirectory(path);
 01317                item = new T
 01318                {
 01319                    Name = name,
 01320                    Id = id,
 01321                    DateCreated = info.CreationTimeUtc,
 01322                    DateModified = info.LastWriteTimeUtc,
 01323                    Path = path
 01324                };
 1325
 01326                CreateItem(item, null);
 1327            }
 1328
 01329            return item;
 1330        }
 1331
 1332        private Guid GetItemByNameId<T>(string path)
 1333              where T : BaseItem, new()
 1334        {
 11335            var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
 11336            return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
 1337        }
 1338
 1339        /// <inheritdoc />
 1340        public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
 1341        {
 1342            // Ensure the location is available.
 01343            Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
 1344
 01345            return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
 1346        }
 1347
 1348        /// <summary>
 1349        /// Reloads the root media folder.
 1350        /// </summary>
 1351        /// <param name="progress">The progress.</param>
 1352        /// <param name="cancellationToken">The cancellation token.</param>
 1353        /// <returns>Task.</returns>
 1354        public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
 1355        {
 1356            // Just run the scheduled task so that the user can see it
 31357            _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 1358
 31359            return Task.CompletedTask;
 1360        }
 1361
 1362        /// <summary>
 1363        /// Validates the media library internal.
 1364        /// </summary>
 1365        /// <param name="progress">The progress.</param>
 1366        /// <param name="cancellationToken">The cancellation token.</param>
 1367        /// <returns>Task.</returns>
 1368        public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
 1369        {
 191370            IsScanRunning = true;
 191371            ClearIgnoreRuleCache();
 191372            LibraryMonitor.Stop();
 1373
 1374            try
 1375            {
 191376                await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false);
 171377            }
 1378            finally
 1379            {
 191380                ClearIgnoreRuleCache();
 191381                LibraryMonitor.Start();
 191382                IsScanRunning = false;
 1383            }
 171384        }
 1385
 1386        public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
 1387        {
 221388            ClearIgnoreRuleCache();
 221389            RootFolder.Children = null;
 221390            await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1391
 1392            // Start by just validating the children of the root, but go no further
 211393            await RootFolder.ValidateChildren(
 211394                new Progress<double>(),
 211395                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 211396                recursive: false,
 211397                allowRemoveRoot: removeRoot,
 211398                cancellationToken: cancellationToken).ConfigureAwait(false);
 1399
 201400            var rootFolder = GetUserRootFolder();
 201401            rootFolder.Children = null;
 1402
 201403            await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1404
 201405            await rootFolder.ValidateChildren(
 201406                new Progress<double>(),
 201407                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 201408                recursive: false,
 201409                allowRemoveRoot: removeRoot,
 201410                cancellationToken: cancellationToken).ConfigureAwait(false);
 1411
 1412            // Quickly scan CollectionFolders for changes
 201413            var toDelete = new List<Guid>();
 561414            foreach (var child in rootFolder.Children!.OfType<Folder>())
 1415            {
 1416                // If the user has somehow deleted the collection directory, remove the metadata from the database.
 81417                if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
 1418                {
 11419                    toDelete.Add(collectionFolder.Id);
 1420                }
 1421                else
 1422                {
 71423                    await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1424                }
 1425            }
 1426
 201427            if (toDelete.Count > 0)
 1428            {
 11429                _persistenceService.DeleteItem(toDelete.ToArray());
 1430            }
 1431
 201432            ClearIgnoreRuleCache();
 201433        }
 1434
 1435        /// <inheritdoc />
 1436        public void ClearIgnoreRuleCache()
 1437        {
 801438            _dotIgnoreIgnoreRule.ClearDirectoryCache();
 801439        }
 1440
 1441        private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
 1442        {
 191443            _logger.LogInformation("Validating media library");
 1444
 191445            await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false);
 1446
 171447            var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96));
 1448
 1449            // Validate the entire media library
 171450            await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem
 1451
 171452            progress.Report(96);
 1453
 171454            innerProgress = new Progress<double>(pct => progress.Report(96 + (pct * .04)));
 1455
 171456            await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
 1457
 171458            progress.Report(100);
 171459        }
 1460
 1461        /// <summary>
 1462        /// Runs the post scan tasks.
 1463        /// </summary>
 1464        /// <param name="progress">The progress.</param>
 1465        /// <param name="cancellationToken">The cancellation token.</param>
 1466        /// <returns>Task.</returns>
 1467        private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
 1468        {
 171469            var tasks = PostScanTasks.ToList();
 1470
 171471            var numComplete = 0;
 171472            var numTasks = tasks.Count;
 1473
 2721474            foreach (var task in tasks)
 1475            {
 1476                // Prevent access to modified closure
 1191477                var currentNumComplete = numComplete;
 1478
 1191479                var innerProgress = new Progress<double>(pct =>
 1191480                {
 1191481                    double innerPercent = pct;
 1191482                    innerPercent /= 100;
 1191483                    innerPercent += currentNumComplete;
 1191484
 1191485                    innerPercent /= numTasks;
 1191486                    innerPercent *= 100;
 1191487
 1191488                    progress.Report(innerPercent);
 1191489                });
 1490
 1191491                _logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
 1492
 1493                try
 1494                {
 1191495                    await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
 1191496                }
 01497                catch (OperationCanceledException)
 1498                {
 01499                    _logger.LogInformation("Post-scan task cancelled: {0}", task.GetType().Name);
 01500                    throw;
 1501                }
 01502                catch (Exception ex)
 1503                {
 01504                    _logger.LogError(ex, "Error running post-scan task");
 01505                }
 1506
 1191507                numComplete++;
 1191508                double percent = numComplete;
 1191509                percent /= numTasks;
 1191510                progress.Report(percent * 100);
 1191511            }
 1512
 171513            _persistenceService.UpdateInheritedValues();
 1514
 171515            progress.Report(100);
 171516        }
 1517
 1518        /// <summary>
 1519        /// Gets the default view.
 1520        /// </summary>
 1521        /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
 1522        public List<VirtualFolderInfo> GetVirtualFolders()
 1523        {
 231524            return GetVirtualFolders(false);
 1525        }
 1526
 1527        public List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState)
 1528        {
 241529            _logger.LogDebug("Getting topLibraryFolders");
 241530            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 1531
 241532            _logger.LogDebug("Getting refreshQueue");
 241533            var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null;
 1534
 241535            return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath)
 241536                .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
 241537                .ToList();
 1538        }
 1539
 1540        private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid>? r
 1541        {
 11542            var info = new VirtualFolderInfo
 11543            {
 11544                Name = Path.GetFileName(dir),
 11545
 11546                Locations = _fileSystem.GetFilePaths(dir, false)
 11547                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 11548                    .Select(i =>
 11549                    {
 11550                        try
 11551                        {
 11552                            return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i));
 11553                        }
 11554                        catch (Exception ex)
 11555                        {
 11556                            _logger.LogError(ex, "Error resolving shortcut file {File}", i);
 11557                            return null;
 11558                        }
 11559                    })
 11560                    .Where(i => i is not null)
 11561                    .Order()
 11562                    .ToArray(),
 11563
 11564                CollectionType = GetCollectionType(dir)
 11565            };
 1566
 11567            var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.Ord
 11568            if (libraryFolder is not null)
 1569            {
 11570                var libraryFolderId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture);
 11571                info.ItemId = libraryFolderId;
 11572                if (libraryFolder.HasImage(ImageType.Primary))
 1573                {
 01574                    info.PrimaryImageItemId = libraryFolderId;
 1575                }
 1576
 11577                info.LibraryOptions = GetLibraryOptions(libraryFolder);
 1578
 11579                if (refreshQueue is not null)
 1580                {
 11581                    info.RefreshProgress = libraryFolder.GetRefreshProgress();
 1582
 11583                    info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.Contains(libraryFolder.
 1584                }
 1585            }
 1586
 11587            return info;
 1588        }
 1589
 1590        private CollectionTypeOptions? GetCollectionType(string path)
 1591        {
 11592            var files = _fileSystem.GetFilePaths(path, [".collection"], true, false);
 21593            foreach (ReadOnlySpan<char> file in files)
 1594            {
 01595                if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
 1596                {
 01597                    return res;
 1598                }
 1599            }
 1600
 11601            return null;
 01602        }
 1603
 1604        /// <inheritdoc />
 1605        public BaseItem? GetItemById(Guid id)
 1606        {
 5261607            if (id.IsEmpty())
 1608            {
 01609                throw new ArgumentException("Guid can't be empty", nameof(id));
 1610            }
 1611
 5261612            if (_cache.TryGet(id, out var item))
 1613            {
 4221614                return item;
 1615            }
 1616
 1041617            item = RetrieveItem(id);
 1618
 1041619            if (item is not null)
 1620            {
 01621                RegisterItem(item);
 1622            }
 1623
 1041624            return item;
 1625        }
 1626
 1627        /// <inheritdoc />
 1628        public T? GetItemById<T>(Guid id)
 1629            where T : BaseItem
 1630        {
 241631            var item = GetItemById(id);
 241632            if (item is T typedItem)
 1633            {
 11634                return typedItem;
 1635            }
 1636
 231637            return null;
 1638        }
 1639
 1640        /// <inheritdoc />
 1641        public T? GetItemById<T>(Guid id, Guid userId)
 1642            where T : BaseItem
 1643        {
 11644            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 11645            return GetItemById<T>(id, user);
 1646        }
 1647
 1648        /// <inheritdoc />
 1649        public T? GetItemById<T>(Guid id, User? user)
 1650            where T : BaseItem
 1651        {
 221652            var item = GetItemById<T>(id);
 221653            return ItemIsVisible(item, user) ? item : null;
 1654        }
 1655
 1656        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
 1657        {
 1751658            if (query.Recursive && !query.ParentId.IsEmpty())
 1659            {
 431660                var parent = GetItemById(query.ParentId);
 431661                if (parent is not null)
 1662                {
 431663                    SetTopParentIdsOrAncestors(query, [parent]);
 1664                }
 1665            }
 1666
 1751667            if (query.User is not null)
 1668            {
 11669                AddUserToQuery(query, query.User, allowExternalContent);
 1670            }
 1671
 1751672            return _itemRepository.GetItemList(query);
 1673        }
 1674
 1675        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 1676        {
 1751677            return GetItemList(query, true);
 1678        }
 1679
 1680        public int GetCount(InternalItemsQuery query)
 1681        {
 01682            if (query.Recursive && !query.ParentId.IsEmpty())
 1683            {
 01684                var parent = GetItemById(query.ParentId);
 01685                if (parent is not null)
 1686                {
 01687                    SetTopParentIdsOrAncestors(query, [parent]);
 1688                }
 1689            }
 1690
 01691            if (query.User is not null)
 1692            {
 01693                AddUserToQuery(query, query.User);
 1694            }
 1695
 01696            return _countService.GetCount(query);
 1697        }
 1698
 1699        public ItemCounts GetItemCounts(InternalItemsQuery query)
 1700        {
 01701            if (query.Recursive && !query.ParentId.IsEmpty())
 1702            {
 01703                var parent = GetItemById(query.ParentId);
 01704                if (parent is not null)
 1705                {
 01706                    SetTopParentIdsOrAncestors(query, [parent]);
 1707                }
 1708            }
 1709
 01710            if (query.User is not null)
 1711            {
 01712                AddUserToQuery(query, query.User);
 1713            }
 1714
 01715            return _countService.GetItemCounts(query);
 1716        }
 1717
 1718        /// <inheritdoc/>
 1719        public ItemCounts GetItemCountsForNameItem(BaseItemKind kind, Guid id, BaseItemKind[] relatedItemKinds, User? us
 1720        {
 01721            var query = new InternalItemsQuery(user);
 01722            if (user is not null)
 1723            {
 01724                AddUserToQuery(query, user);
 1725            }
 1726
 01727            return _countService.GetItemCountsForNameItem(kind, id, relatedItemKinds, query);
 1728        }
 1729
 1730        public Dictionary<Guid, int> GetChildCountBatch(IReadOnlyList<Guid> parentIds, Guid? userId)
 1731        {
 01732            return _countService.GetChildCountBatch(parentIds, userId);
 1733        }
 1734
 1735        /// <inheritdoc/>
 1736        public Dictionary<Guid, (int Played, int Total)> GetPlayedAndTotalCountBatch(IReadOnlyList<Guid> folderIds, User
 1737        {
 01738            return _countService.GetPlayedAndTotalCountBatch(folderIds, user);
 1739        }
 1740
 1741        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
 1742        {
 01743            SetTopParentIdsOrAncestors(query, parents);
 1744
 01745            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1746            {
 01747                if (query.User is not null)
 1748                {
 01749                    AddUserToQuery(query, query.User);
 1750                }
 1751            }
 1752
 01753            return _itemRepository.GetItemList(query);
 1754        }
 1755
 1756        public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, Coll
 1757        {
 01758            SetTopParentIdsOrAncestors(query, parents);
 1759
 01760            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1761            {
 01762                if (query.User is not null)
 1763                {
 01764                    AddUserToQuery(query, query.User);
 1765                }
 1766            }
 1767
 01768            return _itemRepository.GetLatestItemList(query, collectionType);
 1769        }
 1770
 1771        public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents
 1772        {
 01773            SetTopParentIdsOrAncestors(query, parents);
 1774
 01775            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1776            {
 01777                if (query.User is not null)
 1778                {
 01779                    AddUserToQuery(query, query.User);
 1780                }
 1781            }
 1782
 01783            return _nextUpService.GetNextUpSeriesKeys(query, dateCutoff);
 1784        }
 1785
 1786        /// <inheritdoc />
 1787        public IReadOnlyDictionary<string, MediaBrowser.Controller.Persistence.NextUpEpisodeBatchResult> GetNextUpEpisod
 1788            InternalItemsQuery query,
 1789            IReadOnlyList<string> seriesKeys,
 1790            bool includeSpecials,
 1791            bool includeWatchedForRewatching)
 1792        {
 01793            return _nextUpService.GetNextUpEpisodesBatch(query, seriesKeys, includeSpecials, includeWatchedForRewatching
 1794        }
 1795
 1796        public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
 1797        {
 01798            if (query.User is not null)
 1799            {
 01800                AddUserToQuery(query, query.User);
 1801            }
 1802
 01803            if (query.EnableTotalRecordCount)
 1804            {
 01805                return _itemRepository.GetItems(query);
 1806            }
 1807
 01808            return new QueryResult<BaseItem>(
 01809                query.StartIndex,
 01810                null,
 01811                _itemRepository.GetItemList(query));
 1812        }
 1813
 1814        public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
 1815        {
 851816            if (query.User is not null)
 1817            {
 01818                AddUserToQuery(query, query.User);
 1819            }
 1820
 851821            return _itemRepository.GetItemIdsList(query);
 1822        }
 1823
 1824        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
 1825        {
 01826            if (query.User is not null)
 1827            {
 01828                AddUserToQuery(query, query.User);
 1829            }
 1830
 01831            SetTopParentOrAncestorIds(query);
 01832            return _itemRepository.GetStudios(query);
 1833        }
 1834
 1835        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
 1836        {
 01837            if (query.User is not null)
 1838            {
 01839                AddUserToQuery(query, query.User);
 1840            }
 1841
 01842            SetTopParentOrAncestorIds(query);
 01843            return _itemRepository.GetGenres(query);
 1844        }
 1845
 1846        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
 1847        {
 01848            if (query.User is not null)
 1849            {
 01850                AddUserToQuery(query, query.User);
 1851            }
 1852
 01853            SetTopParentOrAncestorIds(query);
 01854            return _itemRepository.GetMusicGenres(query);
 1855        }
 1856
 1857        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
 1858        {
 01859            if (query.User is not null)
 1860            {
 01861                AddUserToQuery(query, query.User);
 1862            }
 1863
 01864            SetTopParentOrAncestorIds(query);
 01865            return _itemRepository.GetAllArtists(query);
 1866        }
 1867
 1868        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
 1869        {
 01870            if (query.User is not null)
 1871            {
 01872                AddUserToQuery(query, query.User);
 1873            }
 1874
 01875            SetTopParentOrAncestorIds(query);
 01876            return _itemRepository.GetArtists(query);
 1877        }
 1878
 1879        private void SetTopParentOrAncestorIds(InternalItemsQuery query)
 1880        {
 01881            var ancestorIds = query.AncestorIds;
 01882            int len = ancestorIds.Length;
 01883            if (len == 0)
 1884            {
 01885                return;
 1886            }
 1887
 01888            var parents = new BaseItem[len];
 01889            for (int i = 0; i < len; i++)
 1890            {
 01891                parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id:
 01892                if (parents[i] is not (ICollectionFolder or UserView))
 1893                {
 01894                    return;
 1895                }
 1896            }
 1897
 1898            // Optimize by querying against top level views
 01899            query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 01900            query.AncestorIds = [];
 1901
 1902            // Prevent searching in all libraries due to empty filter
 01903            if (query.TopParentIds.Length == 0)
 1904            {
 01905                query.TopParentIds = [Guid.NewGuid()];
 1906            }
 01907        }
 1908
 1909        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
 1910        {
 01911            if (query.User is not null)
 1912            {
 01913                AddUserToQuery(query, query.User);
 1914            }
 1915
 01916            SetTopParentOrAncestorIds(query);
 01917            return _itemRepository.GetAlbumArtists(query);
 1918        }
 1919
 1920        public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
 1921        {
 151922            if (query.Recursive && !query.ParentId.IsEmpty())
 1923            {
 141924                var parent = GetItemById(query.ParentId);
 141925                if (parent is not null)
 1926                {
 141927                    SetTopParentIdsOrAncestors(query, [parent]);
 1928                }
 1929            }
 1930
 151931            if (query.User is not null)
 1932            {
 11933                AddUserToQuery(query, query.User);
 1934            }
 1935
 151936            if (query.EnableTotalRecordCount)
 1937            {
 11938                return _itemRepository.GetItems(query);
 1939            }
 1940
 141941            return new QueryResult<BaseItem>(
 141942                query.StartIndex,
 141943                null,
 141944                _itemRepository.GetItemList(query));
 1945        }
 1946
 1947        private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
 1948        {
 571949            if (parents.All(i => i is ICollectionFolder || i is UserView))
 1950            {
 1951                // Optimize by querying against top level views
 141952                query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 1953
 1954                // Prevent searching in all libraries due to empty filter
 141955                if (query.TopParentIds.Length == 0)
 1956                {
 141957                    query.TopParentIds = [Guid.NewGuid()];
 1958                }
 1959            }
 431960            else if (parents.Count == 1 && parents.First() is Folder folder
 431961                && (folder is Playlist || folder is BoxSet)
 431962                && folder.LinkedChildren.Length > 0)
 1963            {
 1964                // Playlists and BoxSets store their contents in LinkedChildren and never
 1965                // populate AncestorIds for those items, so a recursive AncestorIds query
 1966                // would return zero rows. Resolve to the linked child IDs up front and
 1967                // route through the existing indexed ItemIds filter.
 01968                query.ItemIds = folder.LinkedChildren
 01969                    .Where(lc => lc.ItemId.HasValue && !lc.ItemId.Value.IsEmpty())
 01970                    .Select(lc => lc.ItemId!.Value)
 01971                    .ToArray();
 1972
 1973                // Empty linked-children should still return empty rather than scanning everything.
 01974                if (query.ItemIds.Length == 0)
 1975                {
 01976                    query.ItemIds = [Guid.NewGuid()];
 1977                }
 1978            }
 1979            else
 1980            {
 1981                // We need to be able to query from any arbitrary ancestor up the tree
 431982                query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
 1983
 1984                // Prevent searching in all libraries due to empty filter
 431985                if (query.AncestorIds.Length == 0)
 1986                {
 01987                    query.AncestorIds = [Guid.NewGuid()];
 1988                }
 1989            }
 1990
 571991            query.Parent = null;
 571992        }
 1993
 1994        private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
 1995        {
 21996            if (query.User is null)
 1997            {
 01998                query.SetUser(user);
 1999            }
 2000
 22001            if (query.AncestorIds.Length == 0 &&
 22002                query.ParentId.IsEmpty() &&
 22003                query.ChannelIds.Count == 0 &&
 22004                query.TopParentIds.Length == 0 &&
 22005                string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
 22006                string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
 22007                query.ItemIds.Length == 0 &&
 22008                query.OwnerIds.Length == 0)
 2009            {
 12010                var userViews = UserViewManager.GetUserViews(new UserViewQuery
 12011                {
 12012                    User = user,
 12013                    IncludeHidden = true,
 12014                    IncludeExternalContent = allowExternalContent
 12015                });
 2016
 12017                query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
 2018
 2019                // Prevent searching in all libraries due to empty filter
 12020                if (query.TopParentIds.Length == 0)
 2021                {
 12022                    query.TopParentIds = [Guid.NewGuid()];
 2023                }
 2024            }
 22025        }
 2026
 2027        /// <inheritdoc/>
 2028        public void ConfigureUserAccess(InternalItemsQuery query, User user)
 2029        {
 02030            ArgumentNullException.ThrowIfNull(query);
 02031            ArgumentNullException.ThrowIfNull(user);
 2032
 02033            AddUserToQuery(query, user);
 02034        }
 2035
 2036        private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
 2037        {
 142038            if (item is UserView view)
 2039            {
 02040                if (view.ViewType == CollectionType.livetv)
 2041                {
 02042                    return [view.Id];
 2043                }
 2044
 2045                // Translate view into folders
 02046                if (!view.DisplayParentId.IsEmpty())
 2047                {
 02048                    var displayParent = GetItemById(view.DisplayParentId);
 02049                    if (displayParent is not null)
 2050                    {
 02051                        return GetTopParentIdsForQuery(displayParent, user);
 2052                    }
 2053
 02054                    return [];
 2055                }
 2056
 02057                if (!view.ParentId.IsEmpty())
 2058                {
 02059                    var displayParent = GetItemById(view.ParentId);
 02060                    if (displayParent is not null)
 2061                    {
 02062                        return GetTopParentIdsForQuery(displayParent, user);
 2063                    }
 2064
 02065                    return [];
 2066                }
 2067
 2068                // Handle grouping
 02069                if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.V
 02070                    && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
 2071                {
 02072                    return GetUserRootFolder()
 02073                        .GetChildren(user, true)
 02074                        .OfType<CollectionFolder>()
 02075                        .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
 02076                        .Where(i => user.IsFolderGrouped(i.Id))
 02077                        .SelectMany(i => GetTopParentIdsForQuery(i, user));
 2078                }
 2079
 02080                return [];
 2081            }
 2082
 142083            if (item is CollectionFolder collectionFolder)
 2084            {
 142085                return collectionFolder.PhysicalFolderIds;
 2086            }
 2087
 02088            var topParent = item.GetTopParent();
 02089            if (topParent is not null)
 2090            {
 02091                return [topParent.Id];
 2092            }
 2093
 02094            return [];
 2095        }
 2096
 2097        /// <summary>
 2098        /// Gets the intros.
 2099        /// </summary>
 2100        /// <param name="item">The item.</param>
 2101        /// <param name="user">The user.</param>
 2102        /// <returns>IEnumerable{System.String}.</returns>
 2103        public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
 2104        {
 02105            if (IntroProviders.Length == 0)
 2106            {
 02107                return [];
 2108            }
 2109
 02110            var tasks = IntroProviders
 02111                .Select(i => GetIntros(i, item, user));
 2112
 02113            var items = await Task.WhenAll(tasks).ConfigureAwait(false);
 2114
 02115            return items
 02116                .SelectMany(i => i)
 02117                .Select(ResolveIntro)
 02118                .Where(i => i is not null)!; // null values got filtered out
 02119        }
 2120
 2121        /// <summary>
 2122        /// Gets the intros.
 2123        /// </summary>
 2124        /// <param name="provider">The provider.</param>
 2125        /// <param name="item">The item.</param>
 2126        /// <param name="user">The user.</param>
 2127        /// <returns>Task&lt;IEnumerable&lt;IntroInfo&gt;&gt;.</returns>
 2128        private async Task<IEnumerable<IntroInfo>> GetIntros(IIntroProvider provider, BaseItem item, User user)
 2129        {
 2130            try
 2131            {
 02132                return await provider.GetIntros(item, user).ConfigureAwait(false);
 2133            }
 02134            catch (Exception ex)
 2135            {
 02136                _logger.LogError(ex, "Error getting intros");
 2137
 02138                return [];
 2139            }
 02140        }
 2141
 2142        /// <summary>
 2143        /// Resolves the intro.
 2144        /// </summary>
 2145        /// <param name="info">The info.</param>
 2146        /// <returns>Video.</returns>
 2147        private Video? ResolveIntro(IntroInfo info)
 2148        {
 02149            Video? video = null;
 2150
 02151            if (info.ItemId.HasValue)
 2152            {
 2153                // Get an existing item by Id
 02154                video = GetItemById(info.ItemId.Value) as Video;
 2155
 02156                if (video is null)
 2157                {
 02158                    _logger.LogError("Unable to locate item with Id {ID}.", info.ItemId.Value);
 2159                }
 2160            }
 02161            else if (!string.IsNullOrEmpty(info.Path))
 2162            {
 2163                try
 2164                {
 2165                    // Try to resolve the path into a video
 02166                    video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video;
 2167
 02168                    if (video is null)
 2169                    {
 02170                        _logger.LogError("Intro resolver returned null for {Path}.", info.Path);
 2171                    }
 2172                    else
 2173                    {
 2174                        // Pull the saved db item that will include metadata
 02175                        var dbItem = GetItemById(video.Id) as Video;
 2176
 02177                        if (dbItem is not null)
 2178                        {
 02179                            video = dbItem;
 2180                        }
 2181                        else
 2182                        {
 02183                            return null;
 2184                        }
 2185                    }
 02186                }
 02187                catch (Exception ex)
 2188                {
 02189                    _logger.LogError(ex, "Error resolving path {Path}.", info.Path);
 02190                }
 2191            }
 2192            else
 2193            {
 02194                _logger.LogError("IntroProvider returned an IntroInfo with null Path and ItemId.");
 2195            }
 2196
 02197            return video;
 02198        }
 2199
 2200        /// <inheritdoc />
 2201        public IEnumerable<Guid> GetLocalAlternateVersionIds(Video video)
 2202        {
 02203            ArgumentNullException.ThrowIfNull(video);
 2204
 02205            var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.
 02206            if (linkedIds.Count > 0)
 2207            {
 02208                return linkedIds;
 2209            }
 2210
 02211            return [];
 2212        }
 2213
 2214        /// <inheritdoc />
 2215        public IEnumerable<Video> GetLinkedAlternateVersions(Video video)
 2216        {
 02217            ArgumentNullException.ThrowIfNull(video);
 2218
 02219            var linkedIds = _linkedChildrenService.GetLinkedChildrenIds(video.Id, (int)MediaBrowser.Controller.Entities.
 02220            if (linkedIds.Count > 0)
 2221            {
 02222                return linkedIds
 02223                    .Select(id => GetItemById(id))
 02224                    .Where(i => i is not null)
 02225                    .OfType<Video>()
 02226                    .OrderBy(i => i.SortName);
 2227            }
 2228
 02229            return [];
 2230        }
 2231
 2232        /// <inheritdoc />
 2233        public void UpsertLinkedChild(Guid parentId, Guid childId, MediaBrowser.Controller.Entities.LinkedChildType chil
 2234        {
 02235            _linkedChildrenService.UpsertLinkedChild(parentId, childId, childType);
 02236        }
 2237
 2238        /// <inheritdoc />
 2239        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortO
 2240        {
 12241            IOrderedEnumerable<BaseItem>? orderedItems = null;
 2242
 42243            foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null))
 2244            {
 12245                if (orderBy is RandomComparer)
 2246                {
 02247                    var randomItems = items.ToArray();
 02248                    Random.Shared.Shuffle(randomItems);
 02249                    items = randomItems;
 2250                    // Items are no longer ordered at this point, so set orderedItems back to null
 02251                    orderedItems = null;
 2252                }
 12253                else if (orderedItems is null)
 2254                {
 12255                    orderedItems = sortOrder == SortOrder.Descending
 12256                        ? items.OrderByDescending(i => i, orderBy)
 12257                        : items.OrderBy(i => i, orderBy);
 2258                }
 2259                else
 2260                {
 02261                    orderedItems = sortOrder == SortOrder.Descending
 02262                        ? orderedItems!.ThenByDescending(i => i, orderBy)
 02263                        : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration
 2264                }
 2265            }
 2266
 12267            return orderedItems ?? items;
 2268        }
 2269
 2270        /// <inheritdoc />
 2271        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, Sort
 2272        {
 02273            IOrderedEnumerable<BaseItem>? orderedItems = null;
 2274
 02275            foreach (var (name, sortOrder) in orderBy)
 2276            {
 02277                var comparer = GetComparer(name, user);
 02278                if (comparer is null)
 2279                {
 2280                    continue;
 2281                }
 2282
 02283                if (comparer is RandomComparer)
 2284                {
 02285                    var randomItems = items.ToArray();
 02286                    Random.Shared.Shuffle(randomItems);
 02287                    items = randomItems;
 2288                    // Items are no longer ordered at this point, so set orderedItems back to null
 02289                    orderedItems = null;
 2290                }
 02291                else if (orderedItems is null)
 2292                {
 02293                    orderedItems = sortOrder == SortOrder.Descending
 02294                        ? items.OrderByDescending(i => i, comparer)
 02295                        : items.OrderBy(i => i, comparer);
 2296                }
 2297                else
 2298                {
 02299                    orderedItems = sortOrder == SortOrder.Descending
 02300                        ? orderedItems!.ThenByDescending(i => i, comparer)
 02301                        : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration
 2302                }
 2303            }
 2304
 02305            return orderedItems ?? items;
 2306        }
 2307
 2308        /// <summary>
 2309        /// Gets the comparer.
 2310        /// </summary>
 2311        /// <param name="name">The name.</param>
 2312        /// <param name="user">The user.</param>
 2313        /// <returns>IBaseItemComparer.</returns>
 2314        private IBaseItemComparer? GetComparer(ItemSortBy name, User? user)
 2315        {
 12316            var comparer = Comparers.FirstOrDefault(c => name == c.Type);
 2317
 2318            // If it requires a user, create a new one, and assign the user
 12319            if (comparer is IUserBaseItemComparer)
 2320            {
 02321                var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null fo
 2322
 02323                userComparer.User = user;
 02324                userComparer.UserManager = _userManager;
 02325                userComparer.UserDataManager = _userDataManager;
 2326
 02327                return userComparer;
 2328            }
 2329
 12330            return comparer;
 2331        }
 2332
 2333        /// <inheritdoc />
 2334        public void CreateItem(BaseItem item, BaseItem? parent)
 2335        {
 02336            CreateItems([item], parent, CancellationToken.None);
 02337        }
 2338
 2339        /// <inheritdoc />
 2340        public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
 2341        {
 2342            // Resolve and add any local alternate version items that don't exist yet
 2343            // This ensures they exist in the database when LinkedChildren are processed
 22344            var allItems = new List<BaseItem>(items);
 22345            var parentFolder = parent as Folder;
 22346            var parentCollectionType = parent is not null ? GetTopFolderContentType(parent) : null;
 82347            foreach (var item in items)
 2348            {
 22349                if (item is Video video && video.LocalAlternateVersions.Length > 0)
 2350                {
 02351                    var videoType = video.GetType();
 02352                    foreach (var path in video.LocalAlternateVersions)
 2353                    {
 02354                        if (string.IsNullOrEmpty(path))
 2355                        {
 2356                            continue;
 2357                        }
 2358
 2359                        // Use the primary video's type for ID calculation to ensure consistency
 02360                        var altId = GetNewItemId(path, videoType);
 02361                        if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
 2362                        {
 2363                            // Alternate version doesn't exist, resolve and create it
 2364                            // ensuring it has the same type as the primary video
 02365                            var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
 02366                            if (altVideo is not null)
 2367                            {
 02368                                altVideo.OwnerId = video.Id;
 02369                                altVideo.SetPrimaryVersionId(video.Id);
 2370                                // ResolveAlternateVersion only sees the alternate's primary file.
 2371                                // If the alternate is itself a stack (e.g. 1080p part1 + part2),
 2372                                // detect its parts from sibling files so its AdditionalParts persist.
 02373                                SetAdditionalPartsFromStack(altVideo, path);
 02374                                allItems.Add(altVideo);
 2375                            }
 2376                        }
 2377                    }
 2378                }
 2379            }
 2380
 22381            _persistenceService.SaveItems(allItems, cancellationToken);
 2382
 82383            foreach (var item in allItems)
 2384            {
 22385                RegisterItem(item);
 2386            }
 2387
 22388            if (parent is Folder folder)
 2389            {
 22390                folder.Children = null;
 22391                folder.UserData = null;
 2392            }
 2393
 22394            if (ItemAdded is not null)
 2395            {
 82396                foreach (var item in items)
 2397                {
 2398                    // With the live tv guide this just creates too much noise
 22399                    if (item.SourceType != SourceType.Library)
 2400                    {
 2401                        continue;
 2402                    }
 2403
 2404                    try
 2405                    {
 22406                        ItemAdded(
 22407                            this,
 22408                            new ItemChangeEventArgs
 22409                            {
 22410                                Item = item,
 22411                                Parent = parent ?? item.GetParent()
 22412                            });
 22413                    }
 02414                    catch (Exception ex)
 2415                    {
 02416                        _logger.LogError(ex, "Error in ItemAdded event handler");
 02417                    }
 2418                }
 2419            }
 22420        }
 2421
 2422        private bool ImageNeedsRefresh(ItemImageInfo image)
 2423        {
 02424            if (image.Path is not null && image.IsLocalFile)
 2425            {
 02426                if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash))
 2427                {
 02428                    return true;
 2429                }
 2430
 2431                try
 2432                {
 02433                    return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeco
 2434                }
 02435                catch (Exception ex)
 2436                {
 02437                    _logger.LogError(ex, "Cannot get file info for {0}", image.Path);
 02438                    return false;
 2439                }
 2440            }
 2441
 02442            return image.Path is not null && !image.IsLocalFile;
 02443        }
 2444
 2445        /// <inheritdoc />
 2446        public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false)
 2447        {
 1482448            ArgumentNullException.ThrowIfNull(item);
 2449
 1482450            var outdated = forceUpdate
 1482451                ? item.ImageInfos.Where(i => i.Path is not null).ToArray()
 1482452                : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
 2453
 1482454            var parentItem = item.GetParent();
 1482455            var isLiveTvShow = item.SourceType != SourceType.Library &&
 1482456                               parentItem is not null &&
 1482457                               parentItem.SourceType != SourceType.Library; // not a channel
 2458
 2459            // Skip image processing if current or live tv show
 1482460            if (outdated.Length == 0 || isLiveTvShow)
 2461            {
 1482462                RegisterItem(item);
 1482463                return;
 2464            }
 2465
 02466            foreach (var img in outdated)
 2467            {
 02468                var image = img;
 02469                if (!img.IsLocalFile)
 2470                {
 2471                    try
 2472                    {
 02473                        var index = item.GetImageIndex(img);
 02474                        image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
 02475                    }
 02476                    catch (ArgumentException)
 2477                    {
 02478                        _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
 02479                        continue;
 2480                    }
 02481                    catch (Exception ex) when (ex is InvalidOperationException or IOException)
 2482                    {
 02483                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
 02484                        continue;
 2485                    }
 02486                    catch (HttpRequestException ex)
 2487                    {
 02488                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", im
 02489                        continue;
 2490                    }
 2491                }
 2492
 02493                if (!File.Exists(image.Path))
 2494                {
 02495                    _logger.LogWarning("Image not found at {ImagePath}", image.Path);
 02496                    continue;
 2497                }
 2498
 2499                ImageDimensions size;
 2500                try
 2501                {
 02502                    size = _imageProcessor.GetImageDimensions(item, image);
 02503                    image.Width = size.Width;
 02504                    image.Height = size.Height;
 02505                }
 02506                catch (Exception ex)
 2507                {
 02508                    _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
 02509                    size = default;
 02510                    image.Width = 0;
 02511                    image.Height = 0;
 02512                }
 2513
 2514                try
 2515                {
 02516                    var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size);
 02517                    image.BlurHash = blurhash;
 02518                }
 02519                catch (Exception ex)
 2520                {
 02521                    _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
 02522                    image.BlurHash = string.Empty;
 02523                }
 2524
 2525                try
 2526                {
 02527                    var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path);
 02528                    image.DateModified = modifiedDate;
 02529                }
 02530                catch (Exception ex)
 2531                {
 02532                    _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
 02533                }
 02534            }
 2535
 02536            item.ValidateImages();
 2537
 02538            await _persistenceService.SaveImagesAsync(item).ConfigureAwait(false);
 2539
 02540            RegisterItem(item);
 1482541        }
 2542
 2543        /// <inheritdoc />
 2544        public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, 
 2545        {
 4362546            foreach (var item in items)
 2547            {
 1092548                item.DateLastSaved = DateTime.UtcNow;
 1092549                await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
 2550
 2551                // Modify again, so saved value is after write time of externally saved metadata
 1092552                item.DateLastSaved = DateTime.UtcNow;
 1092553            }
 2554
 2555            // Resolve and add any local alternate version items that don't exist yet
 2556            // This ensures they exist in the database when LinkedChildren are processed
 1092557            var allItems = new List<BaseItem>(items);
 1092558            var parentFolder = parent as Folder;
 1092559            var parentCollectionType = GetTopFolderContentType(parent);
 4362560            foreach (var item in items)
 2561            {
 1092562                if (item is Video video && video.LocalAlternateVersions.Length > 0)
 2563                {
 02564                    var videoType = video.GetType();
 02565                    foreach (var path in video.LocalAlternateVersions)
 2566                    {
 02567                        if (string.IsNullOrEmpty(path))
 2568                        {
 2569                            continue;
 2570                        }
 2571
 2572                        // Use the primary video's type for ID calculation to ensure consistency
 02573                        var altId = GetNewItemId(path, videoType);
 02574                        if (GetItemById(altId) is null && !allItems.Any(i => i.Id.Equals(altId)))
 2575                        {
 2576                            // Alternate version doesn't exist, resolve and create it
 2577                            // ensuring it has the same type as the primary video
 02578                            var altVideo = ResolveAlternateVersion(path, videoType, parentFolder, parentCollectionType);
 02579                            if (altVideo is not null)
 2580                            {
 02581                                altVideo.OwnerId = video.Id;
 02582                                altVideo.SetPrimaryVersionId(video.Id);
 2583                                // ResolveAlternateVersion only sees the alternate's primary file.
 2584                                // If the alternate is itself a stack (e.g. 1080p part1 + part2),
 2585                                // detect its parts from sibling files so its AdditionalParts persist.
 02586                                SetAdditionalPartsFromStack(altVideo, path);
 02587                                allItems.Add(altVideo);
 2588                            }
 2589                        }
 2590                    }
 2591                }
 2592            }
 2593
 1092594            _persistenceService.SaveItems(allItems, cancellationToken);
 2595
 4322596            foreach (var item in allItems)
 2597            {
 1082598                if (!items.Contains(item))
 2599                {
 02600                    RegisterItem(item);
 2601                }
 2602            }
 2603
 1082604            if (parent is Folder folder)
 2605            {
 252606                folder.Children = null;
 252607                folder.UserData = null;
 2608            }
 2609
 1082610            if (ItemUpdated is not null)
 2611            {
 4282612                foreach (var item in items)
 2613                {
 2614                    // With the live tv guide this just creates too much noise
 1072615                    if (item.SourceType != SourceType.Library)
 2616                    {
 2617                        continue;
 2618                    }
 2619
 2620                    try
 2621                    {
 1072622                        ItemUpdated(
 1072623                            this,
 1072624                            new ItemChangeEventArgs
 1072625                            {
 1072626                                Item = item,
 1072627                                Parent = parent,
 1072628                                UpdateReason = updateReason
 1072629                            });
 1072630                    }
 02631                    catch (Exception ex)
 2632                    {
 02633                        _logger.LogError(ex, "Error in ItemUpdated event handler");
 02634                    }
 2635                }
 2636            }
 1082637        }
 2638
 2639        /// <inheritdoc />
 2640        public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cance
 1092641            => UpdateItemsAsync([item], parent, updateReason, cancellationToken);
 2642
 2643        /// <inheritdoc />
 2644        public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
 2645        {
 322646            await _persistenceService.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
 322647        }
 2648
 2649        public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
 2650        {
 1092651            if (item.IsFileProtocol)
 2652            {
 1092653                await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
 2654            }
 2655
 1092656            await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
 1092657        }
 2658
 2659        /// <summary>
 2660        /// Reports the item removed.
 2661        /// </summary>
 2662        /// <param name="item">The item.</param>
 2663        /// <param name="parent">The parent item.</param>
 2664        public void ReportItemRemoved(BaseItem item, BaseItem parent)
 2665        {
 02666            if (ItemRemoved is not null)
 2667            {
 2668                try
 2669                {
 02670                    ItemRemoved(
 02671                        this,
 02672                        new ItemChangeEventArgs
 02673                        {
 02674                            Item = item,
 02675                            Parent = parent
 02676                        });
 02677                }
 02678                catch (Exception ex)
 2679                {
 02680                    _logger.LogError(ex, "Error in ItemRemoved event handler");
 02681                }
 2682            }
 02683        }
 2684
 2685        /// <summary>
 2686        /// Retrieves the item.
 2687        /// </summary>
 2688        /// <param name="id">The id.</param>
 2689        /// <returns>BaseItem.</returns>
 2690        public BaseItem RetrieveItem(Guid id)
 2691        {
 1042692            return _itemRepository.RetrieveItem(id);
 2693        }
 2694
 2695        public List<Folder> GetCollectionFolders(BaseItem item)
 2696        {
 7002697            return GetCollectionFolders(item, GetUserRootFolder().Children.OfType<Folder>());
 2698        }
 2699
 2700        public List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2701        {
 7162702            while (item is not null)
 2703            {
 7162704                var parent = item.GetParent();
 2705
 7162706                if (parent is AggregateFolder)
 2707                {
 2708                    break;
 2709                }
 2710
 6362711                if (parent is null)
 2712                {
 6202713                    var owner = item.GetOwner();
 2714
 6202715                    if (owner is null)
 2716                    {
 2717                        break;
 2718                    }
 2719
 02720                    item = owner;
 2721                }
 2722                else
 2723                {
 162724                    item = parent;
 2725                }
 2726            }
 2727
 7002728            if (item is null)
 2729            {
 02730                return [];
 2731            }
 2732
 7002733            return GetCollectionFoldersInternal(item, allUserRootChildren);
 2734        }
 2735
 2736        private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2737        {
 7002738            return allUserRootChildren
 7002739                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 7002740                .ToList();
 2741        }
 2742
 2743        public LibraryOptions GetLibraryOptions(BaseItem item)
 2744        {
 4492745            if (item is CollectionFolder collectionFolder)
 2746            {
 372747                return collectionFolder.GetLibraryOptions();
 2748            }
 2749
 2750            // List.Find is more performant than FirstOrDefault due to enumerator allocation
 4122751            return GetCollectionFolders(item)
 4122752                .Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2
 4122753                ? collectionFolder2.GetLibraryOptions()
 4122754                : new LibraryOptions();
 2755        }
 2756
 2757        public CollectionType? GetContentType(BaseItem item)
 2758        {
 582759            var configuredContentType = GetConfiguredContentType(item, false);
 582760            if (configuredContentType is not null)
 2761            {
 02762                return configuredContentType;
 2763            }
 2764
 582765            configuredContentType = GetConfiguredContentType(item, true);
 582766            if (configuredContentType is not null)
 2767            {
 02768                return configuredContentType;
 2769            }
 2770
 582771            return GetInheritedContentType(item);
 2772        }
 2773
 2774        public CollectionType? GetInheritedContentType(BaseItem item)
 2775        {
 582776            var type = GetTopFolderContentType(item);
 2777
 582778            if (type is not null)
 2779            {
 02780                return type;
 2781            }
 2782
 582783            return item.GetParents()
 582784                .Select(GetConfiguredContentType)
 582785                .LastOrDefault(i => i is not null);
 2786        }
 2787
 2788        public CollectionType? GetConfiguredContentType(BaseItem item)
 2789        {
 02790            return GetConfiguredContentType(item, false);
 2791        }
 2792
 2793        public CollectionType? GetConfiguredContentType(string path)
 2794        {
 02795            return GetContentTypeOverride(path, false);
 2796        }
 2797
 2798        public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
 2799        {
 1162800            if (item is ICollectionFolder collectionFolder)
 2801            {
 02802                return collectionFolder.CollectionType;
 2803            }
 2804
 1162805            return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
 2806        }
 2807
 2808        private CollectionType? GetContentTypeOverride(string path, bool inherit)
 2809        {
 1332810            var nameValuePair = _configurationManager.Configuration.ContentTypes
 1332811                                    .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
 1332812                                                         || (inherit && !string.IsNullOrEmpty(i.Name)
 1332813                                                                     && _fileSystem.ContainsSubPath(i.Name, path)));
 1332814            if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType))
 2815            {
 02816                return collectionType;
 2817            }
 2818
 1332819            return null;
 2820        }
 2821
 2822        private CollectionType? GetTopFolderContentType(BaseItem item)
 2823        {
 1692824            if (item is null)
 2825            {
 842826                return null;
 2827            }
 2828
 852829            while (!item.ParentId.IsEmpty())
 2830            {
 02831                var parent = item.GetParent();
 02832                if (parent is null || parent is AggregateFolder)
 2833                {
 2834                    break;
 2835                }
 2836
 02837                item = parent;
 2838            }
 2839
 852840            return GetUserRootFolder().Children
 852841                .OfType<ICollectionFolder>()
 852842                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 852843                .Select(i => i.CollectionType)
 852844                .FirstOrDefault(i => i is not null);
 2845        }
 2846
 2847        public UserView GetNamedView(
 2848            User user,
 2849            string name,
 2850            CollectionType? viewType,
 2851            string sortName)
 2852        {
 02853            return GetNamedView(user, name, Guid.Empty, viewType, sortName);
 2854        }
 2855
 2856        public UserView GetNamedView(
 2857            string name,
 2858            CollectionType viewType,
 2859            string sortName)
 2860        {
 02861            var path = Path.Combine(
 02862                _configurationManager.ApplicationPaths.InternalMetadataPath,
 02863                "views",
 02864                _fileSystem.GetValidFilename(viewType.ToString()));
 2865
 02866            var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
 2867
 02868            var item = GetItemById(id) as UserView;
 2869
 02870            var refresh = false;
 2871
 02872            if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
 2873            {
 02874                var info = Directory.CreateDirectory(path);
 02875                item = new UserView
 02876                {
 02877                    Path = path,
 02878                    Id = id,
 02879                    DateCreated = info.CreationTimeUtc,
 02880                    DateModified = info.LastWriteTimeUtc,
 02881                    Name = name,
 02882                    ViewType = viewType,
 02883                    ForcedSortName = sortName
 02884                };
 2885
 02886                CreateItem(item, null);
 2887
 02888                refresh = true;
 2889            }
 2890
 02891            if (refresh)
 2892            {
 02893                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResu
 02894                ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), Ref
 2895            }
 2896
 02897            return item;
 2898        }
 2899
 2900        public UserView GetNamedView(
 2901            User user,
 2902            string name,
 2903            Guid parentId,
 2904            CollectionType? viewType,
 2905            string sortName)
 2906        {
 02907            var parentIdString = parentId.IsEmpty()
 02908                ? null
 02909                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02910            var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdStrin
 2911
 02912            var id = GetNewItemId(idValues, typeof(UserView));
 2913
 02914            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2915
 02916            var item = GetItemById(id) as UserView;
 2917
 02918            var isNew = false;
 2919
 02920            if (item is null)
 2921            {
 02922                var info = Directory.CreateDirectory(path);
 02923                item = new UserView
 02924                {
 02925                    Path = path,
 02926                    Id = id,
 02927                    DateCreated = info.CreationTimeUtc,
 02928                    DateModified = info.LastWriteTimeUtc,
 02929                    Name = name,
 02930                    ViewType = viewType,
 02931                    ForcedSortName = sortName,
 02932                    UserId = user.Id,
 02933                    DisplayParentId = parentId
 02934                };
 2935
 02936                CreateItem(item, null);
 2937
 02938                isNew = true;
 2939            }
 2940
 02941            var lastRefreshedUtc = item.DateLastRefreshed;
 02942            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2943
 02944            if (!refresh && !item.DisplayParentId.IsEmpty())
 2945            {
 02946                var displayParent = GetItemById(item.DisplayParentId);
 02947                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 2948            }
 2949
 02950            if (refresh)
 2951            {
 02952                ProviderManager.QueueRefresh(
 02953                    item.Id,
 02954                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02955                    {
 02956                        // Need to force save to increment DateLastSaved
 02957                        ForceSave = true
 02958                    },
 02959                    RefreshPriority.Normal);
 2960            }
 2961
 02962            return item;
 2963        }
 2964
 2965        public UserView GetShadowView(
 2966            BaseItem parent,
 2967            CollectionType? viewType,
 2968            string sortName)
 2969        {
 02970            ArgumentNullException.ThrowIfNull(parent);
 2971
 02972            var name = parent.Name;
 02973            var parentId = parent.Id;
 2974
 02975            var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
 2976
 02977            var id = GetNewItemId(idValues, typeof(UserView));
 2978
 02979            var path = parent.Path;
 2980
 02981            var item = GetItemById(id) as UserView;
 2982
 02983            var isNew = false;
 2984
 02985            if (item is null)
 2986            {
 02987                var info = Directory.CreateDirectory(path);
 02988                item = new UserView
 02989                {
 02990                    Path = path,
 02991                    Id = id,
 02992                    DateCreated = info.CreationTimeUtc,
 02993                    DateModified = info.LastWriteTimeUtc,
 02994                    Name = name,
 02995                    ViewType = viewType,
 02996                    ForcedSortName = sortName,
 02997                    DisplayParentId = parentId
 02998                };
 2999
 03000                CreateItem(item, null);
 3001
 03002                isNew = true;
 3003            }
 3004
 03005            var lastRefreshedUtc = item.DateLastRefreshed;
 03006            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 3007
 03008            if (!refresh && !item.DisplayParentId.IsEmpty())
 3009            {
 03010                var displayParent = GetItemById(item.DisplayParentId);
 03011                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 3012            }
 3013
 03014            if (refresh)
 3015            {
 03016                ProviderManager.QueueRefresh(
 03017                    item.Id,
 03018                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 03019                    {
 03020                        // Need to force save to increment DateLastSaved
 03021                        ForceSave = true
 03022                    },
 03023                    RefreshPriority.Normal);
 3024            }
 3025
 03026            return item;
 3027        }
 3028
 3029        public UserView GetNamedView(
 3030            string name,
 3031            Guid parentId,
 3032            CollectionType? viewType,
 3033            string sortName,
 3034            string uniqueId)
 3035        {
 03036            ArgumentException.ThrowIfNullOrEmpty(name);
 3037
 03038            var parentIdString = parentId.IsEmpty()
 03039                ? null
 03040                : parentId.ToString("N", CultureInfo.InvariantCulture);
 03041            var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.E
 03042            if (!string.IsNullOrEmpty(uniqueId))
 3043            {
 03044                idValues += uniqueId;
 3045            }
 3046
 03047            var id = GetNewItemId(idValues, typeof(UserView));
 3048
 03049            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 3050
 03051            var item = GetItemById(id) as UserView;
 3052
 03053            var isNew = false;
 3054
 03055            if (item is null)
 3056            {
 03057                var info = Directory.CreateDirectory(path);
 03058                item = new UserView
 03059                {
 03060                    Path = path,
 03061                    Id = id,
 03062                    DateCreated = info.CreationTimeUtc,
 03063                    DateModified = info.LastWriteTimeUtc,
 03064                    Name = name,
 03065                    ViewType = viewType,
 03066                    ForcedSortName = sortName,
 03067                    DisplayParentId = parentId
 03068                };
 3069
 03070                CreateItem(item, null);
 3071
 03072                isNew = true;
 3073            }
 3074
 03075            if (viewType != item.ViewType)
 3076            {
 03077                item.ViewType = viewType;
 03078                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult
 3079            }
 3080
 03081            var lastRefreshedUtc = item.DateLastRefreshed;
 03082            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 3083
 03084            if (!refresh && !item.DisplayParentId.IsEmpty())
 3085            {
 03086                var displayParent = GetItemById(item.DisplayParentId);
 03087                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 3088            }
 3089
 03090            if (refresh)
 3091            {
 03092                ProviderManager.QueueRefresh(
 03093                    item.Id,
 03094                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 03095                    {
 03096                        // Need to force save to increment DateLastSaved
 03097                        ForceSave = true
 03098                    },
 03099                    RefreshPriority.Normal);
 3100            }
 3101
 03102            return item;
 3103        }
 3104
 3105        public BaseItem GetParentItem(Guid? parentId, Guid? userId)
 3106        {
 33107            if (parentId.HasValue)
 3108            {
 03109                return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}"
 3110            }
 3111
 33112            if (!userId.IsNullOrEmpty())
 3113            {
 33114                return GetUserRootFolder();
 3115            }
 3116
 03117            return RootFolder;
 3118        }
 3119
 3120        /// <inheritdoc />
 3121        public void QueueLibraryScan()
 3122        {
 03123            _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
 03124        }
 3125
 3126        /// <inheritdoc />
 3127        public int? GetSeasonNumberFromPath(string path, Guid? parentId)
 3128        {
 03129            var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
 03130            return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
 3131        }
 3132
 3133        /// <inheritdoc />
 3134        public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
 3135        {
 03136            var series = episode.Series;
 03137            bool? isAbsoluteNaming = series is not null && string.Equals(series.DisplayOrder, "absolute", StringComparis
 03138            if (!isAbsoluteNaming.Value)
 3139            {
 3140                // In other words, no filter applied
 03141                isAbsoluteNaming = null;
 3142            }
 3143
 03144            var resolver = new EpisodeResolver(_namingOptions);
 3145
 03146            var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 3147
 03148            EpisodeInfo? episodeInfo = null;
 03149            if (episode.IsFileProtocol)
 3150            {
 03151                episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
 3152                // Resolve from parent folder if it's not the Season folder
 03153                var parent = episode.GetParent();
 03154                if (episodeInfo is null && parent.GetType() == typeof(Folder))
 3155                {
 03156                    episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming);
 03157                    if (episodeInfo is not null)
 3158                    {
 3159                        // add the container
 03160                        episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
 3161                    }
 3162                }
 3163            }
 3164
 03165            var changed = false;
 03166            if (episodeInfo is null)
 3167            {
 03168                return changed;
 3169            }
 3170
 03171            if (episodeInfo.IsByDate)
 3172            {
 03173                if (episode.IndexNumber.HasValue)
 3174                {
 03175                    episode.IndexNumber = null;
 03176                    changed = true;
 3177                }
 3178
 03179                if (episode.IndexNumberEnd.HasValue)
 3180                {
 03181                    episode.IndexNumberEnd = null;
 03182                    changed = true;
 3183                }
 3184
 03185                if (!episode.PremiereDate.HasValue)
 3186                {
 03187                    if (episodeInfo.Year.HasValue && episodeInfo.Month.HasValue && episodeInfo.Day.HasValue)
 3188                    {
 03189                        episode.PremiereDate = new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo
 3190                    }
 3191
 03192                    if (episode.PremiereDate.HasValue)
 3193                    {
 03194                        changed = true;
 3195                    }
 3196                }
 3197
 03198                if (!episode.ProductionYear.HasValue)
 3199                {
 03200                    episode.ProductionYear = episodeInfo.Year;
 3201
 03202                    if (episode.ProductionYear.HasValue)
 3203                    {
 03204                        changed = true;
 3205                    }
 3206                }
 3207            }
 3208            else
 3209            {
 03210                if (!episode.IndexNumber.HasValue || forceRefresh)
 3211                {
 03212                    if (episode.IndexNumber != episodeInfo.EpisodeNumber)
 3213                    {
 03214                        changed = true;
 3215                    }
 3216
 03217                    episode.IndexNumber = episodeInfo.EpisodeNumber;
 3218                }
 3219
 03220                if (!episode.IndexNumberEnd.HasValue || forceRefresh)
 3221                {
 03222                    if (episode.IndexNumberEnd != episodeInfo.EndingEpisodeNumber)
 3223                    {
 03224                        changed = true;
 3225                    }
 3226
 03227                    episode.IndexNumberEnd = episodeInfo.EndingEpisodeNumber;
 3228                }
 3229
 03230                if (!episode.ParentIndexNumber.HasValue || forceRefresh)
 3231                {
 03232                    if (episode.ParentIndexNumber != episodeInfo.SeasonNumber)
 3233                    {
 03234                        changed = true;
 3235                    }
 3236
 03237                    episode.ParentIndexNumber = episodeInfo.SeasonNumber;
 3238                }
 3239            }
 3240
 03241            if (!episode.ParentIndexNumber.HasValue)
 3242            {
 03243                var season = episode.Season;
 3244
 03245                if (season is not null)
 3246                {
 03247                    episode.ParentIndexNumber = season.IndexNumber;
 3248                }
 3249
 03250                if (episode.ParentIndexNumber.HasValue)
 3251                {
 03252                    changed = true;
 3253                }
 3254            }
 3255
 03256            return changed;
 3257        }
 3258
 3259        public ItemLookupInfo ParseName(string name)
 3260        {
 03261            var namingOptions = _namingOptions;
 03262            var result = VideoResolver.CleanDateTime(name, namingOptions);
 3263
 03264            return new ItemLookupInfo
 03265            {
 03266                Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName : result.Name
 03267                Year = result.Year
 03268            };
 3269        }
 3270
 3271        public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, ID
 3272        {
 3273            // Apply .ignore rules
 73274            var filtered = fileSystemChildren.Where(c => !_dotIgnoreIgnoreRule.ShouldIgnore(c, owner)).ToList();
 73275            var isFolder = owner.IsFolder || (owner is Video video && (video.VideoType == VideoType.BluRay || video.Vide
 73276            var ownerVideoInfo = VideoResolver.Resolve(owner.Path, isFolder, _namingOptions, libraryRoot: owner.Containi
 73277            if (ownerVideoInfo is null)
 3278            {
 03279                yield break;
 3280            }
 3281
 73282            var count = filtered.Count;
 823283            for (var i = 0; i < count; i++)
 3284            {
 343285                var current = filtered[i];
 343286                if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
 3287                {
 53288                    var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
 53289                    var filesInSubFolderList = filesInSubFolder.ToList();
 3290
 53291                    bool subFolderIsMixedFolder = filesInSubFolderList.Count > 1;
 3292
 203293                    foreach (var file in filesInSubFolderList)
 3294                    {
 53295                        if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType))
 3296                        {
 3297                            continue;
 3298                        }
 3299
 43300                        var extra = GetExtra(file, extraType.Value, subFolderIsMixedFolder);
 43301                        if (extra is not null)
 3302                        {
 43303                            yield return extra;
 3304                        }
 3305                    }
 3306                }
 293307                else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo
 3308                {
 133309                    var extra = GetExtra(current, extraType.Value, false);
 133310                    if (extra is not null)
 3311                    {
 133312                        yield return extra;
 3313                    }
 3314                }
 3315            }
 3316
 3317            BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType, bool isInMixedFolder)
 3318            {
 3319                var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetReso
 3320                if (extra is not Video && extra is not Audio)
 3321                {
 3322                    return null;
 3323                }
 3324
 3325                // Try to retrieve it from the db. If we don't find it, use the resolved version
 3326                var itemById = GetItemById(extra.Id);
 3327                if (itemById is not null)
 3328                {
 3329                    extra = itemById;
 3330                }
 3331
 3332                // Only update extra type if it is more specific then the currently known extra type
 3333                if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown)
 3334                {
 3335                    extra.ExtraType = extraType;
 3336                }
 3337
 3338                // Only return items that are actual extras (have ExtraType set)
 3339                // Note: OwnerId and ParentId are set by RefreshExtras, not here,
 3340                // so that RefreshExtras can detect when they need updating and set ForceSave.
 3341                if (extra.ExtraType is not null)
 3342                {
 3343                    extra.IsInMixedFolder = isInMixedFolder;
 3344                    return extra;
 3345                }
 3346
 3347                return null;
 3348            }
 73349        }
 3350
 3351        public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
 3352        {
 123353            foreach (var map in _configurationManager.Configuration.PathSubstitutions)
 3354            {
 03355                if (path.TryReplaceSubPath(map.From, map.To, out var newPath))
 3356                {
 03357                    return newPath;
 3358                }
 3359            }
 3360
 63361            return path;
 3362        }
 3363
 3364        public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
 3365        {
 03366            return _peopleRepository.GetPeople(query).Items;
 3367        }
 3368
 3369        public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
 3370        {
 63371            if (item.SupportsPeople)
 3372            {
 03373                var people = GetPeople(new InternalPeopleQuery
 03374                {
 03375                    ItemId = item.Id
 03376                });
 3377
 03378                if (people.Count > 0)
 3379                {
 03380                    return people;
 3381                }
 3382            }
 3383
 63384            return [];
 3385        }
 3386
 3387        public QueryResult<BaseItem> GetPeopleItems(InternalPeopleQuery query)
 3388        {
 03389            var queryResult = _peopleRepository.GetPeople(query);
 03390            var baseItems = queryResult.Items.Select(i =>
 03391                {
 03392                    try
 03393                    {
 03394                        return GetPerson(i.Name);
 03395                    }
 03396                    catch (Exception ex)
 03397                    {
 03398                        _logger.LogError(ex, "error retrieving BaseItem for person: {0}", i.Name);
 03399                        return null;
 03400                    }
 03401                })
 03402                .Where(i => i is not null)
 03403                .Where(i => query.User is null || i!.IsVisible(query.User))
 03404                .OfType<BaseItem>()
 03405                .ToList()
 03406                .AsReadOnly();
 3407
 03408            return new QueryResult<BaseItem>
 03409            {
 03410                StartIndex = queryResult.StartIndex,
 03411                TotalRecordCount = queryResult.TotalRecordCount,
 03412                Items = baseItems,
 03413            };
 3414        }
 3415
 3416        public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
 3417        {
 03418            return _peopleRepository.GetPeopleNames(query);
 3419        }
 3420
 3421        /// <inheritdoc/>
 3422        public IReadOnlyDictionary<Guid, IReadOnlyList<string>> GetPeopleNamesByItems(IReadOnlyList<Guid> itemIds, IRead
 3423        {
 03424            return _peopleRepository.GetPeopleNamesByItems(itemIds, personTypes);
 3425        }
 3426
 3427        public void UpdatePeople(BaseItem item, List<PersonInfo> people)
 3428        {
 03429            UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
 03430        }
 3431
 3432        /// <inheritdoc />
 3433        public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellat
 3434        {
 03435            if (!item.SupportsPeople)
 3436            {
 03437                return;
 3438            }
 3439
 03440            if (people is not null)
 3441            {
 03442                people = people.Where(e => e is not null).ToArray();
 03443                _peopleRepository.UpdatePeople(item.Id, people);
 03444                await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
 3445            }
 03446        }
 3447
 3448        public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool re
 3449        {
 03450            foreach (var url in image.Path.Split('|'))
 3451            {
 3452                try
 3453                {
 03454                    _logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
 3455
 03456                    await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).Configure
 3457
 03458                    await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwai
 3459
 03460                    return item.GetImageInfo(image.Type, imageIndex);
 3461                }
 03462                catch (HttpRequestException ex)
 3463                {
 03464                    if (ex.StatusCode.HasValue
 03465                        && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forb
 3466                    {
 03467                        _logger.LogDebug(ex, "Error downloading image {Url}", url);
 03468                        continue;
 3469                    }
 3470
 03471                    throw;
 3472                }
 3473            }
 3474
 03475            if (removeOnFailure)
 3476            {
 3477                // Remove this image to prevent it from retrying over and over
 03478                item.RemoveImage(image);
 03479                await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(fa
 3480            }
 3481
 03482            throw new InvalidOperationException("Unable to convert any images to local");
 03483        }
 3484
 3485        public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, b
 3486        {
 23487            if (string.IsNullOrWhiteSpace(name))
 3488            {
 03489                throw new ArgumentNullException(nameof(name));
 3490            }
 3491
 23492            name = _fileSystem.GetValidFilename(name.Trim());
 3493
 23494            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3495
 23496            var existingNameCount = 1; // first numbered name will be 2
 23497            var virtualFolderPath = Path.Combine(rootFolderPath, name);
 23498            var originalName = name;
 33499            while (Directory.Exists(virtualFolderPath))
 3500            {
 13501                existingNameCount++;
 13502                name = originalName + existingNameCount;
 13503                virtualFolderPath = Path.Combine(rootFolderPath, name);
 3504            }
 3505
 23506            var mediaPathInfos = options.PathInfos;
 23507            if (mediaPathInfos is not null)
 3508            {
 23509                var invalidpath = mediaPathInfos.FirstOrDefault(i => !Directory.Exists(i.Path));
 23510                if (invalidpath is not null)
 3511                {
 03512                    throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + ".");
 3513                }
 3514            }
 3515
 23516            LibraryMonitor.Stop();
 3517
 3518            try
 3519            {
 23520                Directory.CreateDirectory(virtualFolderPath);
 3521
 23522                if (collectionType is not null)
 3523                {
 03524                    var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collect
 3525
 03526                    FileHelper.CreateEmpty(path);
 3527                }
 3528
 23529                CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
 3530
 23531                if (mediaPathInfos is not null)
 3532                {
 43533                    foreach (var path in mediaPathInfos)
 3534                    {
 03535                        AddMediaPathInternal(name, path, false);
 3536                    }
 3537                }
 3538            }
 3539            finally
 3540            {
 23541                await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
 3542
 23543                if (refreshLibrary)
 3544                {
 23545                    StartScanInBackground();
 3546                }
 3547                else
 3548                {
 3549                    // Need to add a delay here or directory watchers may still pick up the changes
 03550                    await Task.Delay(1000).ConfigureAwait(false);
 03551                    LibraryMonitor.Start();
 3552                }
 3553            }
 23554        }
 3555
 3556        private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
 3557        {
 03558            foreach (var person in people)
 3559            {
 03560                cancellationToken.ThrowIfCancellationRequested();
 3561
 03562                var itemUpdateType = ItemUpdateType.MetadataDownload;
 03563                var saveEntity = false;
 03564                var createEntity = false;
 03565                var personEntity = GetPerson(person.Name);
 3566
 03567                if (personEntity is null)
 3568                {
 3569                    try
 3570                    {
 03571                        var path = Person.GetPath(person.Name);
 03572                        var info = Directory.CreateDirectory(path);
 03573                        personEntity = new Person()
 03574                        {
 03575                            Name = person.Name,
 03576                            Id = GetItemByNameId<Person>(path),
 03577                            DateCreated = info.CreationTimeUtc,
 03578                            DateModified = info.LastWriteTimeUtc,
 03579                            Path = path
 03580                        };
 3581
 03582                        personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
 03583                        saveEntity = true;
 03584                        createEntity = true;
 03585                    }
 03586                    catch (Exception ex)
 3587                    {
 03588                        _logger.LogWarning(ex, "Failed to create person {Name}", person.Name);
 03589                        continue;
 3590                    }
 3591                }
 3592
 03593                foreach (var id in person.ProviderIds)
 3594                {
 03595                    if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)
 3596                    {
 03597                        personEntity.SetProviderId(id.Key, id.Value);
 03598                        saveEntity = true;
 3599                    }
 3600                }
 3601
 03602                if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
 3603                {
 03604                    personEntity.SetImage(
 03605                        new ItemImageInfo
 03606                        {
 03607                            Path = person.ImageUrl,
 03608                            Type = ImageType.Primary
 03609                        },
 03610                        0);
 3611
 03612                    saveEntity = true;
 03613                    itemUpdateType = ItemUpdateType.ImageUpdate;
 3614                }
 3615
 03616                if (saveEntity)
 3617                {
 03618                    if (createEntity)
 3619                    {
 03620                        CreateItems([personEntity], null, CancellationToken.None);
 3621                    }
 3622
 03623                    await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
 03624                    personEntity.DateLastSaved = DateTime.UtcNow;
 3625
 03626                    CreateItems([personEntity], null, CancellationToken.None);
 3627                }
 03628            }
 03629        }
 3630
 3631        private void StartScanInBackground()
 3632        {
 33633            Task.Run(() =>
 33634            {
 33635                // No need to start if scanning the library because it will handle it
 33636                ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
 33637            });
 33638        }
 3639
 3640        public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3641        {
 13642            AddMediaPathInternal(virtualFolderName, mediaPath, true);
 03643        }
 3644
 3645        private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
 3646        {
 13647            ArgumentNullException.ThrowIfNull(pathInfo);
 3648
 13649            var path = pathInfo.Path;
 3650
 13651            if (string.IsNullOrWhiteSpace(path))
 3652            {
 03653                throw new ArgumentException(nameof(path));
 3654            }
 3655
 13656            if (!Directory.Exists(path))
 3657            {
 13658                throw new FileNotFoundException("The path does not exist.");
 3659            }
 3660
 03661            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03662            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3663
 03664            CreateShortcut(virtualFolderPath, pathInfo);
 3665
 03666            if (saveLibraryOptions)
 3667            {
 03668                var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3669
 03670                libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
 3671
 03672                SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3673
 03674                CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 3675            }
 03676        }
 3677
 3678        public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3679        {
 03680            ArgumentNullException.ThrowIfNull(mediaPath);
 3681
 03682            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03683            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3684
 03685            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3686
 03687            SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3688
 03689            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03690        }
 3691
 3692        private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options)
 3693        {
 03694            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 03695            var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders, null);
 3696
 03697            if (info.Locations.Length > 0 && info.Locations.Length != options.PathInfos.Length)
 3698            {
 03699                var list = options.PathInfos.ToList();
 3700
 03701                foreach (var location in info.Locations)
 3702                {
 03703                    if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
 3704                    {
 03705                        list.Add(new MediaPathInfo(location));
 3706                    }
 3707                }
 3708
 03709                options.PathInfos = list.ToArray();
 3710            }
 03711        }
 3712
 3713        public async Task RemoveVirtualFolder(string name, bool refreshLibrary)
 3714        {
 23715            if (string.IsNullOrWhiteSpace(name))
 3716            {
 03717                throw new ArgumentNullException(nameof(name));
 3718            }
 3719
 23720            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3721
 23722            var path = Path.Combine(rootFolderPath, name);
 3723
 23724            if (!Directory.Exists(path))
 3725            {
 13726                throw new FileNotFoundException("The media folder does not exist");
 3727            }
 3728
 13729            LibraryMonitor.Stop();
 3730
 3731            try
 3732            {
 13733                Directory.Delete(path, true);
 3734            }
 3735            finally
 3736            {
 13737                CollectionFolder.OnCollectionFolderChange();
 3738
 13739                if (refreshLibrary)
 3740                {
 13741                    await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
 3742
 13743                    StartScanInBackground();
 3744                }
 3745                else
 3746                {
 3747                    // Need to add a delay here or directory watchers may still pick up the changes
 03748                    await Task.Delay(1000).ConfigureAwait(false);
 03749                    LibraryMonitor.Start();
 3750                }
 3751            }
 13752        }
 3753
 3754        private void RemoveContentTypeOverrides(string path)
 3755        {
 03756            if (string.IsNullOrWhiteSpace(path))
 3757            {
 03758                throw new ArgumentNullException(nameof(path));
 3759            }
 3760
 03761            List<NameValuePair>? removeList = null;
 3762
 03763            foreach (var contentType in _configurationManager.Configuration.ContentTypes)
 3764            {
 03765                if (string.IsNullOrWhiteSpace(contentType.Name)
 03766                    || _fileSystem.AreEqual(path, contentType.Name)
 03767                    || _fileSystem.ContainsSubPath(path, contentType.Name))
 3768                {
 03769                    (removeList ??= new()).Add(contentType);
 3770                }
 3771            }
 3772
 03773            if (removeList is not null)
 3774            {
 03775                _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
 03776                    .Except(removeList)
 03777                    .ToArray();
 3778
 03779                _configurationManager.SaveConfiguration();
 3780            }
 03781        }
 3782
 3783        public void RemoveMediaPath(string virtualFolderName, string mediaPath)
 3784        {
 13785            ArgumentException.ThrowIfNullOrEmpty(mediaPath);
 3786
 13787            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 13788            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3789
 13790            if (!Directory.Exists(virtualFolderPath))
 3791            {
 13792                throw new FileNotFoundException(
 13793                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolder
 3794            }
 3795
 03796            var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
 03797                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 03798                .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, String
 3799
 03800            if (!string.IsNullOrEmpty(shortcut))
 3801            {
 03802                _fileSystem.DeleteFile(shortcut);
 3803            }
 3804
 03805            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3806
 03807            libraryOptions.PathInfos = libraryOptions
 03808                .PathInfos
 03809                .Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal))
 03810                .ToArray();
 3811
 03812            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03813        }
 3814
 3815        private static bool ItemIsVisible(BaseItem? item, User? user)
 3816        {
 223817            if (item is null)
 3818            {
 223819                return false;
 3820            }
 3821
 03822            if (user is null)
 3823            {
 03824                return true;
 3825            }
 3826
 03827            return item is UserRootFolder || item.IsVisibleStandalone(user);
 3828        }
 3829
 3830        public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
 3831        {
 03832            var path = pathInfo.Path;
 03833            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3834
 03835            var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 3836
 03837            var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3838
 03839            while (File.Exists(lnk))
 3840            {
 03841                shortcutFilename += "1";
 03842                lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3843            }
 3844
 03845            _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
 03846            RemoveContentTypeOverrides(path);
 03847        }
 3848
 3849        /// <inheritdoc />
 3850        public async Task RerouteLinkedChildReferencesAsync(Guid fromChildId, Guid toChildId)
 3851        {
 03852            var affectedParentIds = _linkedChildrenService.RerouteLinkedChildren(fromChildId, toChildId);
 3853
 3854            // Update in-memory LinkedChildren and re-save metadata (NFO) for affected parents
 03855            foreach (var parentId in affectedParentIds)
 3856            {
 03857                if (GetItemById(parentId) is Folder parent)
 3858                {
 03859                    foreach (var lc in parent.LinkedChildren)
 3860                    {
 03861                        if (lc.ItemId.HasValue && lc.ItemId.Value.Equals(fromChildId))
 3862                        {
 03863                            lc.ItemId = toChildId;
 3864                        }
 3865                    }
 3866
 03867                    await RunMetadataSavers(parent, ItemUpdateType.MetadataEdit).ConfigureAwait(false);
 3868                }
 3869            }
 03870        }
 3871
 3872        /// <inheritdoc />
 3873        public QueryFiltersLegacy GetQueryFiltersLegacy(InternalItemsQuery query)
 3874        {
 03875            if (query.User is not null)
 3876            {
 03877                AddUserToQuery(query, query.User);
 3878            }
 3879
 03880            SetTopParentOrAncestorIds(query);
 03881            return _itemRepository.GetQueryFiltersLegacy(query);
 3882        }
 3883
 3884        /// <inheritdoc />
 3885        public IReadOnlyList<string> GetMediaStreamLanguages(MediaStreamType mediaStreamType)
 3886        {
 03887            return _mediaStreamRepository.GetMediaStreamLanguages(mediaStreamType);
 3888        }
 3889    }
 3890}

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,MediaBrowser.Controller.Persistence.IMediaStreamRepository,System.Lazy`1<MediaBrowser.Controller.IO.IExternalDataManager>)
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>)
SetAdditionalPartsFromStack(MediaBrowser.Controller.Entities.Video,System.String)
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)
GetPeopleNamesByItems(System.Collections.Generic.IReadOnlyList`1<System.Guid>,System.Collections.Generic.IReadOnlyList`1<System.String>)
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)
GetMediaStreamLanguages(MediaBrowser.Model.Entities.MediaStreamType)