< 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
34%
Covered lines: 383
Uncovered lines: 730
Coverable lines: 1113
Total lines: 3375
Line coverage: 34.4%
Branch coverage
28%
Covered branches: 177
Total branches: 630
Branch coverage: 28%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 8/13/2025 - 12:11:40 AM Line coverage: 39.6% (427/1078) Branch coverage: 32.8% (200/608) Total lines: 32718/14/2025 - 12:11:05 AM Line coverage: 39.3% (427/1085) Branch coverage: 32.4% (200/616) Total lines: 32909/17/2025 - 12:11:23 AM Line coverage: 39.4% (428/1085) Branch coverage: 32.7% (202/616) Total lines: 32949/20/2025 - 12:11:30 AM Line coverage: 39.4% (428/1086) Branch coverage: 32.4% (200/616) Total lines: 32959/21/2025 - 12:11:25 AM Line coverage: 39.4% (428/1086) Branch coverage: 32.4% (200/616) Total lines: 33019/25/2025 - 12:11:18 AM Line coverage: 38.6% (428/1107) Branch coverage: 31.9% (200/626) Total lines: 335510/9/2025 - 12:11:25 AM Line coverage: 38.6% (428/1107) Branch coverage: 31.9% (200/626) Total lines: 335710/12/2025 - 12:11:07 AM Line coverage: 34.3% (380/1107) Branch coverage: 27.9% (175/626) Total lines: 335711/18/2025 - 12:11:25 AM Line coverage: 34.4% (383/1113) Branch coverage: 28% (177/630) Total lines: 3375 8/13/2025 - 12:11:40 AM Line coverage: 39.6% (427/1078) Branch coverage: 32.8% (200/608) Total lines: 32718/14/2025 - 12:11:05 AM Line coverage: 39.3% (427/1085) Branch coverage: 32.4% (200/616) Total lines: 32909/17/2025 - 12:11:23 AM Line coverage: 39.4% (428/1085) Branch coverage: 32.7% (202/616) Total lines: 32949/20/2025 - 12:11:30 AM Line coverage: 39.4% (428/1086) Branch coverage: 32.4% (200/616) Total lines: 32959/21/2025 - 12:11:25 AM Line coverage: 39.4% (428/1086) Branch coverage: 32.4% (200/616) Total lines: 33019/25/2025 - 12:11:18 AM Line coverage: 38.6% (428/1107) Branch coverage: 31.9% (200/626) Total lines: 335510/9/2025 - 12:11:25 AM Line coverage: 38.6% (428/1107) Branch coverage: 31.9% (200/626) Total lines: 335710/12/2025 - 12:11:07 AM Line coverage: 34.3% (380/1107) Branch coverage: 27.9% (175/626) Total lines: 335711/18/2025 - 12:11:25 AM Line coverage: 34.4% (383/1113) Branch coverage: 28% (177/630) Total lines: 3375

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(...)0%110100%
DeleteItem(...)0%1190340%
DeleteItemPath(...)0%272160%
IsInternalItem(...)0%342180%
GetMetadataPaths(...)0%620%
GetInternalMetadataPaths(...)0%7280%
ResolveItem(...)100%44100%
Resolve(...)100%1140%
GetNewItemId(...)100%11100%
GetNewItemIdInternal(...)100%66100%
ResolvePath(...)100%22100%
ResolvePath(...)70%242078.12%
IgnoreFile(...)100%11100%
NormalizeRootPathList(...)50%2291.66%
ResolvePaths(...)100%11100%
ResolvePaths(...)58.33%221258.33%
CreateRootFolder()71.42%141488.88%
GetUserRootFolder()80%111079.16%
FindByPath(...)100%210%
GetPerson(...)50%2280%
GetStudio(...)100%210%
GetStudioId(...)100%210%
GetGenreId(...)100%210%
GetMusicGenreId(...)100%210%
GetGenre(...)100%210%
GetMusicGenre(...)100%210%
GetYear(...)0%620%
GetArtist(...)100%210%
GetArtists(...)100%210%
GetArtist(...)100%210%
CreateItemByName(...)0%7280%
GetItemByNameId(...)100%11100%
ValidatePeopleAsync(...)100%210%
ValidateMediaLibrary(...)100%11100%
GetVirtualFolders()100%11100%
GetVirtualFolders(...)100%22100%
GetVirtualFolderInfo(...)70%101097.14%
GetCollectionType(...)50%6450%
GetItemById(...)66.66%7675%
GetItemById(...)100%22100%
GetItemById(...)50%22100%
GetItemById(...)50%22100%
GetItemList(...)100%1010100%
GetItemList(...)100%11100%
GetCount(...)0%7280%
GetItemCounts(...)0%7280%
GetItemList(...)0%4260%
GetLatestItemList(...)0%4260%
GetNextUpSeriesKeys(...)0%4260%
QueryItems(...)0%2040%
GetItemIds(...)50%2266.66%
GetStudios(...)0%620%
GetGenres(...)0%620%
GetMusicGenres(...)0%620%
GetAllArtists(...)0%620%
GetArtists(...)0%620%
SetTopParentOrAncestorIds(...)0%210140%
GetAlbumArtists(...)0%620%
GetItemsResult(...)100%1010100%
SetTopParentIdsOrAncestors(...)83.33%6688.88%
AddUserToQuery(...)100%1616100%
GetTopParentIdsForQuery(...)8.33%4342410.71%
ResolveIntro(...)0%110100%
Sort(...)57.14%341453.33%
Sort(...)0%210140%
GetComparer(...)50%3237.5%
CreateItem(...)100%210%
CreateItems(...)91.66%121285.71%
ImageNeedsRefresh(...)0%156120%
UpdateItemAsync(...)100%11100%
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(...)50%11863.63%
GetNamedView(...)100%210%
GetNamedView(...)0%4260%
GetNamedView(...)0%420200%
GetShadowView(...)0%272160%
GetNamedView(...)0%600240%
GetParentItem(...)33.33%8660%
QueueLibraryScan()100%210%
GetSeasonNumberFromPath(...)0%2040%
FillMissingEpisodeNumbersFromPath(...)0%3192560%
ParseName(...)0%620%
GetPathAfterNetworkSubstitution(...)25%6450%
GetPeople(...)100%210%
GetPeople(...)50%11425%
GetPeopleItems(...)100%210%
GetPeopleNames(...)100%210%
UpdatePeople(...)100%210%
StartScanInBackground()100%11100%
AddMediaPath(...)100%1150%
AddMediaPathInternal(...)25%36823.8%
UpdateMediaPath(...)100%210%
SyncLibraryOptionsToLocations(...)0%7280%
RemoveContentTypeOverrides(...)0%210140%
RemoveMediaPath(...)25%9433.33%
ItemIsVisible(...)16.66%14640%

File(s)

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

#LineLine coverage
 1#pragma warning disable CS1591
 2#pragma warning disable CA5394
 3
 4using System;
 5using System.Collections.Generic;
 6using System.Globalization;
 7using System.IO;
 8using System.Linq;
 9using System.Net;
 10using System.Net.Http;
 11using System.Threading;
 12using System.Threading.Tasks;
 13using BitFaster.Caching.Lru;
 14using Emby.Naming.Common;
 15using Emby.Naming.TV;
 16using Emby.Server.Implementations.Library.Resolvers;
 17using Emby.Server.Implementations.Library.Validators;
 18using Emby.Server.Implementations.Playlists;
 19using Emby.Server.Implementations.ScheduledTasks.Tasks;
 20using Emby.Server.Implementations.Sorting;
 21using Jellyfin.Data;
 22using Jellyfin.Data.Enums;
 23using Jellyfin.Database.Implementations.Entities;
 24using Jellyfin.Database.Implementations.Enums;
 25using Jellyfin.Extensions;
 26using MediaBrowser.Common.Extensions;
 27using MediaBrowser.Controller;
 28using MediaBrowser.Controller.Configuration;
 29using MediaBrowser.Controller.Drawing;
 30using MediaBrowser.Controller.Dto;
 31using MediaBrowser.Controller.Entities;
 32using MediaBrowser.Controller.Entities.Audio;
 33using MediaBrowser.Controller.IO;
 34using MediaBrowser.Controller.Library;
 35using MediaBrowser.Controller.LiveTv;
 36using MediaBrowser.Controller.MediaEncoding;
 37using MediaBrowser.Controller.MediaSegments;
 38using MediaBrowser.Controller.Persistence;
 39using MediaBrowser.Controller.Providers;
 40using MediaBrowser.Controller.Resolvers;
 41using MediaBrowser.Controller.Sorting;
 42using MediaBrowser.Controller.Trickplay;
 43using MediaBrowser.Model.Configuration;
 44using MediaBrowser.Model.Dlna;
 45using MediaBrowser.Model.Drawing;
 46using MediaBrowser.Model.Dto;
 47using MediaBrowser.Model.Entities;
 48using MediaBrowser.Model.IO;
 49using MediaBrowser.Model.Library;
 50using MediaBrowser.Model.Querying;
 51using MediaBrowser.Model.Tasks;
 52using Microsoft.Extensions.Logging;
 53using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 54using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
 55using Genre = MediaBrowser.Controller.Entities.Genre;
 56using Person = MediaBrowser.Controller.Entities.Person;
 57using VideoResolver = Emby.Naming.Video.VideoResolver;
 58
 59namespace Emby.Server.Implementations.Library
 60{
 61    /// <summary>
 62    /// Class LibraryManager.
 63    /// </summary>
 64    public class LibraryManager : ILibraryManager
 65    {
 66        private const string ShortcutFileExtension = ".mblink";
 67
 68        private readonly ILogger<LibraryManager> _logger;
 69        private readonly ITaskManager _taskManager;
 70        private readonly IUserManager _userManager;
 71        private readonly IUserDataManager _userDataManager;
 72        private readonly IServerConfigurationManager _configurationManager;
 73        private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory;
 74        private readonly Lazy<IProviderManager> _providerManagerFactory;
 75        private readonly Lazy<IUserViewManager> _userViewManagerFactory;
 76        private readonly IServerApplicationHost _appHost;
 77        private readonly IMediaEncoder _mediaEncoder;
 78        private readonly IFileSystem _fileSystem;
 79        private readonly IItemRepository _itemRepository;
 80        private readonly IImageProcessor _imageProcessor;
 81        private readonly NamingOptions _namingOptions;
 82        private readonly IPeopleRepository _peopleRepository;
 83        private readonly ExtraResolver _extraResolver;
 84        private readonly IPathManager _pathManager;
 85        private readonly FastConcurrentLru<Guid, BaseItem> _cache;
 86
 87        /// <summary>
 88        /// The _root folder sync lock.
 89        /// </summary>
 2890        private readonly Lock _rootFolderSyncLock = new();
 2891        private readonly Lock _userRootFolderSyncLock = new();
 92
 2893        private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 94
 95        /// <summary>
 96        /// The _root folder.
 97        /// </summary>
 98        private volatile AggregateFolder? _rootFolder;
 99        private volatile UserRootFolder? _userRootFolder;
 100
 101        private bool _wizardCompleted;
 102
 103        /// <summary>
 104        /// Initializes a new instance of the <see cref="LibraryManager" /> class.
 105        /// </summary>
 106        /// <param name="appHost">The application host.</param>
 107        /// <param name="loggerFactory">The logger factory.</param>
 108        /// <param name="taskManager">The task manager.</param>
 109        /// <param name="userManager">The user manager.</param>
 110        /// <param name="configurationManager">The configuration manager.</param>
 111        /// <param name="userDataManager">The user data manager.</param>
 112        /// <param name="libraryMonitorFactory">The library monitor.</param>
 113        /// <param name="fileSystem">The file system.</param>
 114        /// <param name="providerManagerFactory">The provider manager.</param>
 115        /// <param name="userViewManagerFactory">The user view manager.</param>
 116        /// <param name="mediaEncoder">The media encoder.</param>
 117        /// <param name="itemRepository">The item repository.</param>
 118        /// <param name="imageProcessor">The image processor.</param>
 119        /// <param name="namingOptions">The naming options.</param>
 120        /// <param name="directoryService">The directory service.</param>
 121        /// <param name="peopleRepository">The people repository.</param>
 122        /// <param name="pathManager">The path manager.</param>
 123        public LibraryManager(
 124            IServerApplicationHost appHost,
 125            ILoggerFactory loggerFactory,
 126            ITaskManager taskManager,
 127            IUserManager userManager,
 128            IServerConfigurationManager configurationManager,
 129            IUserDataManager userDataManager,
 130            Lazy<ILibraryMonitor> libraryMonitorFactory,
 131            IFileSystem fileSystem,
 132            Lazy<IProviderManager> providerManagerFactory,
 133            Lazy<IUserViewManager> userViewManagerFactory,
 134            IMediaEncoder mediaEncoder,
 135            IItemRepository itemRepository,
 136            IImageProcessor imageProcessor,
 137            NamingOptions namingOptions,
 138            IDirectoryService directoryService,
 139            IPeopleRepository peopleRepository,
 140            IPathManager pathManager)
 141        {
 28142            _appHost = appHost;
 28143            _logger = loggerFactory.CreateLogger<LibraryManager>();
 28144            _taskManager = taskManager;
 28145            _userManager = userManager;
 28146            _configurationManager = configurationManager;
 28147            _userDataManager = userDataManager;
 28148            _libraryMonitorFactory = libraryMonitorFactory;
 28149            _fileSystem = fileSystem;
 28150            _providerManagerFactory = providerManagerFactory;
 28151            _userViewManagerFactory = userViewManagerFactory;
 28152            _mediaEncoder = mediaEncoder;
 28153            _itemRepository = itemRepository;
 28154            _imageProcessor = imageProcessor;
 155
 28156            _cache = new FastConcurrentLru<Guid, BaseItem>(_configurationManager.Configuration.CacheSize);
 157
 28158            _namingOptions = namingOptions;
 28159            _peopleRepository = peopleRepository;
 28160            _pathManager = pathManager;
 28161            _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryServ
 162
 28163            _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 164
 28165            RecordConfigurationValues(_configurationManager.Configuration);
 28166        }
 167
 168        /// <summary>
 169        /// Occurs when [item added].
 170        /// </summary>
 171        public event EventHandler<ItemChangeEventArgs>? ItemAdded;
 172
 173        /// <summary>
 174        /// Occurs when [item updated].
 175        /// </summary>
 176        public event EventHandler<ItemChangeEventArgs>? ItemUpdated;
 177
 178        /// <summary>
 179        /// Occurs when [item removed].
 180        /// </summary>
 181        public event EventHandler<ItemChangeEventArgs>? ItemRemoved;
 182
 183        /// <summary>
 184        /// Gets the root folder.
 185        /// </summary>
 186        /// <value>The root folder.</value>
 187        public AggregateFolder RootFolder
 188        {
 189            get
 190            {
 182191                if (_rootFolder is null)
 21192                {
 193                    lock (_rootFolderSyncLock)
 194                    {
 21195                        _rootFolder ??= CreateRootFolder();
 21196                    }
 197                }
 198
 182199                return _rootFolder;
 200            }
 201        }
 202
 41203        private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
 204
 79205        private IProviderManager ProviderManager => _providerManagerFactory.Value;
 206
 1207        private IUserViewManager UserViewManager => _userViewManagerFactory.Value;
 208
 209        /// <summary>
 210        /// Gets or sets the postscan tasks.
 211        /// </summary>
 212        /// <value>The postscan tasks.</value>
 213        private ILibraryPostScanTask[] PostScanTasks { get; set; } = [];
 214
 215        /// <summary>
 216        /// Gets or sets the intro providers.
 217        /// </summary>
 218        /// <value>The intro providers.</value>
 219        private IIntroProvider[] IntroProviders { get; set; } = [];
 220
 221        /// <summary>
 222        /// Gets or sets the list of entity resolution ignore rules.
 223        /// </summary>
 224        /// <value>The entity resolution ignore rules.</value>
 225        private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = [];
 226
 227        /// <summary>
 228        /// Gets or sets the list of currently registered entity resolvers.
 229        /// </summary>
 230        /// <value>The entity resolvers enumerable.</value>
 231        private IItemResolver[] EntityResolvers { get; set; } = [];
 232
 233        private IMultiItemResolver[] MultiItemResolvers { get; set; } = [];
 234
 235        /// <summary>
 236        /// Gets or sets the comparers.
 237        /// </summary>
 238        /// <value>The comparers.</value>
 239        private IBaseItemComparer[] Comparers { get; set; } = [];
 240
 241        public bool IsScanRunning { get; private set; }
 242
 243        /// <summary>
 244        /// Adds the parts.
 245        /// </summary>
 246        /// <param name="rules">The rules.</param>
 247        /// <param name="resolvers">The resolvers.</param>
 248        /// <param name="introProviders">The intro providers.</param>
 249        /// <param name="itemComparers">The item comparers.</param>
 250        /// <param name="postScanTasks">The post scan tasks.</param>
 251        public void AddParts(
 252            IEnumerable<IResolverIgnoreRule> rules,
 253            IEnumerable<IItemResolver> resolvers,
 254            IEnumerable<IIntroProvider> introProviders,
 255            IEnumerable<IBaseItemComparer> itemComparers,
 256            IEnumerable<ILibraryPostScanTask> postScanTasks)
 257        {
 28258            EntityResolutionIgnoreRules = rules.ToArray();
 28259            EntityResolvers = resolvers.OrderBy(i => i.Priority).ToArray();
 28260            MultiItemResolvers = EntityResolvers.OfType<IMultiItemResolver>().ToArray();
 28261            IntroProviders = introProviders.ToArray();
 28262            Comparers = itemComparers.ToArray();
 28263            PostScanTasks = postScanTasks.ToArray();
 28264        }
 265
 266        /// <summary>
 267        /// Records the configuration values.
 268        /// </summary>
 269        /// <param name="configuration">The configuration.</param>
 270        private void RecordConfigurationValues(ServerConfiguration configuration)
 271        {
 129272            _wizardCompleted = configuration.IsStartupWizardCompleted;
 129273        }
 274
 275        /// <summary>
 276        /// Configurations the updated.
 277        /// </summary>
 278        /// <param name="sender">The sender.</param>
 279        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
 280        private void ConfigurationUpdated(object? sender, EventArgs e)
 281        {
 101282            var config = _configurationManager.Configuration;
 283
 101284            var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted;
 285
 101286            RecordConfigurationValues(config);
 287
 101288            if (wizardChanged)
 289            {
 16290                _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 291            }
 101292        }
 293
 294        public void RegisterItem(BaseItem item)
 295        {
 151296            ArgumentNullException.ThrowIfNull(item);
 297
 151298            if (item is IItemByName)
 299            {
 0300                if (item is not MusicArtist)
 301                {
 0302                    return;
 303                }
 304            }
 151305            else if (!item.IsFolder)
 306            {
 0307                if (item is not Video && item is not LiveTvChannel)
 308                {
 0309                    return;
 310                }
 311            }
 312
 151313            _cache.AddOrUpdate(item.Id, item);
 151314        }
 315
 316        public void DeleteItem(BaseItem item, DeleteOptions options)
 317        {
 0318            DeleteItem(item, options, false);
 0319        }
 320
 321        public void DeleteItem(BaseItem item, DeleteOptions options, bool notifyParentItem)
 322        {
 0323            ArgumentNullException.ThrowIfNull(item);
 324
 0325            var parent = item.GetOwner() ?? item.GetParent();
 326
 0327            DeleteItem(item, options, parent, notifyParentItem);
 0328        }
 329
 330        public void DeleteItemsUnsafeFast(IEnumerable<BaseItem> items)
 331        {
 0332            var pathMaps = items.Select(e => (Item: e, InternalPath: GetInternalMetadataPaths(e), DeletePaths: e.GetDele
 333
 0334            foreach (var (item, internalPaths, pathsToDelete) in pathMaps)
 335            {
 0336                foreach (var metadataPath in internalPaths)
 337                {
 0338                    if (!Directory.Exists(metadataPath))
 339                    {
 340                        continue;
 341                    }
 342
 0343                    _logger.LogDebug(
 0344                        "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0345                        item.GetType().Name,
 0346                        item.Name ?? "Unknown name",
 0347                        metadataPath,
 0348                        item.Id);
 349
 350                    try
 351                    {
 0352                        Directory.Delete(metadataPath, true);
 0353                    }
 0354                    catch (Exception ex)
 355                    {
 0356                        _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0357                    }
 358                }
 359
 0360                foreach (var fileSystemInfo in pathsToDelete)
 361                {
 0362                    DeleteItemPath(item, false, fileSystemInfo);
 363                }
 364            }
 365
 0366            _itemRepository.DeleteItem([.. pathMaps.Select(f => f.Item.Id)]);
 0367        }
 368
 369        public void DeleteItem(BaseItem item, DeleteOptions options, BaseItem parent, bool notifyParentItem)
 370        {
 0371            ArgumentNullException.ThrowIfNull(item);
 372
 0373            if (item.SourceType == SourceType.Channel)
 374            {
 0375                if (options.DeleteFromExternalProvider)
 376                {
 377                    try
 378                    {
 0379                        BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult();
 0380                    }
 0381                    catch (ArgumentException)
 382                    {
 383                        // channel no longer installed
 0384                    }
 385                }
 386
 0387                options.DeleteFileLocation = false;
 388            }
 389
 0390            if (item is LiveTvProgram)
 391            {
 0392                _logger.LogDebug(
 0393                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0394                    item.GetType().Name,
 0395                    item.Name ?? "Unknown name",
 0396                    item.Path ?? string.Empty,
 0397                    item.Id);
 398            }
 399            else
 400            {
 0401                _logger.LogInformation(
 0402                    "Removing item, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0403                    item.GetType().Name,
 0404                    item.Name ?? "Unknown name",
 0405                    item.Path ?? string.Empty,
 0406                    item.Id);
 407            }
 408
 0409            var children = item.IsFolder
 0410                ? ((Folder)item).GetRecursiveChildren(false)
 0411                : [];
 412
 0413            foreach (var metadataPath in GetMetadataPaths(item, children))
 414            {
 0415                if (!Directory.Exists(metadataPath))
 416                {
 417                    continue;
 418                }
 419
 0420                _logger.LogDebug(
 0421                    "Deleting metadata path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0422                    item.GetType().Name,
 0423                    item.Name ?? "Unknown name",
 0424                    metadataPath,
 0425                    item.Id);
 426
 427                try
 428                {
 0429                    Directory.Delete(metadataPath, true);
 0430                }
 0431                catch (Exception ex)
 432                {
 0433                    _logger.LogError(ex, "Error deleting {MetadataPath}", metadataPath);
 0434                }
 435            }
 436
 0437            if ((options.DeleteFileLocation && item.IsFileProtocol) || IsInternalItem(item))
 438            {
 439                // Assume only the first is required
 440                // Add this flag to GetDeletePaths if required in the future
 0441                var isRequiredForDelete = true;
 442
 0443                foreach (var fileSystemInfo in item.GetDeletePaths())
 444                {
 0445                    DeleteItemPath(item, isRequiredForDelete, fileSystemInfo);
 446
 0447                    isRequiredForDelete = false;
 448                }
 449            }
 450
 0451            item.SetParent(null);
 452
 0453            _itemRepository.DeleteItem([item.Id, .. children.Select(f => f.Id)]);
 0454            _cache.TryRemove(item.Id, out _);
 0455            foreach (var child in children)
 456            {
 0457                _cache.TryRemove(child.Id, out _);
 458            }
 459
 0460            if (parent is Folder folder)
 461            {
 0462                folder.Children = null;
 0463                folder.UserData = null;
 464            }
 465
 0466            ReportItemRemoved(item, parent);
 0467        }
 468
 469        private void DeleteItemPath(BaseItem item, bool isRequiredForDelete, FileSystemMetadata fileSystemInfo)
 470        {
 0471            if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName))
 472            {
 473                try
 474                {
 0475                    _logger.LogInformation(
 0476                        "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0477                        item.GetType().Name,
 0478                        item.Name ?? "Unknown name",
 0479                        fileSystemInfo.FullName,
 0480                        item.Id);
 481
 0482                    if (fileSystemInfo.IsDirectory)
 483                    {
 0484                        Directory.Delete(fileSystemInfo.FullName, true);
 485                    }
 486                    else
 487                    {
 0488                        File.Delete(fileSystemInfo.FullName);
 489                    }
 0490                }
 0491                catch (DirectoryNotFoundException)
 492                {
 0493                    _logger.LogInformation(
 0494                        "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id:
 0495                        item.GetType().Name,
 0496                        item.Name ?? "Unknown name",
 0497                        fileSystemInfo.FullName,
 0498                        item.Id);
 0499                }
 0500                catch (FileNotFoundException)
 501                {
 0502                    _logger.LogInformation(
 0503                        "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}
 0504                        item.GetType().Name,
 0505                        item.Name ?? "Unknown name",
 0506                        fileSystemInfo.FullName,
 0507                        item.Id);
 0508                }
 0509                catch (IOException)
 510                {
 0511                    if (isRequiredForDelete)
 512                    {
 0513                        throw;
 514                    }
 0515                }
 0516                catch (UnauthorizedAccessException)
 517                {
 0518                    if (isRequiredForDelete)
 519                    {
 0520                        throw;
 521                    }
 0522                }
 523            }
 0524        }
 525
 526        private bool IsInternalItem(BaseItem item)
 527        {
 0528            if (!item.IsFileProtocol)
 529            {
 0530                return false;
 531            }
 532
 0533            var pathToCheck = item switch
 0534            {
 0535                Genre => _configurationManager.ApplicationPaths.GenrePath,
 0536                MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
 0537                MusicGenre => _configurationManager.ApplicationPaths.MusicGenrePath,
 0538                Person => _configurationManager.ApplicationPaths.PeoplePath,
 0539                Studio => _configurationManager.ApplicationPaths.StudioPath,
 0540                Year => _configurationManager.ApplicationPaths.YearPath,
 0541                _ => null
 0542            };
 543
 0544            var itemPath = item.Path;
 0545            if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath))
 546            {
 0547                var cleanPath = _fileSystem.GetValidFilename(itemPath);
 0548                var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck);
 549
 0550                return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal);
 551            }
 552
 0553            return false;
 554        }
 555
 556        private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
 557        {
 0558            var list = GetInternalMetadataPaths(item);
 0559            foreach (var child in children)
 560            {
 0561                list.AddRange(GetInternalMetadataPaths(child));
 562            }
 563
 0564            return list;
 565        }
 566
 567        private List<string> GetInternalMetadataPaths(BaseItem item)
 568        {
 0569            var list = new List<string>
 0570            {
 0571                item.GetInternalMetadataPath()
 0572            };
 573
 0574            if (item is Video video)
 575            {
 576                // Trickplay
 0577                list.Add(_pathManager.GetTrickplayDirectory(video));
 578
 579                // Subtitles and attachments
 0580                foreach (var mediaSource in item.GetMediaSources(false))
 581                {
 0582                    var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
 0583                    if (subtitleFolder is not null)
 584                    {
 0585                        list.Add(subtitleFolder);
 586                    }
 587
 0588                    var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
 0589                    if (attachmentFolder is not null)
 590                    {
 0591                        list.Add(attachmentFolder);
 592                    }
 593                }
 594            }
 595
 0596            return list;
 597        }
 598
 599        /// <summary>
 600        /// Resolves the item.
 601        /// </summary>
 602        /// <param name="args">The args.</param>
 603        /// <param name="resolvers">The resolvers.</param>
 604        /// <returns>BaseItem.</returns>
 605        private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers)
 606        {
 77607            var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
 77608                .FirstOrDefault(i => i is not null);
 609
 77610            if (item is not null)
 611            {
 67612                ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this);
 613            }
 614
 77615            return item;
 616        }
 617
 618        private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver)
 619        {
 620            try
 621            {
 307622                return resolver.ResolvePath(args);
 623            }
 0624            catch (Exception ex)
 625            {
 0626                _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path);
 0627                return null;
 628            }
 307629        }
 630
 631        public Guid GetNewItemId(string key, Type type)
 632        {
 130633            return GetNewItemIdInternal(key, type, false);
 634        }
 635
 636        private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive)
 637        {
 131638            ArgumentException.ThrowIfNullOrEmpty(key);
 131639            ArgumentNullException.ThrowIfNull(type);
 640
 131641            string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
 131642            if (key.StartsWith(programDataPath, StringComparison.Ordinal))
 643            {
 644                // Try to normalize paths located underneath program-data in an attempt to make them more portable
 114645                key = key.Substring(programDataPath.Length)
 114646                    .TrimStart('/', '\\')
 114647                    .Replace('/', '\\');
 648            }
 649
 131650            if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
 651            {
 1652                key = key.ToLowerInvariant();
 653            }
 654
 131655            key = type.FullName + key;
 656
 131657            return key.GetMD5();
 658        }
 659
 660        public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directorySer
 42661            => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
 662
 663        private BaseItem? ResolvePath(
 664            FileSystemMetadata fileInfo,
 665            IDirectoryService directoryService,
 666            IItemResolver[]? resolvers,
 667            Folder? parent = null,
 668            CollectionType? collectionType = null,
 669            LibraryOptions? libraryOptions = null)
 670        {
 77671            ArgumentNullException.ThrowIfNull(fileInfo);
 672
 77673            var fullPath = fileInfo.FullName;
 674
 77675            if (collectionType is null && parent is not null)
 676            {
 18677                collectionType = GetContentTypeOverride(fullPath, true);
 678            }
 679
 77680            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
 77681            {
 77682                Parent = parent,
 77683                FileInfo = fileInfo,
 77684                CollectionType = collectionType,
 77685                LibraryOptions = libraryOptions
 77686            };
 687
 688            // Return null if ignore rules deem that we should do so
 77689            if (IgnoreFile(args.FileInfo, args.Parent))
 690            {
 0691                return null;
 692            }
 693
 694            // Gather child folder and files
 77695            if (args.IsDirectory)
 696            {
 50697                var isPhysicalRoot = args.IsPhysicalRoot;
 698
 699                // When resolving the root, we need it's grandchildren (children of user views)
 50700                var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
 701
 702                FileSystemMetadata[] files;
 50703                var isVf = args.IsVf;
 704
 705                try
 706                {
 50707                    files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _l
 50708                }
 0709                catch (Exception ex)
 710                {
 0711                    if (parent is not null && parent.IsPhysicalRoot)
 712                    {
 0713                        _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPh
 714
 0715                        files = [];
 716                    }
 717                    else
 718                    {
 0719                        throw;
 720                    }
 0721                }
 722
 723                // Need to remove sub-paths that may have been resolved from shortcuts
 724                // Example: if \\server\movies exists, then strip out \\server\movies\action
 50725                if (isPhysicalRoot)
 726                {
 21727                    files = NormalizeRootPathList(files).ToArray();
 728                }
 729
 50730                args.FileSystemChildren = files;
 731            }
 732
 733            // Filter content based on ignore rules
 77734            if (args.IsDirectory)
 735            {
 50736                var filtered = args.GetActualFileSystemChildren().ToArray();
 50737                args.FileSystemChildren = filtered ?? [];
 738            }
 739
 77740            return ResolveItem(args, resolvers);
 741        }
 742
 743        public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent)
 103744            => EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent));
 745
 746        public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
 747        {
 80748            var originalList = paths.ToList();
 749
 80750            var list = originalList.Where(i => i.IsDirectory)
 80751                .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
 80752                .Distinct()
 80753                .ToList();
 754
 80755            var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.Ordinal) && list.Any(i => _fileS
 80756                .ToList();
 757
 160758            foreach (var dupe in dupes)
 759            {
 0760                _logger.LogInformation("Found duplicate path: {0}", dupe);
 761            }
 762
 80763            var newList = list.Except(dupes, StringComparer.Ordinal).Select(_fileSystem.GetDirectoryInfo).ToList();
 80764            newList.AddRange(originalList.Where(i => !i.IsDirectory));
 80765            return newList;
 766        }
 767
 768        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryServ
 769        {
 58770            return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
 771        }
 772
 773        public IEnumerable<BaseItem> ResolvePaths(
 774            IEnumerable<FileSystemMetadata> files,
 775            IDirectoryService directoryService,
 776            Folder parent,
 777            LibraryOptions libraryOptions,
 778            CollectionType? collectionType,
 779            IItemResolver[] resolvers)
 780        {
 58781            var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
 782
 58783            if (parent is not null)
 784            {
 58785                var multiItemResolvers = resolvers is null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>()
 786
 348787                foreach (var resolver in multiItemResolvers)
 788                {
 116789                    var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
 790
 116791                    if (result?.Items.Count > 0)
 792                    {
 0793                        var items = result.Items;
 0794                        items.RemoveAll(item => !ResolverHelper.SetInitialItemValues(item, parent, this, directoryServic
 0795                        items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, reso
 0796                        return items;
 797                    }
 798                }
 799            }
 800
 58801            return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions);
 0802        }
 803
 804        private IEnumerable<BaseItem> ResolveFileList(
 805            IReadOnlyList<FileSystemMetadata> fileList,
 806            IDirectoryService directoryService,
 807            Folder? parent,
 808            CollectionType? collectionType,
 809            IItemResolver[]? resolvers,
 810            LibraryOptions libraryOptions)
 811        {
 812            // Given that fileList is a list we can save enumerator allocations by indexing
 813            for (var i = 0; i < fileList.Count; i++)
 814            {
 815                var file = fileList[i];
 816                BaseItem? result = null;
 817                try
 818                {
 819                    result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
 820                }
 821                catch (Exception ex)
 822                {
 823                    _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
 824                }
 825
 826                if (result is not null)
 827                {
 828                    yield return result;
 829                }
 830            }
 831        }
 832
 833        /// <summary>
 834        /// Creates the root media folder.
 835        /// </summary>
 836        /// <returns>AggregateFolder.</returns>
 837        /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</except
 838        public AggregateFolder CreateRootFolder()
 839        {
 21840            var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath;
 841
 21842            var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
 21843                             (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOp
 21844                             .DeepCopy<Folder, AggregateFolder>();
 845
 846            // In case program data folder was moved
 21847            if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal))
 848            {
 0849                _logger.LogInformation("Resetting root folder path to {0}", rootFolderPath);
 0850                rootFolder.Path = rootFolderPath;
 851            }
 852
 853            // Add in the plug-in folders
 21854            var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
 855
 21856            var info = Directory.CreateDirectory(path);
 21857            Folder folder = new PlaylistsFolder
 21858            {
 21859                Path = path,
 21860                DateCreated = info.CreationTimeUtc,
 21861                DateModified = info.LastWriteTimeUtc,
 21862            };
 863
 21864            if (folder.Id.IsEmpty())
 865            {
 21866                folder.Id = GetNewItemId(folder.Path, folder.GetType());
 867            }
 868
 21869            var dbItem = GetItemById(folder.Id) as BasePluginFolder;
 870
 21871            if (dbItem is not null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
 872            {
 0873                folder = dbItem;
 874            }
 875
 21876            if (!folder.ParentId.Equals(rootFolder.Id))
 877            {
 21878                rootFolder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().G
 21879                folder.ParentId = rootFolder.Id;
 21880                folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetRe
 881            }
 882
 21883            rootFolder.AddVirtualChild(folder);
 884
 21885            RegisterItem(folder);
 886
 21887            return rootFolder;
 888        }
 889
 890        public Folder GetUserRootFolder()
 891        {
 819892            if (_userRootFolder is null)
 21893            {
 894                lock (_userRootFolderSyncLock)
 895                {
 21896                    if (_userRootFolder is null)
 897                    {
 21898                        var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 899
 21900                        _logger.LogDebug("Creating userRootPath at {Path}", userRootPath);
 21901                        Directory.CreateDirectory(userRootPath);
 902
 21903                        var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
 21904                        UserRootFolder? tmpItem = null;
 905                        try
 906                        {
 21907                            tmpItem = GetItemById(newItemId) as UserRootFolder;
 21908                        }
 0909                        catch (Exception ex)
 910                        {
 0911                            _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId);
 0912                        }
 913
 21914                        if (tmpItem is null)
 915                        {
 21916                            _logger.LogDebug("Creating new userRootFolder with DeepCopy");
 21917                            tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new In
 21918                                        .DeepCopy<Folder, UserRootFolder>();
 919                        }
 920
 921                        // In case program data folder was moved
 21922                        if (!string.Equals(tmpItem.Path, userRootPath, StringComparison.Ordinal))
 923                        {
 0924                            _logger.LogInformation("Resetting user root folder path to {0}", userRootPath);
 0925                            tmpItem.Path = userRootPath;
 926                        }
 927
 21928                        _userRootFolder = tmpItem;
 21929                        _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder);
 930                    }
 21931                }
 932            }
 933
 819934            return _userRootFolder;
 935        }
 936
 937        /// <inheritdoc />
 938        public BaseItem? FindByPath(string path, bool? isFolder)
 939        {
 940            // If this returns multiple items it could be tricky figuring out which one is correct.
 941            // In most cases, the newest one will be and the others obsolete but not yet cleaned up
 0942            ArgumentException.ThrowIfNullOrEmpty(path);
 943
 0944            var query = new InternalItemsQuery
 0945            {
 0946                Path = path,
 0947                IsFolder = isFolder,
 0948                OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)],
 0949                Limit = 1,
 0950                DtoOptions = new DtoOptions(true)
 0951            };
 952
 0953            return GetItemList(query)
 0954                .FirstOrDefault();
 955        }
 956
 957        /// <inheritdoc />
 958        public Person? GetPerson(string name)
 959        {
 1960            var path = Person.GetPath(name);
 1961            var id = GetItemByNameId<Person>(path);
 1962            if (GetItemById(id) is Person item)
 963            {
 0964                return item;
 965            }
 966
 1967            return null;
 968        }
 969
 970        /// <summary>
 971        /// Gets the studio.
 972        /// </summary>
 973        /// <param name="name">The name.</param>
 974        /// <returns>Task{Studio}.</returns>
 975        public Studio GetStudio(string name)
 976        {
 0977            return CreateItemByName<Studio>(Studio.GetPath, name, new DtoOptions(true));
 978        }
 979
 980        public Guid GetStudioId(string name)
 981        {
 0982            return GetItemByNameId<Studio>(Studio.GetPath(name));
 983        }
 984
 985        public Guid GetGenreId(string name)
 986        {
 0987            return GetItemByNameId<Genre>(Genre.GetPath(name));
 988        }
 989
 990        public Guid GetMusicGenreId(string name)
 991        {
 0992            return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
 993        }
 994
 995        /// <summary>
 996        /// Gets the genre.
 997        /// </summary>
 998        /// <param name="name">The name.</param>
 999        /// <returns>Task{Genre}.</returns>
 1000        public Genre GetGenre(string name)
 1001        {
 01002            return CreateItemByName<Genre>(Genre.GetPath, name, new DtoOptions(true));
 1003        }
 1004
 1005        /// <summary>
 1006        /// Gets the music genre.
 1007        /// </summary>
 1008        /// <param name="name">The name.</param>
 1009        /// <returns>Task{MusicGenre}.</returns>
 1010        public MusicGenre GetMusicGenre(string name)
 1011        {
 01012            return CreateItemByName<MusicGenre>(MusicGenre.GetPath, name, new DtoOptions(true));
 1013        }
 1014
 1015        /// <summary>
 1016        /// Gets the year.
 1017        /// </summary>
 1018        /// <param name="value">The value.</param>
 1019        /// <returns>Task{Year}.</returns>
 1020        public Year GetYear(int value)
 1021        {
 01022            if (value <= 0)
 1023            {
 01024                throw new ArgumentOutOfRangeException(nameof(value), "Years less than or equal to 0 are invalid.");
 1025            }
 1026
 01027            var name = value.ToString(CultureInfo.InvariantCulture);
 1028
 01029            return CreateItemByName<Year>(Year.GetPath, name, new DtoOptions(true));
 1030        }
 1031
 1032        /// <summary>
 1033        /// Gets a Genre.
 1034        /// </summary>
 1035        /// <param name="name">The name.</param>
 1036        /// <returns>Task{Genre}.</returns>
 1037        public MusicArtist GetArtist(string name)
 1038        {
 01039            return GetArtist(name, new DtoOptions(true));
 1040        }
 1041
 1042        public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
 1043        {
 01044            return _itemRepository.FindArtists(names);
 1045        }
 1046
 1047        public MusicArtist GetArtist(string name, DtoOptions options)
 1048        {
 01049            return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options);
 1050        }
 1051
 1052        private T CreateItemByName<T>(Func<string, string> getPathFn, string name, DtoOptions options)
 1053            where T : BaseItem, new()
 1054        {
 01055            if (typeof(T) == typeof(MusicArtist))
 1056            {
 01057                var existing = GetItemList(new InternalItemsQuery
 01058                {
 01059                    IncludeItemTypes = [BaseItemKind.MusicArtist],
 01060                    Name = name,
 01061                    DtoOptions = options
 01062                }).Cast<MusicArtist>()
 01063                .OrderBy(i => i.IsAccessedByName ? 1 : 0)
 01064                .Cast<T>()
 01065                .FirstOrDefault();
 1066
 01067                if (existing is not null)
 1068                {
 01069                    return existing;
 1070                }
 1071            }
 1072
 01073            var path = getPathFn(name);
 01074            var id = GetItemByNameId<T>(path);
 01075            var item = GetItemById(id) as T;
 01076            if (item is null)
 1077            {
 01078                var info = Directory.CreateDirectory(path);
 01079                item = new T
 01080                {
 01081                    Name = name,
 01082                    Id = id,
 01083                    DateCreated = info.CreationTimeUtc,
 01084                    DateModified = info.LastWriteTimeUtc,
 01085                    Path = path
 01086                };
 1087
 01088                CreateItem(item, null);
 1089            }
 1090
 01091            return item;
 1092        }
 1093
 1094        private Guid GetItemByNameId<T>(string path)
 1095              where T : BaseItem, new()
 1096        {
 11097            var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
 11098            return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
 1099        }
 1100
 1101        /// <inheritdoc />
 1102        public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
 1103        {
 1104            // Ensure the location is available.
 01105            Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
 1106
 01107            return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
 1108        }
 1109
 1110        /// <summary>
 1111        /// Reloads the root media folder.
 1112        /// </summary>
 1113        /// <param name="progress">The progress.</param>
 1114        /// <param name="cancellationToken">The cancellation token.</param>
 1115        /// <returns>Task.</returns>
 1116        public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
 1117        {
 1118            // Just run the scheduled task so that the user can see it
 31119            _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 1120
 31121            return Task.CompletedTask;
 1122        }
 1123
 1124        /// <summary>
 1125        /// Validates the media library internal.
 1126        /// </summary>
 1127        /// <param name="progress">The progress.</param>
 1128        /// <param name="cancellationToken">The cancellation token.</param>
 1129        /// <returns>Task.</returns>
 1130        public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
 1131        {
 1132            IsScanRunning = true;
 1133            LibraryMonitor.Stop();
 1134
 1135            try
 1136            {
 1137                await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false);
 1138            }
 1139            finally
 1140            {
 1141                LibraryMonitor.Start();
 1142                IsScanRunning = false;
 1143            }
 1144        }
 1145
 1146        public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
 1147        {
 1148            RootFolder.Children = null;
 1149            await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1150
 1151            // Start by just validating the children of the root, but go no further
 1152            await RootFolder.ValidateChildren(
 1153                new Progress<double>(),
 1154                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 1155                recursive: false,
 1156                allowRemoveRoot: removeRoot,
 1157                cancellationToken: cancellationToken).ConfigureAwait(false);
 1158
 1159            var rootFolder = GetUserRootFolder();
 1160            rootFolder.Children = null;
 1161
 1162            await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1163
 1164            await rootFolder.ValidateChildren(
 1165                new Progress<double>(),
 1166                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 1167                recursive: false,
 1168                allowRemoveRoot: removeRoot,
 1169                cancellationToken: cancellationToken).ConfigureAwait(false);
 1170
 1171            // Quickly scan CollectionFolders for changes
 1172            var toDelete = new List<Guid>();
 1173            foreach (var child in rootFolder.Children!.OfType<Folder>())
 1174            {
 1175                // If the user has somehow deleted the collection directory, remove the metadata from the database.
 1176                if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
 1177                {
 1178                    toDelete.Add(collectionFolder.Id);
 1179                }
 1180                else
 1181                {
 1182                    await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1183                }
 1184            }
 1185
 1186            if (toDelete.Count > 0)
 1187            {
 1188                _itemRepository.DeleteItem(toDelete.ToArray());
 1189            }
 1190        }
 1191
 1192        private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
 1193        {
 1194            _logger.LogInformation("Validating media library");
 1195
 1196            await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false);
 1197
 1198            var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96));
 1199
 1200            // Validate the entire media library
 1201            await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem
 1202
 1203            progress.Report(96);
 1204
 1205            innerProgress = new Progress<double>(pct => progress.Report(96 + (pct * .04)));
 1206
 1207            await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
 1208
 1209            progress.Report(100);
 1210        }
 1211
 1212        /// <summary>
 1213        /// Runs the post scan tasks.
 1214        /// </summary>
 1215        /// <param name="progress">The progress.</param>
 1216        /// <param name="cancellationToken">The cancellation token.</param>
 1217        /// <returns>Task.</returns>
 1218        private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
 1219        {
 1220            var tasks = PostScanTasks.ToList();
 1221
 1222            var numComplete = 0;
 1223            var numTasks = tasks.Count;
 1224
 1225            foreach (var task in tasks)
 1226            {
 1227                // Prevent access to modified closure
 1228                var currentNumComplete = numComplete;
 1229
 1230                var innerProgress = new Progress<double>(pct =>
 1231                {
 1232                    double innerPercent = pct;
 1233                    innerPercent /= 100;
 1234                    innerPercent += currentNumComplete;
 1235
 1236                    innerPercent /= numTasks;
 1237                    innerPercent *= 100;
 1238
 1239                    progress.Report(innerPercent);
 1240                });
 1241
 1242                _logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
 1243
 1244                try
 1245                {
 1246                    await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
 1247                }
 1248                catch (OperationCanceledException)
 1249                {
 1250                    _logger.LogInformation("Post-scan task cancelled: {0}", task.GetType().Name);
 1251                    throw;
 1252                }
 1253                catch (Exception ex)
 1254                {
 1255                    _logger.LogError(ex, "Error running post-scan task");
 1256                }
 1257
 1258                numComplete++;
 1259                double percent = numComplete;
 1260                percent /= numTasks;
 1261                progress.Report(percent * 100);
 1262            }
 1263
 1264            _itemRepository.UpdateInheritedValues();
 1265
 1266            progress.Report(100);
 1267        }
 1268
 1269        /// <summary>
 1270        /// Gets the default view.
 1271        /// </summary>
 1272        /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
 1273        public List<VirtualFolderInfo> GetVirtualFolders()
 1274        {
 231275            return GetVirtualFolders(false);
 1276        }
 1277
 1278        public List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState)
 1279        {
 241280            _logger.LogDebug("Getting topLibraryFolders");
 241281            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 1282
 241283            _logger.LogDebug("Getting refreshQueue");
 241284            var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null;
 1285
 241286            return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath)
 241287                .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
 241288                .ToList();
 1289        }
 1290
 1291        private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid>? r
 1292        {
 11293            var info = new VirtualFolderInfo
 11294            {
 11295                Name = Path.GetFileName(dir),
 11296
 11297                Locations = _fileSystem.GetFilePaths(dir, false)
 11298                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 11299                    .Select(i =>
 11300                    {
 11301                        try
 11302                        {
 11303                            return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i));
 11304                        }
 11305                        catch (Exception ex)
 11306                        {
 11307                            _logger.LogError(ex, "Error resolving shortcut file {File}", i);
 11308                            return null;
 11309                        }
 11310                    })
 11311                    .Where(i => i is not null)
 11312                    .Order()
 11313                    .ToArray(),
 11314
 11315                CollectionType = GetCollectionType(dir)
 11316            };
 1317
 11318            var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.Ord
 11319            if (libraryFolder is not null)
 1320            {
 11321                var libraryFolderId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture);
 11322                info.ItemId = libraryFolderId;
 11323                if (libraryFolder.HasImage(ImageType.Primary))
 1324                {
 01325                    info.PrimaryImageItemId = libraryFolderId;
 1326                }
 1327
 11328                info.LibraryOptions = GetLibraryOptions(libraryFolder);
 1329
 11330                if (refreshQueue is not null)
 1331                {
 11332                    info.RefreshProgress = libraryFolder.GetRefreshProgress();
 1333
 11334                    info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.Contains(libraryFolder.
 1335                }
 1336            }
 1337
 11338            return info;
 1339        }
 1340
 1341        private CollectionTypeOptions? GetCollectionType(string path)
 1342        {
 11343            var files = _fileSystem.GetFilePaths(path, [".collection"], true, false);
 21344            foreach (ReadOnlySpan<char> file in files)
 1345            {
 01346                if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
 1347                {
 01348                    return res;
 1349                }
 1350            }
 1351
 11352            return null;
 01353        }
 1354
 1355        /// <inheritdoc />
 1356        public BaseItem? GetItemById(Guid id)
 1357        {
 5551358            if (id.IsEmpty())
 1359            {
 01360                throw new ArgumentException("Guid can't be empty", nameof(id));
 1361            }
 1362
 5551363            if (_cache.TryGet(id, out var item))
 1364            {
 4521365                return item;
 1366            }
 1367
 1031368            item = RetrieveItem(id);
 1369
 1031370            if (item is not null)
 1371            {
 01372                RegisterItem(item);
 1373            }
 1374
 1031375            return item;
 1376        }
 1377
 1378        /// <inheritdoc />
 1379        public T? GetItemById<T>(Guid id)
 1380            where T : BaseItem
 1381        {
 231382            var item = GetItemById(id);
 231383            if (item is T typedItem)
 1384            {
 11385                return typedItem;
 1386            }
 1387
 221388            return null;
 1389        }
 1390
 1391        /// <inheritdoc />
 1392        public T? GetItemById<T>(Guid id, Guid userId)
 1393            where T : BaseItem
 1394        {
 11395            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 11396            return GetItemById<T>(id, user);
 1397        }
 1398
 1399        /// <inheritdoc />
 1400        public T? GetItemById<T>(Guid id, User? user)
 1401            where T : BaseItem
 1402        {
 211403            var item = GetItemById<T>(id);
 211404            return ItemIsVisible(item, user) ? item : null;
 1405        }
 1406
 1407        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
 1408        {
 1171409            if (query.Recursive && !query.ParentId.IsEmpty())
 1410            {
 451411                var parent = GetItemById(query.ParentId);
 451412                if (parent is not null)
 1413                {
 451414                    SetTopParentIdsOrAncestors(query, [parent]);
 1415                }
 1416            }
 1417
 1171418            if (query.User is not null)
 1419            {
 11420                AddUserToQuery(query, query.User, allowExternalContent);
 1421            }
 1422
 1171423            var itemList = _itemRepository.GetItemList(query);
 1171424            var user = query.User;
 1171425            if (user is not null)
 1426            {
 11427                return itemList.Where(i => i.IsVisible(user)).ToList();
 1428            }
 1429
 1161430            return itemList;
 1431        }
 1432
 1433        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 1434        {
 1171435            return GetItemList(query, true);
 1436        }
 1437
 1438        public int GetCount(InternalItemsQuery query)
 1439        {
 01440            if (query.Recursive && !query.ParentId.IsEmpty())
 1441            {
 01442                var parent = GetItemById(query.ParentId);
 01443                if (parent is not null)
 1444                {
 01445                    SetTopParentIdsOrAncestors(query, [parent]);
 1446                }
 1447            }
 1448
 01449            if (query.User is not null)
 1450            {
 01451                AddUserToQuery(query, query.User);
 1452            }
 1453
 01454            return _itemRepository.GetCount(query);
 1455        }
 1456
 1457        public ItemCounts GetItemCounts(InternalItemsQuery query)
 1458        {
 01459            if (query.Recursive && !query.ParentId.IsEmpty())
 1460            {
 01461                var parent = GetItemById(query.ParentId);
 01462                if (parent is not null)
 1463                {
 01464                    SetTopParentIdsOrAncestors(query, [parent]);
 1465                }
 1466            }
 1467
 01468            if (query.User is not null)
 1469            {
 01470                AddUserToQuery(query, query.User);
 1471            }
 1472
 01473            return _itemRepository.GetItemCounts(query);
 1474        }
 1475
 1476        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
 1477        {
 01478            SetTopParentIdsOrAncestors(query, parents);
 1479
 01480            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1481            {
 01482                if (query.User is not null)
 1483                {
 01484                    AddUserToQuery(query, query.User);
 1485                }
 1486            }
 1487
 01488            return _itemRepository.GetItemList(query);
 1489        }
 1490
 1491        public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, Coll
 1492        {
 01493            SetTopParentIdsOrAncestors(query, parents);
 1494
 01495            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1496            {
 01497                if (query.User is not null)
 1498                {
 01499                    AddUserToQuery(query, query.User);
 1500                }
 1501            }
 1502
 01503            return _itemRepository.GetLatestItemList(query, collectionType);
 1504        }
 1505
 1506        public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents
 1507        {
 01508            SetTopParentIdsOrAncestors(query, parents);
 1509
 01510            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1511            {
 01512                if (query.User is not null)
 1513                {
 01514                    AddUserToQuery(query, query.User);
 1515                }
 1516            }
 1517
 01518            return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
 1519        }
 1520
 1521        public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
 1522        {
 01523            if (query.User is not null)
 1524            {
 01525                AddUserToQuery(query, query.User);
 1526            }
 1527
 01528            if (query.EnableTotalRecordCount)
 1529            {
 01530                return _itemRepository.GetItems(query);
 1531            }
 1532
 01533            return new QueryResult<BaseItem>(
 01534                query.StartIndex,
 01535                null,
 01536                _itemRepository.GetItemList(query));
 1537        }
 1538
 1539        public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
 1540        {
 161541            if (query.User is not null)
 1542            {
 01543                AddUserToQuery(query, query.User);
 1544            }
 1545
 161546            return _itemRepository.GetItemIdsList(query);
 1547        }
 1548
 1549        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
 1550        {
 01551            if (query.User is not null)
 1552            {
 01553                AddUserToQuery(query, query.User);
 1554            }
 1555
 01556            SetTopParentOrAncestorIds(query);
 01557            return _itemRepository.GetStudios(query);
 1558        }
 1559
 1560        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
 1561        {
 01562            if (query.User is not null)
 1563            {
 01564                AddUserToQuery(query, query.User);
 1565            }
 1566
 01567            SetTopParentOrAncestorIds(query);
 01568            return _itemRepository.GetGenres(query);
 1569        }
 1570
 1571        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
 1572        {
 01573            if (query.User is not null)
 1574            {
 01575                AddUserToQuery(query, query.User);
 1576            }
 1577
 01578            SetTopParentOrAncestorIds(query);
 01579            return _itemRepository.GetMusicGenres(query);
 1580        }
 1581
 1582        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
 1583        {
 01584            if (query.User is not null)
 1585            {
 01586                AddUserToQuery(query, query.User);
 1587            }
 1588
 01589            SetTopParentOrAncestorIds(query);
 01590            return _itemRepository.GetAllArtists(query);
 1591        }
 1592
 1593        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
 1594        {
 01595            if (query.User is not null)
 1596            {
 01597                AddUserToQuery(query, query.User);
 1598            }
 1599
 01600            SetTopParentOrAncestorIds(query);
 01601            return _itemRepository.GetArtists(query);
 1602        }
 1603
 1604        private void SetTopParentOrAncestorIds(InternalItemsQuery query)
 1605        {
 01606            var ancestorIds = query.AncestorIds;
 01607            int len = ancestorIds.Length;
 01608            if (len == 0)
 1609            {
 01610                return;
 1611            }
 1612
 01613            var parents = new BaseItem[len];
 01614            for (int i = 0; i < len; i++)
 1615            {
 01616                parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id:
 01617                if (parents[i] is not (ICollectionFolder or UserView))
 1618                {
 01619                    return;
 1620                }
 1621            }
 1622
 1623            // Optimize by querying against top level views
 01624            query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 01625            query.AncestorIds = [];
 1626
 1627            // Prevent searching in all libraries due to empty filter
 01628            if (query.TopParentIds.Length == 0)
 1629            {
 01630                query.TopParentIds = [Guid.NewGuid()];
 1631            }
 01632        }
 1633
 1634        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
 1635        {
 01636            if (query.User is not null)
 1637            {
 01638                AddUserToQuery(query, query.User);
 1639            }
 1640
 01641            SetTopParentOrAncestorIds(query);
 01642            return _itemRepository.GetAlbumArtists(query);
 1643        }
 1644
 1645        public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
 1646        {
 141647            if (query.Recursive && !query.ParentId.IsEmpty())
 1648            {
 131649                var parent = GetItemById(query.ParentId);
 131650                if (parent is not null)
 1651                {
 131652                    SetTopParentIdsOrAncestors(query, [parent]);
 1653                }
 1654            }
 1655
 141656            if (query.User is not null)
 1657            {
 11658                AddUserToQuery(query, query.User);
 1659            }
 1660
 141661            if (query.EnableTotalRecordCount)
 1662            {
 11663                return _itemRepository.GetItems(query);
 1664            }
 1665
 131666            return new QueryResult<BaseItem>(
 131667                query.StartIndex,
 131668                null,
 131669                _itemRepository.GetItemList(query));
 1670        }
 1671
 1672        private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
 1673        {
 581674            if (parents.All(i => i is ICollectionFolder || i is UserView))
 1675            {
 1676                // Optimize by querying against top level views
 131677                query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 1678
 1679                // Prevent searching in all libraries due to empty filter
 131680                if (query.TopParentIds.Length == 0)
 1681                {
 131682                    query.TopParentIds = [Guid.NewGuid()];
 1683                }
 1684            }
 1685            else
 1686            {
 1687                // We need to be able to query from any arbitrary ancestor up the tree
 451688                query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
 1689
 1690                // Prevent searching in all libraries due to empty filter
 451691                if (query.AncestorIds.Length == 0)
 1692                {
 01693                    query.AncestorIds = [Guid.NewGuid()];
 1694                }
 1695            }
 1696
 581697            query.Parent = null;
 581698        }
 1699
 1700        private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
 1701        {
 21702            if (query.AncestorIds.Length == 0 &&
 21703                query.ParentId.IsEmpty() &&
 21704                query.ChannelIds.Count == 0 &&
 21705                query.TopParentIds.Length == 0 &&
 21706                string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
 21707                string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
 21708                query.ItemIds.Length == 0)
 1709            {
 11710                var userViews = UserViewManager.GetUserViews(new UserViewQuery
 11711                {
 11712                    User = user,
 11713                    IncludeHidden = true,
 11714                    IncludeExternalContent = allowExternalContent
 11715                });
 1716
 11717                query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
 1718
 1719                // Prevent searching in all libraries due to empty filter
 11720                if (query.TopParentIds.Length == 0)
 1721                {
 11722                    query.TopParentIds = [Guid.NewGuid()];
 1723                }
 1724            }
 21725        }
 1726
 1727        private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
 1728        {
 131729            if (item is UserView view)
 1730            {
 01731                if (view.ViewType == CollectionType.livetv)
 1732                {
 01733                    return [view.Id];
 1734                }
 1735
 1736                // Translate view into folders
 01737                if (!view.DisplayParentId.IsEmpty())
 1738                {
 01739                    var displayParent = GetItemById(view.DisplayParentId);
 01740                    if (displayParent is not null)
 1741                    {
 01742                        return GetTopParentIdsForQuery(displayParent, user);
 1743                    }
 1744
 01745                    return [];
 1746                }
 1747
 01748                if (!view.ParentId.IsEmpty())
 1749                {
 01750                    var displayParent = GetItemById(view.ParentId);
 01751                    if (displayParent is not null)
 1752                    {
 01753                        return GetTopParentIdsForQuery(displayParent, user);
 1754                    }
 1755
 01756                    return [];
 1757                }
 1758
 1759                // Handle grouping
 01760                if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.V
 01761                    && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
 1762                {
 01763                    return GetUserRootFolder()
 01764                        .GetChildren(user, true)
 01765                        .OfType<CollectionFolder>()
 01766                        .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
 01767                        .Where(i => user.IsFolderGrouped(i.Id))
 01768                        .SelectMany(i => GetTopParentIdsForQuery(i, user));
 1769                }
 1770
 01771                return [];
 1772            }
 1773
 131774            if (item is CollectionFolder collectionFolder)
 1775            {
 131776                return collectionFolder.PhysicalFolderIds;
 1777            }
 1778
 01779            var topParent = item.GetTopParent();
 01780            if (topParent is not null)
 1781            {
 01782                return [topParent.Id];
 1783            }
 1784
 01785            return [];
 1786        }
 1787
 1788        /// <summary>
 1789        /// Gets the intros.
 1790        /// </summary>
 1791        /// <param name="item">The item.</param>
 1792        /// <param name="user">The user.</param>
 1793        /// <returns>IEnumerable{System.String}.</returns>
 1794        public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
 1795        {
 1796            if (IntroProviders.Length == 0)
 1797            {
 1798                return [];
 1799            }
 1800
 1801            var tasks = IntroProviders
 1802                .Select(i => GetIntros(i, item, user));
 1803
 1804            var items = await Task.WhenAll(tasks).ConfigureAwait(false);
 1805
 1806            return items
 1807                .SelectMany(i => i)
 1808                .Select(ResolveIntro)
 1809                .Where(i => i is not null)!; // null values got filtered out
 1810        }
 1811
 1812        /// <summary>
 1813        /// Gets the intros.
 1814        /// </summary>
 1815        /// <param name="provider">The provider.</param>
 1816        /// <param name="item">The item.</param>
 1817        /// <param name="user">The user.</param>
 1818        /// <returns>Task&lt;IEnumerable&lt;IntroInfo&gt;&gt;.</returns>
 1819        private async Task<IEnumerable<IntroInfo>> GetIntros(IIntroProvider provider, BaseItem item, User user)
 1820        {
 1821            try
 1822            {
 1823                return await provider.GetIntros(item, user).ConfigureAwait(false);
 1824            }
 1825            catch (Exception ex)
 1826            {
 1827                _logger.LogError(ex, "Error getting intros");
 1828
 1829                return [];
 1830            }
 1831        }
 1832
 1833        /// <summary>
 1834        /// Resolves the intro.
 1835        /// </summary>
 1836        /// <param name="info">The info.</param>
 1837        /// <returns>Video.</returns>
 1838        private Video? ResolveIntro(IntroInfo info)
 1839        {
 01840            Video? video = null;
 1841
 01842            if (info.ItemId.HasValue)
 1843            {
 1844                // Get an existing item by Id
 01845                video = GetItemById(info.ItemId.Value) as Video;
 1846
 01847                if (video is null)
 1848                {
 01849                    _logger.LogError("Unable to locate item with Id {ID}.", info.ItemId.Value);
 1850                }
 1851            }
 01852            else if (!string.IsNullOrEmpty(info.Path))
 1853            {
 1854                try
 1855                {
 1856                    // Try to resolve the path into a video
 01857                    video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video;
 1858
 01859                    if (video is null)
 1860                    {
 01861                        _logger.LogError("Intro resolver returned null for {Path}.", info.Path);
 1862                    }
 1863                    else
 1864                    {
 1865                        // Pull the saved db item that will include metadata
 01866                        var dbItem = GetItemById(video.Id) as Video;
 1867
 01868                        if (dbItem is not null)
 1869                        {
 01870                            video = dbItem;
 1871                        }
 1872                        else
 1873                        {
 01874                            return null;
 1875                        }
 1876                    }
 01877                }
 01878                catch (Exception ex)
 1879                {
 01880                    _logger.LogError(ex, "Error resolving path {Path}.", info.Path);
 01881                }
 1882            }
 1883            else
 1884            {
 01885                _logger.LogError("IntroProvider returned an IntroInfo with null Path and ItemId.");
 1886            }
 1887
 01888            return video;
 01889        }
 1890
 1891        /// <inheritdoc />
 1892        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortO
 1893        {
 11894            IOrderedEnumerable<BaseItem>? orderedItems = null;
 1895
 41896            foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null))
 1897            {
 11898                if (orderBy is RandomComparer)
 1899                {
 01900                    var randomItems = items.ToArray();
 01901                    Random.Shared.Shuffle(randomItems);
 01902                    items = randomItems;
 1903                    // Items are no longer ordered at this point, so set orderedItems back to null
 01904                    orderedItems = null;
 1905                }
 11906                else if (orderedItems is null)
 1907                {
 11908                    orderedItems = sortOrder == SortOrder.Descending
 11909                        ? items.OrderByDescending(i => i, orderBy)
 11910                        : items.OrderBy(i => i, orderBy);
 1911                }
 1912                else
 1913                {
 01914                    orderedItems = sortOrder == SortOrder.Descending
 01915                        ? orderedItems!.ThenByDescending(i => i, orderBy)
 01916                        : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration
 1917                }
 1918            }
 1919
 11920            return orderedItems ?? items;
 1921        }
 1922
 1923        /// <inheritdoc />
 1924        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, Sort
 1925        {
 01926            IOrderedEnumerable<BaseItem>? orderedItems = null;
 1927
 01928            foreach (var (name, sortOrder) in orderBy)
 1929            {
 01930                var comparer = GetComparer(name, user);
 01931                if (comparer is null)
 1932                {
 1933                    continue;
 1934                }
 1935
 01936                if (comparer is RandomComparer)
 1937                {
 01938                    var randomItems = items.ToArray();
 01939                    Random.Shared.Shuffle(randomItems);
 01940                    items = randomItems;
 1941                    // Items are no longer ordered at this point, so set orderedItems back to null
 01942                    orderedItems = null;
 1943                }
 01944                else if (orderedItems is null)
 1945                {
 01946                    orderedItems = sortOrder == SortOrder.Descending
 01947                        ? items.OrderByDescending(i => i, comparer)
 01948                        : items.OrderBy(i => i, comparer);
 1949                }
 1950                else
 1951                {
 01952                    orderedItems = sortOrder == SortOrder.Descending
 01953                        ? orderedItems!.ThenByDescending(i => i, comparer)
 01954                        : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration
 1955                }
 1956            }
 1957
 01958            return orderedItems ?? items;
 1959        }
 1960
 1961        /// <summary>
 1962        /// Gets the comparer.
 1963        /// </summary>
 1964        /// <param name="name">The name.</param>
 1965        /// <param name="user">The user.</param>
 1966        /// <returns>IBaseItemComparer.</returns>
 1967        private IBaseItemComparer? GetComparer(ItemSortBy name, User? user)
 1968        {
 11969            var comparer = Comparers.FirstOrDefault(c => name == c.Type);
 1970
 1971            // If it requires a user, create a new one, and assign the user
 11972            if (comparer is IUserBaseItemComparer)
 1973            {
 01974                var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null fo
 1975
 01976                userComparer.User = user;
 01977                userComparer.UserManager = _userManager;
 01978                userComparer.UserDataManager = _userDataManager;
 1979
 01980                return userComparer;
 1981            }
 1982
 11983            return comparer;
 1984        }
 1985
 1986        /// <inheritdoc />
 1987        public void CreateItem(BaseItem item, BaseItem? parent)
 1988        {
 01989            CreateItems([item], parent, CancellationToken.None);
 01990        }
 1991
 1992        /// <inheritdoc />
 1993        public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
 1994        {
 21995            _itemRepository.SaveItems(items, cancellationToken);
 1996
 81997            foreach (var item in items)
 1998            {
 21999                RegisterItem(item);
 2000            }
 2001
 22002            if (parent is Folder folder)
 2003            {
 22004                folder.Children = null;
 22005                folder.UserData = null;
 2006            }
 2007
 22008            if (ItemAdded is not null)
 2009            {
 82010                foreach (var item in items)
 2011                {
 2012                    // With the live tv guide this just creates too much noise
 22013                    if (item.SourceType != SourceType.Library)
 2014                    {
 2015                        continue;
 2016                    }
 2017
 2018                    try
 2019                    {
 22020                        ItemAdded(
 22021                            this,
 22022                            new ItemChangeEventArgs
 22023                            {
 22024                                Item = item,
 22025                                Parent = parent ?? item.GetParent()
 22026                            });
 22027                    }
 02028                    catch (Exception ex)
 2029                    {
 02030                        _logger.LogError(ex, "Error in ItemAdded event handler");
 02031                    }
 2032                }
 2033            }
 22034        }
 2035
 2036        private bool ImageNeedsRefresh(ItemImageInfo image)
 2037        {
 02038            if (image.Path is not null && image.IsLocalFile)
 2039            {
 02040                if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash))
 2041                {
 02042                    return true;
 2043                }
 2044
 2045                try
 2046                {
 02047                    return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeco
 2048                }
 02049                catch (Exception ex)
 2050                {
 02051                    _logger.LogError(ex, "Cannot get file info for {0}", image.Path);
 02052                    return false;
 2053                }
 2054            }
 2055
 02056            return image.Path is not null && !image.IsLocalFile;
 02057        }
 2058
 2059        /// <inheritdoc />
 2060        public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false)
 2061        {
 2062            ArgumentNullException.ThrowIfNull(item);
 2063
 2064            var outdated = forceUpdate
 2065                ? item.ImageInfos.Where(i => i.Path is not null).ToArray()
 2066                : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
 2067            // Skip image processing if current or live tv source
 2068            if (outdated.Length == 0 || item.SourceType != SourceType.Library)
 2069            {
 2070                RegisterItem(item);
 2071                return;
 2072            }
 2073
 2074            foreach (var img in outdated)
 2075            {
 2076                var image = img;
 2077                if (!img.IsLocalFile)
 2078                {
 2079                    try
 2080                    {
 2081                        var index = item.GetImageIndex(img);
 2082                        image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
 2083                    }
 2084                    catch (ArgumentException)
 2085                    {
 2086                        _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
 2087                        continue;
 2088                    }
 2089                    catch (Exception ex) when (ex is InvalidOperationException or IOException)
 2090                    {
 2091                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
 2092                        continue;
 2093                    }
 2094                    catch (HttpRequestException ex)
 2095                    {
 2096                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", im
 2097                        continue;
 2098                    }
 2099                }
 2100
 2101                if (!File.Exists(image.Path))
 2102                {
 2103                    _logger.LogWarning("Image not found at {ImagePath}", image.Path);
 2104                    continue;
 2105                }
 2106
 2107                ImageDimensions size;
 2108                try
 2109                {
 2110                    size = _imageProcessor.GetImageDimensions(item, image);
 2111                    image.Width = size.Width;
 2112                    image.Height = size.Height;
 2113                }
 2114                catch (Exception ex)
 2115                {
 2116                    _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
 2117                    size = default;
 2118                    image.Width = 0;
 2119                    image.Height = 0;
 2120                }
 2121
 2122                try
 2123                {
 2124                    var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size);
 2125                    image.BlurHash = blurhash;
 2126                }
 2127                catch (Exception ex)
 2128                {
 2129                    _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
 2130                    image.BlurHash = string.Empty;
 2131                }
 2132
 2133                try
 2134                {
 2135                    var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path);
 2136                    image.DateModified = modifiedDate;
 2137                }
 2138                catch (Exception ex)
 2139                {
 2140                    _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
 2141                }
 2142            }
 2143
 2144            item.ValidateImages();
 2145
 2146            _itemRepository.SaveImages(item);
 2147
 2148            RegisterItem(item);
 2149        }
 2150
 2151        /// <inheritdoc />
 2152        public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, 
 2153        {
 2154            foreach (var item in items)
 2155            {
 2156                item.DateLastSaved = DateTime.UtcNow;
 2157                await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
 2158
 2159                // Modify again, so saved value is after write time of externally saved metadata
 2160                item.DateLastSaved = DateTime.UtcNow;
 2161            }
 2162
 2163            _itemRepository.SaveItems(items, cancellationToken);
 2164
 2165            if (parent is Folder folder)
 2166            {
 2167                folder.Children = null;
 2168                folder.UserData = null;
 2169            }
 2170
 2171            if (ItemUpdated is not null)
 2172            {
 2173                foreach (var item in items)
 2174                {
 2175                    // With the live tv guide this just creates too much noise
 2176                    if (item.SourceType != SourceType.Library)
 2177                    {
 2178                        continue;
 2179                    }
 2180
 2181                    try
 2182                    {
 2183                        ItemUpdated(
 2184                            this,
 2185                            new ItemChangeEventArgs
 2186                            {
 2187                                Item = item,
 2188                                Parent = parent,
 2189                                UpdateReason = updateReason
 2190                            });
 2191                    }
 2192                    catch (Exception ex)
 2193                    {
 2194                        _logger.LogError(ex, "Error in ItemUpdated event handler");
 2195                    }
 2196                }
 2197            }
 2198        }
 2199
 2200        /// <inheritdoc />
 2201        public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cance
 782202            => UpdateItemsAsync([item], parent, updateReason, cancellationToken);
 2203
 2204        public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
 2205        {
 2206            if (item.IsFileProtocol)
 2207            {
 2208                await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
 2209            }
 2210
 2211            await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
 2212        }
 2213
 2214        /// <summary>
 2215        /// Reports the item removed.
 2216        /// </summary>
 2217        /// <param name="item">The item.</param>
 2218        /// <param name="parent">The parent item.</param>
 2219        public void ReportItemRemoved(BaseItem item, BaseItem parent)
 2220        {
 02221            if (ItemRemoved is not null)
 2222            {
 2223                try
 2224                {
 02225                    ItemRemoved(
 02226                        this,
 02227                        new ItemChangeEventArgs
 02228                        {
 02229                            Item = item,
 02230                            Parent = parent
 02231                        });
 02232                }
 02233                catch (Exception ex)
 2234                {
 02235                    _logger.LogError(ex, "Error in ItemRemoved event handler");
 02236                }
 2237            }
 02238        }
 2239
 2240        /// <summary>
 2241        /// Retrieves the item.
 2242        /// </summary>
 2243        /// <param name="id">The id.</param>
 2244        /// <returns>BaseItem.</returns>
 2245        public BaseItem RetrieveItem(Guid id)
 2246        {
 1032247            return _itemRepository.RetrieveItem(id);
 2248        }
 2249
 2250        public List<Folder> GetCollectionFolders(BaseItem item)
 2251        {
 6992252            return GetCollectionFolders(item, GetUserRootFolder().Children.OfType<Folder>());
 2253        }
 2254
 2255        public List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2256        {
 7332257            while (item is not null)
 2258            {
 7332259                var parent = item.GetParent();
 2260
 7332261                if (parent is AggregateFolder)
 2262                {
 2263                    break;
 2264                }
 2265
 6412266                if (parent is null)
 2267                {
 6072268                    var owner = item.GetOwner();
 2269
 6072270                    if (owner is null)
 2271                    {
 2272                        break;
 2273                    }
 2274
 02275                    item = owner;
 2276                }
 2277                else
 2278                {
 342279                    item = parent;
 2280                }
 2281            }
 2282
 6992283            if (item is null)
 2284            {
 02285                return new List<Folder>();
 2286            }
 2287
 6992288            return GetCollectionFoldersInternal(item, allUserRootChildren);
 2289        }
 2290
 2291        private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2292        {
 6992293            return allUserRootChildren
 6992294                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 6992295                .ToList();
 2296        }
 2297
 2298        public LibraryOptions GetLibraryOptions(BaseItem item)
 2299        {
 4702300            if (item is CollectionFolder collectionFolder)
 2301            {
 562302                return collectionFolder.GetLibraryOptions();
 2303            }
 2304
 2305            // List.Find is more performant than FirstOrDefault due to enumerator allocation
 4142306            return GetCollectionFolders(item)
 4142307                .Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2
 4142308                ? collectionFolder2.GetLibraryOptions()
 4142309                : new LibraryOptions();
 2310        }
 2311
 2312        public CollectionType? GetContentType(BaseItem item)
 2313        {
 582314            var configuredContentType = GetConfiguredContentType(item, false);
 582315            if (configuredContentType is not null)
 2316            {
 02317                return configuredContentType;
 2318            }
 2319
 582320            configuredContentType = GetConfiguredContentType(item, true);
 582321            if (configuredContentType is not null)
 2322            {
 02323                return configuredContentType;
 2324            }
 2325
 582326            return GetInheritedContentType(item);
 2327        }
 2328
 2329        public CollectionType? GetInheritedContentType(BaseItem item)
 2330        {
 582331            var type = GetTopFolderContentType(item);
 2332
 582333            if (type is not null)
 2334            {
 02335                return type;
 2336            }
 2337
 582338            return item.GetParents()
 582339                .Select(GetConfiguredContentType)
 582340                .LastOrDefault(i => i is not null);
 2341        }
 2342
 2343        public CollectionType? GetConfiguredContentType(BaseItem item)
 2344        {
 02345            return GetConfiguredContentType(item, false);
 2346        }
 2347
 2348        public CollectionType? GetConfiguredContentType(string path)
 2349        {
 02350            return GetContentTypeOverride(path, false);
 2351        }
 2352
 2353        public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
 2354        {
 1162355            if (item is ICollectionFolder collectionFolder)
 2356            {
 02357                return collectionFolder.CollectionType;
 2358            }
 2359
 1162360            return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
 2361        }
 2362
 2363        private CollectionType? GetContentTypeOverride(string path, bool inherit)
 2364        {
 1342365            var nameValuePair = _configurationManager.Configuration.ContentTypes
 1342366                                    .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
 1342367                                                         || (inherit && !string.IsNullOrEmpty(i.Name)
 1342368                                                                     && _fileSystem.ContainsSubPath(i.Name, path)));
 1342369            if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType))
 2370            {
 02371                return collectionType;
 2372            }
 2373
 1342374            return null;
 2375        }
 2376
 2377        private CollectionType? GetTopFolderContentType(BaseItem item)
 2378        {
 582379            if (item is null)
 2380            {
 02381                return null;
 2382            }
 2383
 582384            while (!item.ParentId.IsEmpty())
 2385            {
 02386                var parent = item.GetParent();
 02387                if (parent is null || parent is AggregateFolder)
 2388                {
 2389                    break;
 2390                }
 2391
 02392                item = parent;
 2393            }
 2394
 582395            return GetUserRootFolder().Children
 582396                .OfType<ICollectionFolder>()
 582397                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 582398                .Select(i => i.CollectionType)
 582399                .FirstOrDefault(i => i is not null);
 2400        }
 2401
 2402        public UserView GetNamedView(
 2403            User user,
 2404            string name,
 2405            CollectionType? viewType,
 2406            string sortName)
 2407        {
 02408            return GetNamedView(user, name, Guid.Empty, viewType, sortName);
 2409        }
 2410
 2411        public UserView GetNamedView(
 2412            string name,
 2413            CollectionType viewType,
 2414            string sortName)
 2415        {
 02416            var path = Path.Combine(
 02417                _configurationManager.ApplicationPaths.InternalMetadataPath,
 02418                "views",
 02419                _fileSystem.GetValidFilename(viewType.ToString()));
 2420
 02421            var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
 2422
 02423            var item = GetItemById(id) as UserView;
 2424
 02425            var refresh = false;
 2426
 02427            if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
 2428            {
 02429                var info = Directory.CreateDirectory(path);
 02430                item = new UserView
 02431                {
 02432                    Path = path,
 02433                    Id = id,
 02434                    DateCreated = info.CreationTimeUtc,
 02435                    DateModified = info.LastWriteTimeUtc,
 02436                    Name = name,
 02437                    ViewType = viewType,
 02438                    ForcedSortName = sortName
 02439                };
 2440
 02441                CreateItem(item, null);
 2442
 02443                refresh = true;
 2444            }
 2445
 02446            if (refresh)
 2447            {
 02448                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResu
 02449                ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), Ref
 2450            }
 2451
 02452            return item;
 2453        }
 2454
 2455        public UserView GetNamedView(
 2456            User user,
 2457            string name,
 2458            Guid parentId,
 2459            CollectionType? viewType,
 2460            string sortName)
 2461        {
 02462            var parentIdString = parentId.IsEmpty()
 02463                ? null
 02464                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02465            var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdStrin
 2466
 02467            var id = GetNewItemId(idValues, typeof(UserView));
 2468
 02469            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2470
 02471            var item = GetItemById(id) as UserView;
 2472
 02473            var isNew = false;
 2474
 02475            if (item is null)
 2476            {
 02477                var info = Directory.CreateDirectory(path);
 02478                item = new UserView
 02479                {
 02480                    Path = path,
 02481                    Id = id,
 02482                    DateCreated = info.CreationTimeUtc,
 02483                    DateModified = info.LastWriteTimeUtc,
 02484                    Name = name,
 02485                    ViewType = viewType,
 02486                    ForcedSortName = sortName,
 02487                    UserId = user.Id,
 02488                    DisplayParentId = parentId
 02489                };
 2490
 02491                CreateItem(item, null);
 2492
 02493                isNew = true;
 2494            }
 2495
 02496            var lastRefreshedUtc = item.DateLastRefreshed;
 02497            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2498
 02499            if (!refresh && !item.DisplayParentId.IsEmpty())
 2500            {
 02501                var displayParent = GetItemById(item.DisplayParentId);
 02502                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 2503            }
 2504
 02505            if (refresh)
 2506            {
 02507                ProviderManager.QueueRefresh(
 02508                    item.Id,
 02509                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02510                    {
 02511                        // Need to force save to increment DateLastSaved
 02512                        ForceSave = true
 02513                    },
 02514                    RefreshPriority.Normal);
 2515            }
 2516
 02517            return item;
 2518        }
 2519
 2520        public UserView GetShadowView(
 2521            BaseItem parent,
 2522            CollectionType? viewType,
 2523            string sortName)
 2524        {
 02525            ArgumentNullException.ThrowIfNull(parent);
 2526
 02527            var name = parent.Name;
 02528            var parentId = parent.Id;
 2529
 02530            var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
 2531
 02532            var id = GetNewItemId(idValues, typeof(UserView));
 2533
 02534            var path = parent.Path;
 2535
 02536            var item = GetItemById(id) as UserView;
 2537
 02538            var isNew = false;
 2539
 02540            if (item is null)
 2541            {
 02542                var info = Directory.CreateDirectory(path);
 02543                item = new UserView
 02544                {
 02545                    Path = path,
 02546                    Id = id,
 02547                    DateCreated = info.CreationTimeUtc,
 02548                    DateModified = info.LastWriteTimeUtc,
 02549                    Name = name,
 02550                    ViewType = viewType,
 02551                    ForcedSortName = sortName,
 02552                    DisplayParentId = parentId
 02553                };
 2554
 02555                CreateItem(item, null);
 2556
 02557                isNew = true;
 2558            }
 2559
 02560            var lastRefreshedUtc = item.DateLastRefreshed;
 02561            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2562
 02563            if (!refresh && !item.DisplayParentId.IsEmpty())
 2564            {
 02565                var displayParent = GetItemById(item.DisplayParentId);
 02566                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 2567            }
 2568
 02569            if (refresh)
 2570            {
 02571                ProviderManager.QueueRefresh(
 02572                    item.Id,
 02573                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02574                    {
 02575                        // Need to force save to increment DateLastSaved
 02576                        ForceSave = true
 02577                    },
 02578                    RefreshPriority.Normal);
 2579            }
 2580
 02581            return item;
 2582        }
 2583
 2584        public UserView GetNamedView(
 2585            string name,
 2586            Guid parentId,
 2587            CollectionType? viewType,
 2588            string sortName,
 2589            string uniqueId)
 2590        {
 02591            ArgumentException.ThrowIfNullOrEmpty(name);
 2592
 02593            var parentIdString = parentId.IsEmpty()
 02594                ? null
 02595                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02596            var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.E
 02597            if (!string.IsNullOrEmpty(uniqueId))
 2598            {
 02599                idValues += uniqueId;
 2600            }
 2601
 02602            var id = GetNewItemId(idValues, typeof(UserView));
 2603
 02604            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2605
 02606            var item = GetItemById(id) as UserView;
 2607
 02608            var isNew = false;
 2609
 02610            if (item is null)
 2611            {
 02612                var info = Directory.CreateDirectory(path);
 02613                item = new UserView
 02614                {
 02615                    Path = path,
 02616                    Id = id,
 02617                    DateCreated = info.CreationTimeUtc,
 02618                    DateModified = info.LastWriteTimeUtc,
 02619                    Name = name,
 02620                    ViewType = viewType,
 02621                    ForcedSortName = sortName,
 02622                    DisplayParentId = parentId
 02623                };
 2624
 02625                CreateItem(item, null);
 2626
 02627                isNew = true;
 2628            }
 2629
 02630            if (viewType != item.ViewType)
 2631            {
 02632                item.ViewType = viewType;
 02633                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult
 2634            }
 2635
 02636            var lastRefreshedUtc = item.DateLastRefreshed;
 02637            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2638
 02639            if (!refresh && !item.DisplayParentId.IsEmpty())
 2640            {
 02641                var displayParent = GetItemById(item.DisplayParentId);
 02642                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 2643            }
 2644
 02645            if (refresh)
 2646            {
 02647                ProviderManager.QueueRefresh(
 02648                    item.Id,
 02649                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02650                    {
 02651                        // Need to force save to increment DateLastSaved
 02652                        ForceSave = true
 02653                    },
 02654                    RefreshPriority.Normal);
 2655            }
 2656
 02657            return item;
 2658        }
 2659
 2660        public BaseItem GetParentItem(Guid? parentId, Guid? userId)
 2661        {
 32662            if (parentId.HasValue)
 2663            {
 02664                return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}"
 2665            }
 2666
 32667            if (!userId.IsNullOrEmpty())
 2668            {
 32669                return GetUserRootFolder();
 2670            }
 2671
 02672            return RootFolder;
 2673        }
 2674
 2675        /// <inheritdoc />
 2676        public void QueueLibraryScan()
 2677        {
 02678            _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
 02679        }
 2680
 2681        /// <inheritdoc />
 2682        public int? GetSeasonNumberFromPath(string path, Guid? parentId)
 2683        {
 02684            var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
 02685            return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
 2686        }
 2687
 2688        /// <inheritdoc />
 2689        public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
 2690        {
 02691            var series = episode.Series;
 02692            bool? isAbsoluteNaming = series is not null && string.Equals(series.DisplayOrder, "absolute", StringComparis
 02693            if (!isAbsoluteNaming.Value)
 2694            {
 2695                // In other words, no filter applied
 02696                isAbsoluteNaming = null;
 2697            }
 2698
 02699            var resolver = new EpisodeResolver(_namingOptions);
 2700
 02701            var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 2702
 02703            EpisodeInfo? episodeInfo = null;
 02704            if (episode.IsFileProtocol)
 2705            {
 02706                episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
 2707                // Resolve from parent folder if it's not the Season folder
 02708                var parent = episode.GetParent();
 02709                if (episodeInfo is null && parent.GetType() == typeof(Folder))
 2710                {
 02711                    episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming);
 02712                    if (episodeInfo is not null)
 2713                    {
 2714                        // add the container
 02715                        episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
 2716                    }
 2717                }
 2718            }
 2719
 02720            var changed = false;
 02721            if (episodeInfo is null)
 2722            {
 02723                return changed;
 2724            }
 2725
 02726            if (episodeInfo.IsByDate)
 2727            {
 02728                if (episode.IndexNumber.HasValue)
 2729                {
 02730                    episode.IndexNumber = null;
 02731                    changed = true;
 2732                }
 2733
 02734                if (episode.IndexNumberEnd.HasValue)
 2735                {
 02736                    episode.IndexNumberEnd = null;
 02737                    changed = true;
 2738                }
 2739
 02740                if (!episode.PremiereDate.HasValue)
 2741                {
 02742                    if (episodeInfo.Year.HasValue && episodeInfo.Month.HasValue && episodeInfo.Day.HasValue)
 2743                    {
 02744                        episode.PremiereDate = new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo
 2745                    }
 2746
 02747                    if (episode.PremiereDate.HasValue)
 2748                    {
 02749                        changed = true;
 2750                    }
 2751                }
 2752
 02753                if (!episode.ProductionYear.HasValue)
 2754                {
 02755                    episode.ProductionYear = episodeInfo.Year;
 2756
 02757                    if (episode.ProductionYear.HasValue)
 2758                    {
 02759                        changed = true;
 2760                    }
 2761                }
 2762            }
 2763            else
 2764            {
 02765                if (!episode.IndexNumber.HasValue || forceRefresh)
 2766                {
 02767                    if (episode.IndexNumber != episodeInfo.EpisodeNumber)
 2768                    {
 02769                        changed = true;
 2770                    }
 2771
 02772                    episode.IndexNumber = episodeInfo.EpisodeNumber;
 2773                }
 2774
 02775                if (!episode.IndexNumberEnd.HasValue || forceRefresh)
 2776                {
 02777                    if (episode.IndexNumberEnd != episodeInfo.EndingEpisodeNumber)
 2778                    {
 02779                        changed = true;
 2780                    }
 2781
 02782                    episode.IndexNumberEnd = episodeInfo.EndingEpisodeNumber;
 2783                }
 2784
 02785                if (!episode.ParentIndexNumber.HasValue || forceRefresh)
 2786                {
 02787                    if (episode.ParentIndexNumber != episodeInfo.SeasonNumber)
 2788                    {
 02789                        changed = true;
 2790                    }
 2791
 02792                    episode.ParentIndexNumber = episodeInfo.SeasonNumber;
 2793                }
 2794            }
 2795
 02796            if (!episode.ParentIndexNumber.HasValue)
 2797            {
 02798                var season = episode.Season;
 2799
 02800                if (season is not null)
 2801                {
 02802                    episode.ParentIndexNumber = season.IndexNumber;
 2803                }
 2804
 02805                if (episode.ParentIndexNumber.HasValue)
 2806                {
 02807                    changed = true;
 2808                }
 2809            }
 2810
 02811            return changed;
 2812        }
 2813
 2814        public ItemLookupInfo ParseName(string name)
 2815        {
 02816            var namingOptions = _namingOptions;
 02817            var result = VideoResolver.CleanDateTime(name, namingOptions);
 2818
 02819            return new ItemLookupInfo
 02820            {
 02821                Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName : result.Name
 02822                Year = result.Year
 02823            };
 2824        }
 2825
 2826        public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, ID
 2827        {
 2828            // Apply .ignore rules
 2829            var filtered = fileSystemChildren.Where(c => !DotIgnoreIgnoreRule.IsIgnored(c, owner)).ToList();
 2830            var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.Co
 2831            if (ownerVideoInfo is null)
 2832            {
 2833                yield break;
 2834            }
 2835
 2836            var count = filtered.Count;
 2837            for (var i = 0; i < count; i++)
 2838            {
 2839                var current = filtered[i];
 2840                if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
 2841                {
 2842                    var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
 2843                    var filesInSubFolderList = filesInSubFolder.ToList();
 2844
 2845                    bool subFolderIsMixedFolder = filesInSubFolderList.Count > 1;
 2846
 2847                    foreach (var file in filesInSubFolderList)
 2848                    {
 2849                        if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType))
 2850                        {
 2851                            continue;
 2852                        }
 2853
 2854                        var extra = GetExtra(file, extraType.Value, subFolderIsMixedFolder);
 2855                        if (extra is not null)
 2856                        {
 2857                            yield return extra;
 2858                        }
 2859                    }
 2860                }
 2861                else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo
 2862                {
 2863                    var extra = GetExtra(current, extraType.Value, false);
 2864                    if (extra is not null)
 2865                    {
 2866                        yield return extra;
 2867                    }
 2868                }
 2869            }
 2870
 2871            BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType, bool isInMixedFolder)
 2872            {
 2873                var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetReso
 2874                if (extra is not Video && extra is not Audio)
 2875                {
 2876                    return null;
 2877                }
 2878
 2879                // Try to retrieve it from the db. If we don't find it, use the resolved version
 2880                var itemById = GetItemById(extra.Id);
 2881                if (itemById is not null)
 2882                {
 2883                    extra = itemById;
 2884                }
 2885
 2886                // Only update extra type if it is more specific then the currently known extra type
 2887                if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown)
 2888                {
 2889                    extra.ExtraType = extraType;
 2890                }
 2891
 2892                extra.ParentId = Guid.Empty;
 2893                extra.OwnerId = owner.Id;
 2894                extra.IsInMixedFolder = isInMixedFolder;
 2895                return extra;
 2896            }
 2897        }
 2898
 2899        public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
 2900        {
 122901            foreach (var map in _configurationManager.Configuration.PathSubstitutions)
 2902            {
 02903                if (path.TryReplaceSubPath(map.From, map.To, out var newPath))
 2904                {
 02905                    return newPath;
 2906                }
 2907            }
 2908
 62909            return path;
 2910        }
 2911
 2912        public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
 2913        {
 02914            return _peopleRepository.GetPeople(query);
 2915        }
 2916
 2917        public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
 2918        {
 62919            if (item.SupportsPeople)
 2920            {
 02921                var people = GetPeople(new InternalPeopleQuery
 02922                {
 02923                    ItemId = item.Id
 02924                });
 2925
 02926                if (people.Count > 0)
 2927                {
 02928                    return people;
 2929                }
 2930            }
 2931
 62932            return [];
 2933        }
 2934
 2935        public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
 2936        {
 02937            return _peopleRepository.GetPeopleNames(query)
 02938            .Select(i =>
 02939            {
 02940                try
 02941                {
 02942                    return GetPerson(i);
 02943                }
 02944                catch (Exception ex)
 02945                {
 02946                    _logger.LogError(ex, "Error getting person");
 02947                    return null;
 02948                }
 02949            })
 02950            .Where(i => i is not null)
 02951            .Where(i => query.User is null || i!.IsVisible(query.User))
 02952            .ToList()!; // null values are filtered out
 2953        }
 2954
 2955        public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
 2956        {
 02957            return _peopleRepository.GetPeopleNames(query);
 2958        }
 2959
 2960        public void UpdatePeople(BaseItem item, List<PersonInfo> people)
 2961        {
 02962            UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
 02963        }
 2964
 2965        /// <inheritdoc />
 2966        public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellat
 2967        {
 2968            if (!item.SupportsPeople)
 2969            {
 2970                return;
 2971            }
 2972
 2973            if (people is not null)
 2974            {
 2975                people = people.Where(e => e is not null).ToArray();
 2976                _peopleRepository.UpdatePeople(item.Id, people);
 2977                await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
 2978            }
 2979        }
 2980
 2981        public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool re
 2982        {
 2983            foreach (var url in image.Path.Split('|'))
 2984            {
 2985                try
 2986                {
 2987                    _logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
 2988
 2989                    await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).Configure
 2990
 2991                    await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwai
 2992
 2993                    return item.GetImageInfo(image.Type, imageIndex);
 2994                }
 2995                catch (HttpRequestException ex)
 2996                {
 2997                    if (ex.StatusCode.HasValue
 2998                        && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forb
 2999                    {
 3000                        _logger.LogDebug(ex, "Error downloading image {Url}", url);
 3001                        continue;
 3002                    }
 3003
 3004                    throw;
 3005                }
 3006            }
 3007
 3008            if (removeOnFailure)
 3009            {
 3010                // Remove this image to prevent it from retrying over and over
 3011                item.RemoveImage(image);
 3012                await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(fa
 3013            }
 3014
 3015            throw new InvalidOperationException("Unable to convert any images to local");
 3016        }
 3017
 3018        public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, b
 3019        {
 3020            if (string.IsNullOrWhiteSpace(name))
 3021            {
 3022                throw new ArgumentNullException(nameof(name));
 3023            }
 3024
 3025            name = _fileSystem.GetValidFilename(name.Trim());
 3026
 3027            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3028
 3029            var existingNameCount = 1; // first numbered name will be 2
 3030            var virtualFolderPath = Path.Combine(rootFolderPath, name);
 3031            var originalName = name;
 3032            while (Directory.Exists(virtualFolderPath))
 3033            {
 3034                existingNameCount++;
 3035                name = originalName + existingNameCount;
 3036                virtualFolderPath = Path.Combine(rootFolderPath, name);
 3037            }
 3038
 3039            var mediaPathInfos = options.PathInfos;
 3040            if (mediaPathInfos is not null)
 3041            {
 3042                var invalidpath = mediaPathInfos.FirstOrDefault(i => !Directory.Exists(i.Path));
 3043                if (invalidpath is not null)
 3044                {
 3045                    throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + ".");
 3046                }
 3047            }
 3048
 3049            LibraryMonitor.Stop();
 3050
 3051            try
 3052            {
 3053                Directory.CreateDirectory(virtualFolderPath);
 3054
 3055                if (collectionType is not null)
 3056                {
 3057                    var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collect
 3058
 3059                    FileHelper.CreateEmpty(path);
 3060                }
 3061
 3062                CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
 3063
 3064                if (mediaPathInfos is not null)
 3065                {
 3066                    foreach (var path in mediaPathInfos)
 3067                    {
 3068                        AddMediaPathInternal(name, path, false);
 3069                    }
 3070                }
 3071            }
 3072            finally
 3073            {
 3074                await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
 3075
 3076                if (refreshLibrary)
 3077                {
 3078                    StartScanInBackground();
 3079                }
 3080                else
 3081                {
 3082                    // Need to add a delay here or directory watchers may still pick up the changes
 3083                    await Task.Delay(1000).ConfigureAwait(false);
 3084                    LibraryMonitor.Start();
 3085                }
 3086            }
 3087        }
 3088
 3089        private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
 3090        {
 3091            foreach (var person in people)
 3092            {
 3093                cancellationToken.ThrowIfCancellationRequested();
 3094
 3095                var itemUpdateType = ItemUpdateType.MetadataDownload;
 3096                var saveEntity = false;
 3097                var createEntity = false;
 3098                var personEntity = GetPerson(person.Name);
 3099
 3100                if (personEntity is null)
 3101                {
 3102                    try
 3103                    {
 3104                        var path = Person.GetPath(person.Name);
 3105                        var info = Directory.CreateDirectory(path);
 3106                        personEntity = new Person()
 3107                        {
 3108                            Name = person.Name,
 3109                            Id = GetItemByNameId<Person>(path),
 3110                            DateCreated = info.CreationTimeUtc,
 3111                            DateModified = info.LastWriteTimeUtc,
 3112                            Path = path
 3113                        };
 3114
 3115                        personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
 3116                        saveEntity = true;
 3117                        createEntity = true;
 3118                    }
 3119                    catch (Exception ex)
 3120                    {
 3121                        _logger.LogWarning(ex, "Failed to create person {Name}", person.Name);
 3122                        continue;
 3123                    }
 3124                }
 3125
 3126                foreach (var id in person.ProviderIds)
 3127                {
 3128                    if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)
 3129                    {
 3130                        personEntity.SetProviderId(id.Key, id.Value);
 3131                        saveEntity = true;
 3132                    }
 3133                }
 3134
 3135                if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
 3136                {
 3137                    personEntity.SetImage(
 3138                        new ItemImageInfo
 3139                        {
 3140                            Path = person.ImageUrl,
 3141                            Type = ImageType.Primary
 3142                        },
 3143                        0);
 3144
 3145                    saveEntity = true;
 3146                    itemUpdateType = ItemUpdateType.ImageUpdate;
 3147                }
 3148
 3149                if (saveEntity)
 3150                {
 3151                    if (createEntity)
 3152                    {
 3153                        CreateItems([personEntity], null, CancellationToken.None);
 3154                    }
 3155
 3156                    await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
 3157                    personEntity.DateLastSaved = DateTime.UtcNow;
 3158
 3159                    CreateItems([personEntity], null, CancellationToken.None);
 3160                }
 3161            }
 3162        }
 3163
 3164        private void StartScanInBackground()
 3165        {
 33166            Task.Run(() =>
 33167            {
 33168                // No need to start if scanning the library because it will handle it
 33169                ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
 33170            });
 33171        }
 3172
 3173        public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3174        {
 13175            AddMediaPathInternal(virtualFolderName, mediaPath, true);
 03176        }
 3177
 3178        private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
 3179        {
 13180            ArgumentNullException.ThrowIfNull(pathInfo);
 3181
 13182            var path = pathInfo.Path;
 3183
 13184            if (string.IsNullOrWhiteSpace(path))
 3185            {
 03186                throw new ArgumentException(nameof(path));
 3187            }
 3188
 13189            if (!Directory.Exists(path))
 3190            {
 13191                throw new FileNotFoundException("The path does not exist.");
 3192            }
 3193
 03194            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03195            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3196
 03197            var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 3198
 03199            var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3200
 03201            while (File.Exists(lnk))
 3202            {
 03203                shortcutFilename += "1";
 03204                lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3205            }
 3206
 03207            _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
 3208
 03209            RemoveContentTypeOverrides(path);
 3210
 03211            if (saveLibraryOptions)
 3212            {
 03213                var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3214
 03215                libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
 3216
 03217                SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3218
 03219                CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 3220            }
 03221        }
 3222
 3223        public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3224        {
 03225            ArgumentNullException.ThrowIfNull(mediaPath);
 3226
 03227            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03228            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3229
 03230            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3231
 03232            SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3233
 03234            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03235        }
 3236
 3237        private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options)
 3238        {
 03239            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 03240            var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders, null);
 3241
 03242            if (info.Locations.Length > 0 && info.Locations.Length != options.PathInfos.Length)
 3243            {
 03244                var list = options.PathInfos.ToList();
 3245
 03246                foreach (var location in info.Locations)
 3247                {
 03248                    if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
 3249                    {
 03250                        list.Add(new MediaPathInfo(location));
 3251                    }
 3252                }
 3253
 03254                options.PathInfos = list.ToArray();
 3255            }
 03256        }
 3257
 3258        public async Task RemoveVirtualFolder(string name, bool refreshLibrary)
 3259        {
 3260            if (string.IsNullOrWhiteSpace(name))
 3261            {
 3262                throw new ArgumentNullException(nameof(name));
 3263            }
 3264
 3265            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3266
 3267            var path = Path.Combine(rootFolderPath, name);
 3268
 3269            if (!Directory.Exists(path))
 3270            {
 3271                throw new FileNotFoundException("The media folder does not exist");
 3272            }
 3273
 3274            LibraryMonitor.Stop();
 3275
 3276            try
 3277            {
 3278                Directory.Delete(path, true);
 3279            }
 3280            finally
 3281            {
 3282                CollectionFolder.OnCollectionFolderChange();
 3283
 3284                if (refreshLibrary)
 3285                {
 3286                    await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
 3287
 3288                    StartScanInBackground();
 3289                }
 3290                else
 3291                {
 3292                    // Need to add a delay here or directory watchers may still pick up the changes
 3293                    await Task.Delay(1000).ConfigureAwait(false);
 3294                    LibraryMonitor.Start();
 3295                }
 3296            }
 3297        }
 3298
 3299        private void RemoveContentTypeOverrides(string path)
 3300        {
 03301            if (string.IsNullOrWhiteSpace(path))
 3302            {
 03303                throw new ArgumentNullException(nameof(path));
 3304            }
 3305
 03306            List<NameValuePair>? removeList = null;
 3307
 03308            foreach (var contentType in _configurationManager.Configuration.ContentTypes)
 3309            {
 03310                if (string.IsNullOrWhiteSpace(contentType.Name)
 03311                    || _fileSystem.AreEqual(path, contentType.Name)
 03312                    || _fileSystem.ContainsSubPath(path, contentType.Name))
 3313                {
 03314                    (removeList ??= new()).Add(contentType);
 3315                }
 3316            }
 3317
 03318            if (removeList is not null)
 3319            {
 03320                _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
 03321                    .Except(removeList)
 03322                    .ToArray();
 3323
 03324                _configurationManager.SaveConfiguration();
 3325            }
 03326        }
 3327
 3328        public void RemoveMediaPath(string virtualFolderName, string mediaPath)
 3329        {
 13330            ArgumentException.ThrowIfNullOrEmpty(mediaPath);
 3331
 13332            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 13333            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3334
 13335            if (!Directory.Exists(virtualFolderPath))
 3336            {
 13337                throw new FileNotFoundException(
 13338                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolder
 3339            }
 3340
 03341            var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
 03342                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 03343                .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, String
 3344
 03345            if (!string.IsNullOrEmpty(shortcut))
 3346            {
 03347                _fileSystem.DeleteFile(shortcut);
 3348            }
 3349
 03350            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3351
 03352            libraryOptions.PathInfos = libraryOptions
 03353                .PathInfos
 03354                .Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal))
 03355                .ToArray();
 3356
 03357            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03358        }
 3359
 3360        private static bool ItemIsVisible(BaseItem? item, User? user)
 3361        {
 213362            if (item is null)
 3363            {
 213364                return false;
 3365            }
 3366
 03367            if (user is null)
 3368            {
 03369                return true;
 3370            }
 3371
 03372            return item is UserRootFolder || item.IsVisibleStandalone(user);
 3373        }
 3374    }
 3375}

Methods/Properties

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