< 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: 380
Uncovered lines: 727
Coverable lines: 1107
Total lines: 3357
Line coverage: 34.3%
Branch coverage
27%
Covered branches: 175
Total branches: 626
Branch coverage: 27.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_RootFolder()100%44100%
get_LibraryMonitor()100%11100%
get_ProviderManager()100%11100%
get_UserViewManager()100%11100%
AddParts(...)100%11100%
RecordConfigurationValues(...)100%11100%
ConfigurationUpdated(...)100%22100%
RegisterItem(...)50%191055.55%
DeleteItem(...)100%210%
DeleteItem(...)0%620%
DeleteItemsUnsafeFast(...)0%110100%
DeleteItem(...)0%1056320%
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(...)90%101083.33%
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
 113205        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        {
 182296            ArgumentNullException.ThrowIfNull(item);
 297
 182298            if (item is IItemByName)
 299            {
 0300                if (item is not MusicArtist)
 301                {
 0302                    return;
 303                }
 304            }
 182305            else if (!item.IsFolder)
 306            {
 0307                if (item is not Video && item is not LiveTvChannel)
 308                {
 0309                    return;
 310                }
 311            }
 312
 182313            _cache.AddOrUpdate(item.Id, item);
 182314        }
 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            ReportItemRemoved(item, parent);
 0461        }
 462
 463        private void DeleteItemPath(BaseItem item, bool isRequiredForDelete, FileSystemMetadata fileSystemInfo)
 464        {
 0465            if (Directory.Exists(fileSystemInfo.FullName) || File.Exists(fileSystemInfo.FullName))
 466            {
 467                try
 468                {
 0469                    _logger.LogInformation(
 0470                        "Deleting item path, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}",
 0471                        item.GetType().Name,
 0472                        item.Name ?? "Unknown name",
 0473                        fileSystemInfo.FullName,
 0474                        item.Id);
 475
 0476                    if (fileSystemInfo.IsDirectory)
 477                    {
 0478                        Directory.Delete(fileSystemInfo.FullName, true);
 479                    }
 480                    else
 481                    {
 0482                        File.Delete(fileSystemInfo.FullName);
 483                    }
 0484                }
 0485                catch (DirectoryNotFoundException)
 486                {
 0487                    _logger.LogInformation(
 0488                        "Directory not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id:
 0489                        item.GetType().Name,
 0490                        item.Name ?? "Unknown name",
 0491                        fileSystemInfo.FullName,
 0492                        item.Id);
 0493                }
 0494                catch (FileNotFoundException)
 495                {
 0496                    _logger.LogInformation(
 0497                        "File not found, only removing from database, Type: {Type}, Name: {Name}, Path: {Path}, Id: {Id}
 0498                        item.GetType().Name,
 0499                        item.Name ?? "Unknown name",
 0500                        fileSystemInfo.FullName,
 0501                        item.Id);
 0502                }
 0503                catch (IOException)
 504                {
 0505                    if (isRequiredForDelete)
 506                    {
 0507                        throw;
 508                    }
 0509                }
 0510                catch (UnauthorizedAccessException)
 511                {
 0512                    if (isRequiredForDelete)
 513                    {
 0514                        throw;
 515                    }
 0516                }
 517            }
 0518        }
 519
 520        private bool IsInternalItem(BaseItem item)
 521        {
 0522            if (!item.IsFileProtocol)
 523            {
 0524                return false;
 525            }
 526
 0527            var pathToCheck = item switch
 0528            {
 0529                Genre => _configurationManager.ApplicationPaths.GenrePath,
 0530                MusicArtist => _configurationManager.ApplicationPaths.ArtistsPath,
 0531                MusicGenre => _configurationManager.ApplicationPaths.MusicGenrePath,
 0532                Person => _configurationManager.ApplicationPaths.PeoplePath,
 0533                Studio => _configurationManager.ApplicationPaths.StudioPath,
 0534                Year => _configurationManager.ApplicationPaths.YearPath,
 0535                _ => null
 0536            };
 537
 0538            var itemPath = item.Path;
 0539            if (!string.IsNullOrEmpty(pathToCheck) && !string.IsNullOrEmpty(itemPath))
 540            {
 0541                var cleanPath = _fileSystem.GetValidFilename(itemPath);
 0542                var cleanCheckPath = _fileSystem.GetValidFilename(pathToCheck);
 543
 0544                return cleanPath.StartsWith(cleanCheckPath, StringComparison.Ordinal);
 545            }
 546
 0547            return false;
 548        }
 549
 550        private List<string> GetMetadataPaths(BaseItem item, IEnumerable<BaseItem> children)
 551        {
 0552            var list = GetInternalMetadataPaths(item);
 0553            foreach (var child in children)
 554            {
 0555                list.AddRange(GetInternalMetadataPaths(child));
 556            }
 557
 0558            return list;
 559        }
 560
 561        private List<string> GetInternalMetadataPaths(BaseItem item)
 562        {
 0563            var list = new List<string>
 0564            {
 0565                item.GetInternalMetadataPath()
 0566            };
 567
 0568            if (item is Video video)
 569            {
 570                // Trickplay
 0571                list.Add(_pathManager.GetTrickplayDirectory(video));
 572
 573                // Subtitles and attachments
 0574                foreach (var mediaSource in item.GetMediaSources(false))
 575                {
 0576                    var subtitleFolder = _pathManager.GetSubtitleFolderPath(mediaSource.Id);
 0577                    if (subtitleFolder is not null)
 578                    {
 0579                        list.Add(subtitleFolder);
 580                    }
 581
 0582                    var attachmentFolder = _pathManager.GetAttachmentFolderPath(mediaSource.Id);
 0583                    if (attachmentFolder is not null)
 584                    {
 0585                        list.Add(attachmentFolder);
 586                    }
 587                }
 588            }
 589
 0590            return list;
 591        }
 592
 593        /// <summary>
 594        /// Resolves the item.
 595        /// </summary>
 596        /// <param name="args">The args.</param>
 597        /// <param name="resolvers">The resolvers.</param>
 598        /// <returns>BaseItem.</returns>
 599        private BaseItem? ResolveItem(ItemResolveArgs args, IItemResolver[]? resolvers)
 600        {
 77601            var item = (resolvers ?? EntityResolvers).Select(r => Resolve(args, r))
 77602                .FirstOrDefault(i => i is not null);
 603
 77604            if (item is not null)
 605            {
 67606                ResolverHelper.SetInitialItemValues(item, args, _fileSystem, this);
 607            }
 608
 77609            return item;
 610        }
 611
 612        private BaseItem? Resolve(ItemResolveArgs args, IItemResolver resolver)
 613        {
 614            try
 615            {
 307616                return resolver.ResolvePath(args);
 617            }
 0618            catch (Exception ex)
 619            {
 0620                _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path);
 0621                return null;
 622            }
 307623        }
 624
 625        public Guid GetNewItemId(string key, Type type)
 626        {
 130627            return GetNewItemIdInternal(key, type, false);
 628        }
 629
 630        private Guid GetNewItemIdInternal(string key, Type type, bool forceCaseInsensitive)
 631        {
 131632            ArgumentException.ThrowIfNullOrEmpty(key);
 131633            ArgumentNullException.ThrowIfNull(type);
 634
 131635            string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath;
 131636            if (key.StartsWith(programDataPath, StringComparison.Ordinal))
 637            {
 638                // Try to normalize paths located underneath program-data in an attempt to make them more portable
 114639                key = key.Substring(programDataPath.Length)
 114640                    .TrimStart('/', '\\')
 114641                    .Replace('/', '\\');
 642            }
 643
 131644            if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
 645            {
 1646                key = key.ToLowerInvariant();
 647            }
 648
 131649            key = type.FullName + key;
 650
 131651            return key.GetMD5();
 652        }
 653
 654        public BaseItem? ResolvePath(FileSystemMetadata fileInfo, Folder? parent = null, IDirectoryService? directorySer
 42655            => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
 656
 657        private BaseItem? ResolvePath(
 658            FileSystemMetadata fileInfo,
 659            IDirectoryService directoryService,
 660            IItemResolver[]? resolvers,
 661            Folder? parent = null,
 662            CollectionType? collectionType = null,
 663            LibraryOptions? libraryOptions = null)
 664        {
 77665            ArgumentNullException.ThrowIfNull(fileInfo);
 666
 77667            var fullPath = fileInfo.FullName;
 668
 77669            if (collectionType is null && parent is not null)
 670            {
 18671                collectionType = GetContentTypeOverride(fullPath, true);
 672            }
 673
 77674            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
 77675            {
 77676                Parent = parent,
 77677                FileInfo = fileInfo,
 77678                CollectionType = collectionType,
 77679                LibraryOptions = libraryOptions
 77680            };
 681
 682            // Return null if ignore rules deem that we should do so
 77683            if (IgnoreFile(args.FileInfo, args.Parent))
 684            {
 0685                return null;
 686            }
 687
 688            // Gather child folder and files
 77689            if (args.IsDirectory)
 690            {
 50691                var isPhysicalRoot = args.IsPhysicalRoot;
 692
 693                // When resolving the root, we need it's grandchildren (children of user views)
 50694                var flattenFolderDepth = isPhysicalRoot ? 2 : 0;
 695
 696                FileSystemMetadata[] files;
 50697                var isVf = args.IsVf;
 698
 699                try
 700                {
 50701                    files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, _fileSystem, _appHost, _l
 50702                }
 0703                catch (Exception ex)
 704                {
 0705                    if (parent is not null && parent.IsPhysicalRoot)
 706                    {
 0707                        _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPh
 708
 0709                        files = [];
 710                    }
 711                    else
 712                    {
 0713                        throw;
 714                    }
 0715                }
 716
 717                // Need to remove sub-paths that may have been resolved from shortcuts
 718                // Example: if \\server\movies exists, then strip out \\server\movies\action
 50719                if (isPhysicalRoot)
 720                {
 21721                    files = NormalizeRootPathList(files).ToArray();
 722                }
 723
 50724                args.FileSystemChildren = files;
 725            }
 726
 727            // Filter content based on ignore rules
 77728            if (args.IsDirectory)
 729            {
 50730                var filtered = args.GetActualFileSystemChildren().ToArray();
 50731                args.FileSystemChildren = filtered ?? [];
 732            }
 733
 77734            return ResolveItem(args, resolvers);
 735        }
 736
 737        public bool IgnoreFile(FileSystemMetadata file, BaseItem? parent)
 103738            => EntityResolutionIgnoreRules.Any(r => r.ShouldIgnore(file, parent));
 739
 740        public List<FileSystemMetadata> NormalizeRootPathList(IEnumerable<FileSystemMetadata> paths)
 741        {
 80742            var originalList = paths.ToList();
 743
 80744            var list = originalList.Where(i => i.IsDirectory)
 80745                .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
 80746                .Distinct()
 80747                .ToList();
 748
 80749            var dupes = list.Where(subPath => !subPath.EndsWith(":\\", StringComparison.Ordinal) && list.Any(i => _fileS
 80750                .ToList();
 751
 160752            foreach (var dupe in dupes)
 753            {
 0754                _logger.LogInformation("Found duplicate path: {0}", dupe);
 755            }
 756
 80757            var newList = list.Except(dupes, StringComparer.Ordinal).Select(_fileSystem.GetDirectoryInfo).ToList();
 80758            newList.AddRange(originalList.Where(i => !i.IsDirectory));
 80759            return newList;
 760        }
 761
 762        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryServ
 763        {
 59764            return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
 765        }
 766
 767        public IEnumerable<BaseItem> ResolvePaths(
 768            IEnumerable<FileSystemMetadata> files,
 769            IDirectoryService directoryService,
 770            Folder parent,
 771            LibraryOptions libraryOptions,
 772            CollectionType? collectionType,
 773            IItemResolver[] resolvers)
 774        {
 59775            var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
 776
 59777            if (parent is not null)
 778            {
 59779                var multiItemResolvers = resolvers is null ? MultiItemResolvers : resolvers.OfType<IMultiItemResolver>()
 780
 354781                foreach (var resolver in multiItemResolvers)
 782                {
 118783                    var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
 784
 118785                    if (result?.Items.Count > 0)
 786                    {
 0787                        var items = result.Items;
 0788                        items.RemoveAll(item => !ResolverHelper.SetInitialItemValues(item, parent, this, directoryServic
 0789                        items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, reso
 0790                        return items;
 791                    }
 792                }
 793            }
 794
 59795            return ResolveFileList(fileList, directoryService, parent, collectionType, resolvers, libraryOptions);
 0796        }
 797
 798        private IEnumerable<BaseItem> ResolveFileList(
 799            IReadOnlyList<FileSystemMetadata> fileList,
 800            IDirectoryService directoryService,
 801            Folder? parent,
 802            CollectionType? collectionType,
 803            IItemResolver[]? resolvers,
 804            LibraryOptions libraryOptions)
 805        {
 806            // Given that fileList is a list we can save enumerator allocations by indexing
 807            for (var i = 0; i < fileList.Count; i++)
 808            {
 809                var file = fileList[i];
 810                BaseItem? result = null;
 811                try
 812                {
 813                    result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
 814                }
 815                catch (Exception ex)
 816                {
 817                    _logger.LogError(ex, "Error resolving path {Path}", file.FullName);
 818                }
 819
 820                if (result is not null)
 821                {
 822                    yield return result;
 823                }
 824            }
 825        }
 826
 827        /// <summary>
 828        /// Creates the root media folder.
 829        /// </summary>
 830        /// <returns>AggregateFolder.</returns>
 831        /// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</except
 832        public AggregateFolder CreateRootFolder()
 833        {
 21834            var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath;
 835
 21836            var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ??
 21837                             (ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath)) as Folder ?? throw new InvalidOp
 21838                             .DeepCopy<Folder, AggregateFolder>();
 839
 840            // In case program data folder was moved
 21841            if (!string.Equals(rootFolder.Path, rootFolderPath, StringComparison.Ordinal))
 842            {
 0843                _logger.LogInformation("Resetting root folder path to {0}", rootFolderPath);
 0844                rootFolder.Path = rootFolderPath;
 845            }
 846
 847            // Add in the plug-in folders
 21848            var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
 849
 21850            var info = Directory.CreateDirectory(path);
 21851            Folder folder = new PlaylistsFolder
 21852            {
 21853                Path = path,
 21854                DateCreated = info.CreationTimeUtc,
 21855                DateModified = info.LastWriteTimeUtc,
 21856            };
 857
 21858            if (folder.Id.IsEmpty())
 859            {
 21860                folder.Id = GetNewItemId(folder.Path, folder.GetType());
 861            }
 862
 21863            var dbItem = GetItemById(folder.Id) as BasePluginFolder;
 864
 21865            if (dbItem is not null && string.Equals(dbItem.Path, folder.Path, StringComparison.OrdinalIgnoreCase))
 866            {
 0867                folder = dbItem;
 868            }
 869
 21870            if (!folder.ParentId.Equals(rootFolder.Id))
 871            {
 21872                rootFolder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().G
 21873                folder.ParentId = rootFolder.Id;
 21874                folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetRe
 875            }
 876
 21877            rootFolder.AddVirtualChild(folder);
 878
 21879            RegisterItem(folder);
 880
 21881            return rootFolder;
 882        }
 883
 884        public Folder GetUserRootFolder()
 885        {
 914886            if (_userRootFolder is null)
 21887            {
 888                lock (_userRootFolderSyncLock)
 889                {
 21890                    if (_userRootFolder is null)
 891                    {
 21892                        var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 893
 21894                        _logger.LogDebug("Creating userRootPath at {Path}", userRootPath);
 21895                        Directory.CreateDirectory(userRootPath);
 896
 21897                        var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
 21898                        UserRootFolder? tmpItem = null;
 899                        try
 900                        {
 21901                            tmpItem = GetItemById(newItemId) as UserRootFolder;
 21902                        }
 0903                        catch (Exception ex)
 904                        {
 0905                            _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId);
 0906                        }
 907
 21908                        if (tmpItem is null)
 909                        {
 21910                            _logger.LogDebug("Creating new userRootFolder with DeepCopy");
 21911                            tmpItem = (ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath)) as Folder ?? throw new In
 21912                                        .DeepCopy<Folder, UserRootFolder>();
 913                        }
 914
 915                        // In case program data folder was moved
 21916                        if (!string.Equals(tmpItem.Path, userRootPath, StringComparison.Ordinal))
 917                        {
 0918                            _logger.LogInformation("Resetting user root folder path to {0}", userRootPath);
 0919                            tmpItem.Path = userRootPath;
 920                        }
 921
 21922                        _userRootFolder = tmpItem;
 21923                        _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder);
 924                    }
 21925                }
 926            }
 927
 914928            return _userRootFolder;
 929        }
 930
 931        /// <inheritdoc />
 932        public BaseItem? FindByPath(string path, bool? isFolder)
 933        {
 934            // If this returns multiple items it could be tricky figuring out which one is correct.
 935            // In most cases, the newest one will be and the others obsolete but not yet cleaned up
 0936            ArgumentException.ThrowIfNullOrEmpty(path);
 937
 0938            var query = new InternalItemsQuery
 0939            {
 0940                Path = path,
 0941                IsFolder = isFolder,
 0942                OrderBy = [(ItemSortBy.DateCreated, SortOrder.Descending)],
 0943                Limit = 1,
 0944                DtoOptions = new DtoOptions(true)
 0945            };
 946
 0947            return GetItemList(query)
 0948                .FirstOrDefault();
 949        }
 950
 951        /// <inheritdoc />
 952        public Person? GetPerson(string name)
 953        {
 1954            var path = Person.GetPath(name);
 1955            var id = GetItemByNameId<Person>(path);
 1956            if (GetItemById(id) is Person item)
 957            {
 0958                return item;
 959            }
 960
 1961            return null;
 962        }
 963
 964        /// <summary>
 965        /// Gets the studio.
 966        /// </summary>
 967        /// <param name="name">The name.</param>
 968        /// <returns>Task{Studio}.</returns>
 969        public Studio GetStudio(string name)
 970        {
 0971            return CreateItemByName<Studio>(Studio.GetPath, name, new DtoOptions(true));
 972        }
 973
 974        public Guid GetStudioId(string name)
 975        {
 0976            return GetItemByNameId<Studio>(Studio.GetPath(name));
 977        }
 978
 979        public Guid GetGenreId(string name)
 980        {
 0981            return GetItemByNameId<Genre>(Genre.GetPath(name));
 982        }
 983
 984        public Guid GetMusicGenreId(string name)
 985        {
 0986            return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name));
 987        }
 988
 989        /// <summary>
 990        /// Gets the genre.
 991        /// </summary>
 992        /// <param name="name">The name.</param>
 993        /// <returns>Task{Genre}.</returns>
 994        public Genre GetGenre(string name)
 995        {
 0996            return CreateItemByName<Genre>(Genre.GetPath, name, new DtoOptions(true));
 997        }
 998
 999        /// <summary>
 1000        /// Gets the music genre.
 1001        /// </summary>
 1002        /// <param name="name">The name.</param>
 1003        /// <returns>Task{MusicGenre}.</returns>
 1004        public MusicGenre GetMusicGenre(string name)
 1005        {
 01006            return CreateItemByName<MusicGenre>(MusicGenre.GetPath, name, new DtoOptions(true));
 1007        }
 1008
 1009        /// <summary>
 1010        /// Gets the year.
 1011        /// </summary>
 1012        /// <param name="value">The value.</param>
 1013        /// <returns>Task{Year}.</returns>
 1014        public Year GetYear(int value)
 1015        {
 01016            if (value <= 0)
 1017            {
 01018                throw new ArgumentOutOfRangeException(nameof(value), "Years less than or equal to 0 are invalid.");
 1019            }
 1020
 01021            var name = value.ToString(CultureInfo.InvariantCulture);
 1022
 01023            return CreateItemByName<Year>(Year.GetPath, name, new DtoOptions(true));
 1024        }
 1025
 1026        /// <summary>
 1027        /// Gets a Genre.
 1028        /// </summary>
 1029        /// <param name="name">The name.</param>
 1030        /// <returns>Task{Genre}.</returns>
 1031        public MusicArtist GetArtist(string name)
 1032        {
 01033            return GetArtist(name, new DtoOptions(true));
 1034        }
 1035
 1036        public IReadOnlyDictionary<string, MusicArtist[]> GetArtists(IReadOnlyList<string> names)
 1037        {
 01038            return _itemRepository.FindArtists(names);
 1039        }
 1040
 1041        public MusicArtist GetArtist(string name, DtoOptions options)
 1042        {
 01043            return CreateItemByName<MusicArtist>(MusicArtist.GetPath, name, options);
 1044        }
 1045
 1046        private T CreateItemByName<T>(Func<string, string> getPathFn, string name, DtoOptions options)
 1047            where T : BaseItem, new()
 1048        {
 01049            if (typeof(T) == typeof(MusicArtist))
 1050            {
 01051                var existing = GetItemList(new InternalItemsQuery
 01052                {
 01053                    IncludeItemTypes = [BaseItemKind.MusicArtist],
 01054                    Name = name,
 01055                    DtoOptions = options
 01056                }).Cast<MusicArtist>()
 01057                .OrderBy(i => i.IsAccessedByName ? 1 : 0)
 01058                .Cast<T>()
 01059                .FirstOrDefault();
 1060
 01061                if (existing is not null)
 1062                {
 01063                    return existing;
 1064                }
 1065            }
 1066
 01067            var path = getPathFn(name);
 01068            var id = GetItemByNameId<T>(path);
 01069            var item = GetItemById(id) as T;
 01070            if (item is null)
 1071            {
 01072                var info = Directory.CreateDirectory(path);
 01073                item = new T
 01074                {
 01075                    Name = name,
 01076                    Id = id,
 01077                    DateCreated = info.CreationTimeUtc,
 01078                    DateModified = info.LastWriteTimeUtc,
 01079                    Path = path
 01080                };
 1081
 01082                CreateItem(item, null);
 1083            }
 1084
 01085            return item;
 1086        }
 1087
 1088        private Guid GetItemByNameId<T>(string path)
 1089              where T : BaseItem, new()
 1090        {
 11091            var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
 11092            return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
 1093        }
 1094
 1095        /// <inheritdoc />
 1096        public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken)
 1097        {
 1098            // Ensure the location is available.
 01099            Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
 1100
 01101            return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
 1102        }
 1103
 1104        /// <summary>
 1105        /// Reloads the root media folder.
 1106        /// </summary>
 1107        /// <param name="progress">The progress.</param>
 1108        /// <param name="cancellationToken">The cancellation token.</param>
 1109        /// <returns>Task.</returns>
 1110        public Task ValidateMediaLibrary(IProgress<double> progress, CancellationToken cancellationToken)
 1111        {
 1112            // Just run the scheduled task so that the user can see it
 31113            _taskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
 1114
 31115            return Task.CompletedTask;
 1116        }
 1117
 1118        /// <summary>
 1119        /// Validates the media library internal.
 1120        /// </summary>
 1121        /// <param name="progress">The progress.</param>
 1122        /// <param name="cancellationToken">The cancellation token.</param>
 1123        /// <returns>Task.</returns>
 1124        public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
 1125        {
 1126            IsScanRunning = true;
 1127            LibraryMonitor.Stop();
 1128
 1129            try
 1130            {
 1131                await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false);
 1132            }
 1133            finally
 1134            {
 1135                LibraryMonitor.Start();
 1136                IsScanRunning = false;
 1137            }
 1138        }
 1139
 1140        public async Task ValidateTopLibraryFolders(CancellationToken cancellationToken, bool removeRoot = false)
 1141        {
 1142            RootFolder.Children = null;
 1143            await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1144
 1145            // Start by just validating the children of the root, but go no further
 1146            await RootFolder.ValidateChildren(
 1147                new Progress<double>(),
 1148                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 1149                recursive: false,
 1150                allowRemoveRoot: removeRoot,
 1151                cancellationToken: cancellationToken).ConfigureAwait(false);
 1152
 1153            var rootFolder = GetUserRootFolder();
 1154            rootFolder.Children = null;
 1155
 1156            await rootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1157
 1158            await rootFolder.ValidateChildren(
 1159                new Progress<double>(),
 1160                new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
 1161                recursive: false,
 1162                allowRemoveRoot: removeRoot,
 1163                cancellationToken: cancellationToken).ConfigureAwait(false);
 1164
 1165            // Quickly scan CollectionFolders for changes
 1166            var toDelete = new List<Guid>();
 1167            foreach (var child in rootFolder.Children!.OfType<Folder>())
 1168            {
 1169                // If the user has somehow deleted the collection directory, remove the metadata from the database.
 1170                if (child is CollectionFolder collectionFolder && !Directory.Exists(collectionFolder.Path))
 1171                {
 1172                    toDelete.Add(collectionFolder.Id);
 1173                }
 1174                else
 1175                {
 1176                    await child.RefreshMetadata(cancellationToken).ConfigureAwait(false);
 1177                }
 1178            }
 1179
 1180            if (toDelete.Count > 0)
 1181            {
 1182                _itemRepository.DeleteItem(toDelete.ToArray());
 1183            }
 1184        }
 1185
 1186        private async Task PerformLibraryValidation(IProgress<double> progress, CancellationToken cancellationToken)
 1187        {
 1188            _logger.LogInformation("Validating media library");
 1189
 1190            await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false);
 1191
 1192            var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96));
 1193
 1194            // Validate the entire media library
 1195            await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem
 1196
 1197            progress.Report(96);
 1198
 1199            innerProgress = new Progress<double>(pct => progress.Report(96 + (pct * .04)));
 1200
 1201            await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
 1202
 1203            progress.Report(100);
 1204        }
 1205
 1206        /// <summary>
 1207        /// Runs the post scan tasks.
 1208        /// </summary>
 1209        /// <param name="progress">The progress.</param>
 1210        /// <param name="cancellationToken">The cancellation token.</param>
 1211        /// <returns>Task.</returns>
 1212        private async Task RunPostScanTasks(IProgress<double> progress, CancellationToken cancellationToken)
 1213        {
 1214            var tasks = PostScanTasks.ToList();
 1215
 1216            var numComplete = 0;
 1217            var numTasks = tasks.Count;
 1218
 1219            foreach (var task in tasks)
 1220            {
 1221                // Prevent access to modified closure
 1222                var currentNumComplete = numComplete;
 1223
 1224                var innerProgress = new Progress<double>(pct =>
 1225                {
 1226                    double innerPercent = pct;
 1227                    innerPercent /= 100;
 1228                    innerPercent += currentNumComplete;
 1229
 1230                    innerPercent /= numTasks;
 1231                    innerPercent *= 100;
 1232
 1233                    progress.Report(innerPercent);
 1234                });
 1235
 1236                _logger.LogDebug("Running post-scan task {0}", task.GetType().Name);
 1237
 1238                try
 1239                {
 1240                    await task.Run(innerProgress, cancellationToken).ConfigureAwait(false);
 1241                }
 1242                catch (OperationCanceledException)
 1243                {
 1244                    _logger.LogInformation("Post-scan task cancelled: {0}", task.GetType().Name);
 1245                    throw;
 1246                }
 1247                catch (Exception ex)
 1248                {
 1249                    _logger.LogError(ex, "Error running post-scan task");
 1250                }
 1251
 1252                numComplete++;
 1253                double percent = numComplete;
 1254                percent /= numTasks;
 1255                progress.Report(percent * 100);
 1256            }
 1257
 1258            _itemRepository.UpdateInheritedValues();
 1259
 1260            progress.Report(100);
 1261        }
 1262
 1263        /// <summary>
 1264        /// Gets the default view.
 1265        /// </summary>
 1266        /// <returns>IEnumerable{VirtualFolderInfo}.</returns>
 1267        public List<VirtualFolderInfo> GetVirtualFolders()
 1268        {
 231269            return GetVirtualFolders(false);
 1270        }
 1271
 1272        public List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState)
 1273        {
 241274            _logger.LogDebug("Getting topLibraryFolders");
 241275            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 1276
 241277            _logger.LogDebug("Getting refreshQueue");
 241278            var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null;
 1279
 241280            return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath)
 241281                .Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
 241282                .ToList();
 1283        }
 1284
 1285        private VirtualFolderInfo GetVirtualFolderInfo(string dir, List<BaseItem> allCollectionFolders, HashSet<Guid>? r
 1286        {
 11287            var info = new VirtualFolderInfo
 11288            {
 11289                Name = Path.GetFileName(dir),
 11290
 11291                Locations = _fileSystem.GetFilePaths(dir, false)
 11292                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 11293                    .Select(i =>
 11294                    {
 11295                        try
 11296                        {
 11297                            return _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(i));
 11298                        }
 11299                        catch (Exception ex)
 11300                        {
 11301                            _logger.LogError(ex, "Error resolving shortcut file {File}", i);
 11302                            return null;
 11303                        }
 11304                    })
 11305                    .Where(i => i is not null)
 11306                    .Order()
 11307                    .ToArray(),
 11308
 11309                CollectionType = GetCollectionType(dir)
 11310            };
 1311
 11312            var libraryFolder = allCollectionFolders.FirstOrDefault(i => string.Equals(i.Path, dir, StringComparison.Ord
 11313            if (libraryFolder is not null)
 1314            {
 11315                var libraryFolderId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture);
 11316                info.ItemId = libraryFolderId;
 11317                if (libraryFolder.HasImage(ImageType.Primary))
 1318                {
 01319                    info.PrimaryImageItemId = libraryFolderId;
 1320                }
 1321
 11322                info.LibraryOptions = GetLibraryOptions(libraryFolder);
 1323
 11324                if (refreshQueue is not null)
 1325                {
 11326                    info.RefreshProgress = libraryFolder.GetRefreshProgress();
 1327
 11328                    info.RefreshStatus = info.RefreshProgress.HasValue ? "Active" : refreshQueue.Contains(libraryFolder.
 1329                }
 1330            }
 1331
 11332            return info;
 1333        }
 1334
 1335        private CollectionTypeOptions? GetCollectionType(string path)
 1336        {
 11337            var files = _fileSystem.GetFilePaths(path, [".collection"], true, false);
 21338            foreach (ReadOnlySpan<char> file in files)
 1339            {
 01340                if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
 1341                {
 01342                    return res;
 1343                }
 1344            }
 1345
 11346            return null;
 01347        }
 1348
 1349        /// <inheritdoc />
 1350        public BaseItem? GetItemById(Guid id)
 1351        {
 5551352            if (id.IsEmpty())
 1353            {
 01354                throw new ArgumentException("Guid can't be empty", nameof(id));
 1355            }
 1356
 5551357            if (_cache.TryGet(id, out var item))
 1358            {
 4521359                return item;
 1360            }
 1361
 1031362            item = RetrieveItem(id);
 1363
 1031364            if (item is not null)
 1365            {
 01366                RegisterItem(item);
 1367            }
 1368
 1031369            return item;
 1370        }
 1371
 1372        /// <inheritdoc />
 1373        public T? GetItemById<T>(Guid id)
 1374            where T : BaseItem
 1375        {
 231376            var item = GetItemById(id);
 231377            if (item is T typedItem)
 1378            {
 11379                return typedItem;
 1380            }
 1381
 221382            return null;
 1383        }
 1384
 1385        /// <inheritdoc />
 1386        public T? GetItemById<T>(Guid id, Guid userId)
 1387            where T : BaseItem
 1388        {
 11389            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 11390            return GetItemById<T>(id, user);
 1391        }
 1392
 1393        /// <inheritdoc />
 1394        public T? GetItemById<T>(Guid id, User? user)
 1395            where T : BaseItem
 1396        {
 211397            var item = GetItemById<T>(id);
 211398            return ItemIsVisible(item, user) ? item : null;
 1399        }
 1400
 1401        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
 1402        {
 1191403            if (query.Recursive && !query.ParentId.IsEmpty())
 1404            {
 441405                var parent = GetItemById(query.ParentId);
 441406                if (parent is not null)
 1407                {
 441408                    SetTopParentIdsOrAncestors(query, [parent]);
 1409                }
 1410            }
 1411
 1191412            if (query.User is not null)
 1413            {
 11414                AddUserToQuery(query, query.User, allowExternalContent);
 1415            }
 1416
 1191417            var itemList = _itemRepository.GetItemList(query);
 1191418            var user = query.User;
 1191419            if (user is not null)
 1420            {
 11421                return itemList.Where(i => i.IsVisible(user)).ToList();
 1422            }
 1423
 1181424            return itemList;
 1425        }
 1426
 1427        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query)
 1428        {
 1191429            return GetItemList(query, true);
 1430        }
 1431
 1432        public int GetCount(InternalItemsQuery query)
 1433        {
 01434            if (query.Recursive && !query.ParentId.IsEmpty())
 1435            {
 01436                var parent = GetItemById(query.ParentId);
 01437                if (parent is not null)
 1438                {
 01439                    SetTopParentIdsOrAncestors(query, [parent]);
 1440                }
 1441            }
 1442
 01443            if (query.User is not null)
 1444            {
 01445                AddUserToQuery(query, query.User);
 1446            }
 1447
 01448            return _itemRepository.GetCount(query);
 1449        }
 1450
 1451        public ItemCounts GetItemCounts(InternalItemsQuery query)
 1452        {
 01453            if (query.Recursive && !query.ParentId.IsEmpty())
 1454            {
 01455                var parent = GetItemById(query.ParentId);
 01456                if (parent is not null)
 1457                {
 01458                    SetTopParentIdsOrAncestors(query, [parent]);
 1459                }
 1460            }
 1461
 01462            if (query.User is not null)
 1463            {
 01464                AddUserToQuery(query, query.User);
 1465            }
 1466
 01467            return _itemRepository.GetItemCounts(query);
 1468        }
 1469
 1470        public IReadOnlyList<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
 1471        {
 01472            SetTopParentIdsOrAncestors(query, parents);
 1473
 01474            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1475            {
 01476                if (query.User is not null)
 1477                {
 01478                    AddUserToQuery(query, query.User);
 1479                }
 1480            }
 1481
 01482            return _itemRepository.GetItemList(query);
 1483        }
 1484
 1485        public IReadOnlyList<BaseItem> GetLatestItemList(InternalItemsQuery query, IReadOnlyList<BaseItem> parents, Coll
 1486        {
 01487            SetTopParentIdsOrAncestors(query, parents);
 1488
 01489            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1490            {
 01491                if (query.User is not null)
 1492                {
 01493                    AddUserToQuery(query, query.User);
 1494                }
 1495            }
 1496
 01497            return _itemRepository.GetLatestItemList(query, collectionType);
 1498        }
 1499
 1500        public IReadOnlyList<string> GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents
 1501        {
 01502            SetTopParentIdsOrAncestors(query, parents);
 1503
 01504            if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0)
 1505            {
 01506                if (query.User is not null)
 1507                {
 01508                    AddUserToQuery(query, query.User);
 1509                }
 1510            }
 1511
 01512            return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff);
 1513        }
 1514
 1515        public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
 1516        {
 01517            if (query.User is not null)
 1518            {
 01519                AddUserToQuery(query, query.User);
 1520            }
 1521
 01522            if (query.EnableTotalRecordCount)
 1523            {
 01524                return _itemRepository.GetItems(query);
 1525            }
 1526
 01527            return new QueryResult<BaseItem>(
 01528                query.StartIndex,
 01529                null,
 01530                _itemRepository.GetItemList(query));
 1531        }
 1532
 1533        public IReadOnlyList<Guid> GetItemIds(InternalItemsQuery query)
 1534        {
 171535            if (query.User is not null)
 1536            {
 01537                AddUserToQuery(query, query.User);
 1538            }
 1539
 171540            return _itemRepository.GetItemIdsList(query);
 1541        }
 1542
 1543        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetStudios(InternalItemsQuery query)
 1544        {
 01545            if (query.User is not null)
 1546            {
 01547                AddUserToQuery(query, query.User);
 1548            }
 1549
 01550            SetTopParentOrAncestorIds(query);
 01551            return _itemRepository.GetStudios(query);
 1552        }
 1553
 1554        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetGenres(InternalItemsQuery query)
 1555        {
 01556            if (query.User is not null)
 1557            {
 01558                AddUserToQuery(query, query.User);
 1559            }
 1560
 01561            SetTopParentOrAncestorIds(query);
 01562            return _itemRepository.GetGenres(query);
 1563        }
 1564
 1565        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetMusicGenres(InternalItemsQuery query)
 1566        {
 01567            if (query.User is not null)
 1568            {
 01569                AddUserToQuery(query, query.User);
 1570            }
 1571
 01572            SetTopParentOrAncestorIds(query);
 01573            return _itemRepository.GetMusicGenres(query);
 1574        }
 1575
 1576        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAllArtists(InternalItemsQuery query)
 1577        {
 01578            if (query.User is not null)
 1579            {
 01580                AddUserToQuery(query, query.User);
 1581            }
 1582
 01583            SetTopParentOrAncestorIds(query);
 01584            return _itemRepository.GetAllArtists(query);
 1585        }
 1586
 1587        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetArtists(InternalItemsQuery query)
 1588        {
 01589            if (query.User is not null)
 1590            {
 01591                AddUserToQuery(query, query.User);
 1592            }
 1593
 01594            SetTopParentOrAncestorIds(query);
 01595            return _itemRepository.GetArtists(query);
 1596        }
 1597
 1598        private void SetTopParentOrAncestorIds(InternalItemsQuery query)
 1599        {
 01600            var ancestorIds = query.AncestorIds;
 01601            int len = ancestorIds.Length;
 01602            if (len == 0)
 1603            {
 01604                return;
 1605            }
 1606
 01607            var parents = new BaseItem[len];
 01608            for (int i = 0; i < len; i++)
 1609            {
 01610                parents[i] = GetItemById(ancestorIds[i]) ?? throw new ArgumentException($"Failed to find parent with id:
 01611                if (parents[i] is not (ICollectionFolder or UserView))
 1612                {
 01613                    return;
 1614                }
 1615            }
 1616
 1617            // Optimize by querying against top level views
 01618            query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 01619            query.AncestorIds = [];
 1620
 1621            // Prevent searching in all libraries due to empty filter
 01622            if (query.TopParentIds.Length == 0)
 1623            {
 01624                query.TopParentIds = [Guid.NewGuid()];
 1625            }
 01626        }
 1627
 1628        public QueryResult<(BaseItem Item, ItemCounts ItemCounts)> GetAlbumArtists(InternalItemsQuery query)
 1629        {
 01630            if (query.User is not null)
 1631            {
 01632                AddUserToQuery(query, query.User);
 1633            }
 1634
 01635            SetTopParentOrAncestorIds(query);
 01636            return _itemRepository.GetAlbumArtists(query);
 1637        }
 1638
 1639        public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
 1640        {
 131641            if (query.Recursive && !query.ParentId.IsEmpty())
 1642            {
 121643                var parent = GetItemById(query.ParentId);
 121644                if (parent is not null)
 1645                {
 121646                    SetTopParentIdsOrAncestors(query, [parent]);
 1647                }
 1648            }
 1649
 131650            if (query.User is not null)
 1651            {
 11652                AddUserToQuery(query, query.User);
 1653            }
 1654
 131655            if (query.EnableTotalRecordCount)
 1656            {
 11657                return _itemRepository.GetItems(query);
 1658            }
 1659
 121660            return new QueryResult<BaseItem>(
 121661                query.StartIndex,
 121662                null,
 121663                _itemRepository.GetItemList(query));
 1664        }
 1665
 1666        private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
 1667        {
 561668            if (parents.All(i => i is ICollectionFolder || i is UserView))
 1669            {
 1670                // Optimize by querying against top level views
 121671                query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray();
 1672
 1673                // Prevent searching in all libraries due to empty filter
 121674                if (query.TopParentIds.Length == 0)
 1675                {
 121676                    query.TopParentIds = [Guid.NewGuid()];
 1677                }
 1678            }
 1679            else
 1680            {
 1681                // We need to be able to query from any arbitrary ancestor up the tree
 441682                query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).ToArray();
 1683
 1684                // Prevent searching in all libraries due to empty filter
 441685                if (query.AncestorIds.Length == 0)
 1686                {
 01687                    query.AncestorIds = [Guid.NewGuid()];
 1688                }
 1689            }
 1690
 561691            query.Parent = null;
 561692        }
 1693
 1694        private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
 1695        {
 21696            if (query.AncestorIds.Length == 0 &&
 21697                query.ParentId.IsEmpty() &&
 21698                query.ChannelIds.Count == 0 &&
 21699                query.TopParentIds.Length == 0 &&
 21700                string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
 21701                string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
 21702                query.ItemIds.Length == 0)
 1703            {
 11704                var userViews = UserViewManager.GetUserViews(new UserViewQuery
 11705                {
 11706                    User = user,
 11707                    IncludeHidden = true,
 11708                    IncludeExternalContent = allowExternalContent
 11709                });
 1710
 11711                query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
 1712
 1713                // Prevent searching in all libraries due to empty filter
 11714                if (query.TopParentIds.Length == 0)
 1715                {
 11716                    query.TopParentIds = [Guid.NewGuid()];
 1717                }
 1718            }
 21719        }
 1720
 1721        private IEnumerable<Guid> GetTopParentIdsForQuery(BaseItem item, User? user)
 1722        {
 121723            if (item is UserView view)
 1724            {
 01725                if (view.ViewType == CollectionType.livetv)
 1726                {
 01727                    return [view.Id];
 1728                }
 1729
 1730                // Translate view into folders
 01731                if (!view.DisplayParentId.IsEmpty())
 1732                {
 01733                    var displayParent = GetItemById(view.DisplayParentId);
 01734                    if (displayParent is not null)
 1735                    {
 01736                        return GetTopParentIdsForQuery(displayParent, user);
 1737                    }
 1738
 01739                    return [];
 1740                }
 1741
 01742                if (!view.ParentId.IsEmpty())
 1743                {
 01744                    var displayParent = GetItemById(view.ParentId);
 01745                    if (displayParent is not null)
 1746                    {
 01747                        return GetTopParentIdsForQuery(displayParent, user);
 1748                    }
 1749
 01750                    return [];
 1751                }
 1752
 1753                // Handle grouping
 01754                if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.V
 01755                    && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
 1756                {
 01757                    return GetUserRootFolder()
 01758                        .GetChildren(user, true)
 01759                        .OfType<CollectionFolder>()
 01760                        .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
 01761                        .Where(i => user.IsFolderGrouped(i.Id))
 01762                        .SelectMany(i => GetTopParentIdsForQuery(i, user));
 1763                }
 1764
 01765                return [];
 1766            }
 1767
 121768            if (item is CollectionFolder collectionFolder)
 1769            {
 121770                return collectionFolder.PhysicalFolderIds;
 1771            }
 1772
 01773            var topParent = item.GetTopParent();
 01774            if (topParent is not null)
 1775            {
 01776                return [topParent.Id];
 1777            }
 1778
 01779            return [];
 1780        }
 1781
 1782        /// <summary>
 1783        /// Gets the intros.
 1784        /// </summary>
 1785        /// <param name="item">The item.</param>
 1786        /// <param name="user">The user.</param>
 1787        /// <returns>IEnumerable{System.String}.</returns>
 1788        public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
 1789        {
 1790            if (IntroProviders.Length == 0)
 1791            {
 1792                return [];
 1793            }
 1794
 1795            var tasks = IntroProviders
 1796                .Select(i => GetIntros(i, item, user));
 1797
 1798            var items = await Task.WhenAll(tasks).ConfigureAwait(false);
 1799
 1800            return items
 1801                .SelectMany(i => i)
 1802                .Select(ResolveIntro)
 1803                .Where(i => i is not null)!; // null values got filtered out
 1804        }
 1805
 1806        /// <summary>
 1807        /// Gets the intros.
 1808        /// </summary>
 1809        /// <param name="provider">The provider.</param>
 1810        /// <param name="item">The item.</param>
 1811        /// <param name="user">The user.</param>
 1812        /// <returns>Task&lt;IEnumerable&lt;IntroInfo&gt;&gt;.</returns>
 1813        private async Task<IEnumerable<IntroInfo>> GetIntros(IIntroProvider provider, BaseItem item, User user)
 1814        {
 1815            try
 1816            {
 1817                return await provider.GetIntros(item, user).ConfigureAwait(false);
 1818            }
 1819            catch (Exception ex)
 1820            {
 1821                _logger.LogError(ex, "Error getting intros");
 1822
 1823                return [];
 1824            }
 1825        }
 1826
 1827        /// <summary>
 1828        /// Resolves the intro.
 1829        /// </summary>
 1830        /// <param name="info">The info.</param>
 1831        /// <returns>Video.</returns>
 1832        private Video? ResolveIntro(IntroInfo info)
 1833        {
 01834            Video? video = null;
 1835
 01836            if (info.ItemId.HasValue)
 1837            {
 1838                // Get an existing item by Id
 01839                video = GetItemById(info.ItemId.Value) as Video;
 1840
 01841                if (video is null)
 1842                {
 01843                    _logger.LogError("Unable to locate item with Id {ID}.", info.ItemId.Value);
 1844                }
 1845            }
 01846            else if (!string.IsNullOrEmpty(info.Path))
 1847            {
 1848                try
 1849                {
 1850                    // Try to resolve the path into a video
 01851                    video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video;
 1852
 01853                    if (video is null)
 1854                    {
 01855                        _logger.LogError("Intro resolver returned null for {Path}.", info.Path);
 1856                    }
 1857                    else
 1858                    {
 1859                        // Pull the saved db item that will include metadata
 01860                        var dbItem = GetItemById(video.Id) as Video;
 1861
 01862                        if (dbItem is not null)
 1863                        {
 01864                            video = dbItem;
 1865                        }
 1866                        else
 1867                        {
 01868                            return null;
 1869                        }
 1870                    }
 01871                }
 01872                catch (Exception ex)
 1873                {
 01874                    _logger.LogError(ex, "Error resolving path {Path}.", info.Path);
 01875                }
 1876            }
 1877            else
 1878            {
 01879                _logger.LogError("IntroProvider returned an IntroInfo with null Path and ItemId.");
 1880            }
 1881
 01882            return video;
 01883        }
 1884
 1885        /// <inheritdoc />
 1886        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<ItemSortBy> sortBy, SortO
 1887        {
 11888            IOrderedEnumerable<BaseItem>? orderedItems = null;
 1889
 41890            foreach (var orderBy in sortBy.Select(o => GetComparer(o, user)).Where(c => c is not null))
 1891            {
 11892                if (orderBy is RandomComparer)
 1893                {
 01894                    var randomItems = items.ToArray();
 01895                    Random.Shared.Shuffle(randomItems);
 01896                    items = randomItems;
 1897                    // Items are no longer ordered at this point, so set orderedItems back to null
 01898                    orderedItems = null;
 1899                }
 11900                else if (orderedItems is null)
 1901                {
 11902                    orderedItems = sortOrder == SortOrder.Descending
 11903                        ? items.OrderByDescending(i => i, orderBy)
 11904                        : items.OrderBy(i => i, orderBy);
 1905                }
 1906                else
 1907                {
 01908                    orderedItems = sortOrder == SortOrder.Descending
 01909                        ? orderedItems!.ThenByDescending(i => i, orderBy)
 01910                        : orderedItems!.ThenBy(i => i, orderBy); // orderedItems is set during the first iteration
 1911                }
 1912            }
 1913
 11914            return orderedItems ?? items;
 1915        }
 1916
 1917        /// <inheritdoc />
 1918        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User? user, IEnumerable<(ItemSortBy OrderBy, Sort
 1919        {
 01920            IOrderedEnumerable<BaseItem>? orderedItems = null;
 1921
 01922            foreach (var (name, sortOrder) in orderBy)
 1923            {
 01924                var comparer = GetComparer(name, user);
 01925                if (comparer is null)
 1926                {
 1927                    continue;
 1928                }
 1929
 01930                if (comparer is RandomComparer)
 1931                {
 01932                    var randomItems = items.ToArray();
 01933                    Random.Shared.Shuffle(randomItems);
 01934                    items = randomItems;
 1935                    // Items are no longer ordered at this point, so set orderedItems back to null
 01936                    orderedItems = null;
 1937                }
 01938                else if (orderedItems is null)
 1939                {
 01940                    orderedItems = sortOrder == SortOrder.Descending
 01941                        ? items.OrderByDescending(i => i, comparer)
 01942                        : items.OrderBy(i => i, comparer);
 1943                }
 1944                else
 1945                {
 01946                    orderedItems = sortOrder == SortOrder.Descending
 01947                        ? orderedItems!.ThenByDescending(i => i, comparer)
 01948                        : orderedItems!.ThenBy(i => i, comparer); // orderedItems is set during the first iteration
 1949                }
 1950            }
 1951
 01952            return orderedItems ?? items;
 1953        }
 1954
 1955        /// <summary>
 1956        /// Gets the comparer.
 1957        /// </summary>
 1958        /// <param name="name">The name.</param>
 1959        /// <param name="user">The user.</param>
 1960        /// <returns>IBaseItemComparer.</returns>
 1961        private IBaseItemComparer? GetComparer(ItemSortBy name, User? user)
 1962        {
 11963            var comparer = Comparers.FirstOrDefault(c => name == c.Type);
 1964
 1965            // If it requires a user, create a new one, and assign the user
 11966            if (comparer is IUserBaseItemComparer)
 1967            {
 01968                var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null fo
 1969
 01970                userComparer.User = user;
 01971                userComparer.UserManager = _userManager;
 01972                userComparer.UserDataManager = _userDataManager;
 1973
 01974                return userComparer;
 1975            }
 1976
 11977            return comparer;
 1978        }
 1979
 1980        /// <inheritdoc />
 1981        public void CreateItem(BaseItem item, BaseItem? parent)
 1982        {
 01983            CreateItems([item], parent, CancellationToken.None);
 01984        }
 1985
 1986        /// <inheritdoc />
 1987        public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem? parent, CancellationToken cancellationToken)
 1988        {
 21989            _itemRepository.SaveItems(items, cancellationToken);
 1990
 81991            foreach (var item in items)
 1992            {
 21993                RegisterItem(item);
 1994            }
 1995
 21996            if (ItemAdded is not null)
 1997            {
 81998                foreach (var item in items)
 1999                {
 2000                    // With the live tv guide this just creates too much noise
 22001                    if (item.SourceType != SourceType.Library)
 2002                    {
 2003                        continue;
 2004                    }
 2005
 2006                    try
 2007                    {
 22008                        ItemAdded(
 22009                            this,
 22010                            new ItemChangeEventArgs
 22011                            {
 22012                                Item = item,
 22013                                Parent = parent ?? item.GetParent()
 22014                            });
 22015                    }
 02016                    catch (Exception ex)
 2017                    {
 02018                        _logger.LogError(ex, "Error in ItemAdded event handler");
 02019                    }
 2020                }
 2021            }
 22022        }
 2023
 2024        private bool ImageNeedsRefresh(ItemImageInfo image)
 2025        {
 02026            if (image.Path is not null && image.IsLocalFile)
 2027            {
 02028                if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash))
 2029                {
 02030                    return true;
 2031                }
 2032
 2033                try
 2034                {
 02035                    return image.DateModified.Subtract(_fileSystem.GetLastWriteTimeUtc(image.Path)).Duration().TotalSeco
 2036                }
 02037                catch (Exception ex)
 2038                {
 02039                    _logger.LogError(ex, "Cannot get file info for {0}", image.Path);
 02040                    return false;
 2041                }
 2042            }
 2043
 02044            return image.Path is not null && !image.IsLocalFile;
 02045        }
 2046
 2047        /// <inheritdoc />
 2048        public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false)
 2049        {
 2050            ArgumentNullException.ThrowIfNull(item);
 2051
 2052            var outdated = forceUpdate
 2053                ? item.ImageInfos.Where(i => i.Path is not null).ToArray()
 2054                : item.ImageInfos.Where(ImageNeedsRefresh).ToArray();
 2055            // Skip image processing if current or live tv source
 2056            if (outdated.Length == 0 || item.SourceType != SourceType.Library)
 2057            {
 2058                RegisterItem(item);
 2059                return;
 2060            }
 2061
 2062            foreach (var img in outdated)
 2063            {
 2064                var image = img;
 2065                if (!img.IsLocalFile)
 2066                {
 2067                    try
 2068                    {
 2069                        var index = item.GetImageIndex(img);
 2070                        image = await ConvertImageToLocal(item, img, index, true).ConfigureAwait(false);
 2071                    }
 2072                    catch (ArgumentException)
 2073                    {
 2074                        _logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
 2075                        continue;
 2076                    }
 2077                    catch (Exception ex) when (ex is InvalidOperationException or IOException)
 2078                    {
 2079                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
 2080                        continue;
 2081                    }
 2082                    catch (HttpRequestException ex)
 2083                    {
 2084                        _logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", im
 2085                        continue;
 2086                    }
 2087                }
 2088
 2089                if (!File.Exists(image.Path))
 2090                {
 2091                    _logger.LogWarning("Image not found at {ImagePath}", image.Path);
 2092                    continue;
 2093                }
 2094
 2095                ImageDimensions size;
 2096                try
 2097                {
 2098                    size = _imageProcessor.GetImageDimensions(item, image);
 2099                    image.Width = size.Width;
 2100                    image.Height = size.Height;
 2101                }
 2102                catch (Exception ex)
 2103                {
 2104                    _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
 2105                    size = default;
 2106                    image.Width = 0;
 2107                    image.Height = 0;
 2108                }
 2109
 2110                try
 2111                {
 2112                    var blurhash = _imageProcessor.GetImageBlurHash(image.Path, size);
 2113                    image.BlurHash = blurhash;
 2114                }
 2115                catch (Exception ex)
 2116                {
 2117                    _logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
 2118                    image.BlurHash = string.Empty;
 2119                }
 2120
 2121                try
 2122                {
 2123                    var modifiedDate = _fileSystem.GetLastWriteTimeUtc(image.Path);
 2124                    image.DateModified = modifiedDate;
 2125                }
 2126                catch (Exception ex)
 2127                {
 2128                    _logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
 2129                }
 2130            }
 2131
 2132            item.ValidateImages();
 2133
 2134            _itemRepository.SaveImages(item);
 2135
 2136            RegisterItem(item);
 2137        }
 2138
 2139        /// <inheritdoc />
 2140        public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, 
 2141        {
 2142            foreach (var item in items)
 2143            {
 2144                item.DateLastSaved = DateTime.UtcNow;
 2145                await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
 2146
 2147                // Modify again, so saved value is after write time of externally saved metadata
 2148                item.DateLastSaved = DateTime.UtcNow;
 2149            }
 2150
 2151            _itemRepository.SaveItems(items, cancellationToken);
 2152
 2153            if (ItemUpdated is not null)
 2154            {
 2155                foreach (var item in items)
 2156                {
 2157                    // With the live tv guide this just creates too much noise
 2158                    if (item.SourceType != SourceType.Library)
 2159                    {
 2160                        continue;
 2161                    }
 2162
 2163                    try
 2164                    {
 2165                        ItemUpdated(
 2166                            this,
 2167                            new ItemChangeEventArgs
 2168                            {
 2169                                Item = item,
 2170                                Parent = parent,
 2171                                UpdateReason = updateReason
 2172                            });
 2173                    }
 2174                    catch (Exception ex)
 2175                    {
 2176                        _logger.LogError(ex, "Error in ItemUpdated event handler");
 2177                    }
 2178                }
 2179            }
 2180        }
 2181
 2182        /// <inheritdoc />
 2183        public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cance
 1122184            => UpdateItemsAsync([item], parent, updateReason, cancellationToken);
 2185
 2186        public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
 2187        {
 2188            if (item.IsFileProtocol)
 2189            {
 2190                await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false);
 2191            }
 2192
 2193            await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
 2194        }
 2195
 2196        /// <summary>
 2197        /// Reports the item removed.
 2198        /// </summary>
 2199        /// <param name="item">The item.</param>
 2200        /// <param name="parent">The parent item.</param>
 2201        public void ReportItemRemoved(BaseItem item, BaseItem parent)
 2202        {
 02203            if (ItemRemoved is not null)
 2204            {
 2205                try
 2206                {
 02207                    ItemRemoved(
 02208                        this,
 02209                        new ItemChangeEventArgs
 02210                        {
 02211                            Item = item,
 02212                            Parent = parent
 02213                        });
 02214                }
 02215                catch (Exception ex)
 2216                {
 02217                    _logger.LogError(ex, "Error in ItemRemoved event handler");
 02218                }
 2219            }
 02220        }
 2221
 2222        /// <summary>
 2223        /// Retrieves the item.
 2224        /// </summary>
 2225        /// <param name="id">The id.</param>
 2226        /// <returns>BaseItem.</returns>
 2227        public BaseItem RetrieveItem(Guid id)
 2228        {
 1032229            return _itemRepository.RetrieveItem(id);
 2230        }
 2231
 2232        public List<Folder> GetCollectionFolders(BaseItem item)
 2233        {
 7932234            return GetCollectionFolders(item, GetUserRootFolder().Children.OfType<Folder>());
 2235        }
 2236
 2237        public List<Folder> GetCollectionFolders(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2238        {
 8292239            while (item is not null)
 2240            {
 8292241                var parent = item.GetParent();
 2242
 8292243                if (parent is AggregateFolder)
 2244                {
 2245                    break;
 2246                }
 2247
 7362248                if (parent is null)
 2249                {
 7002250                    var owner = item.GetOwner();
 2251
 7002252                    if (owner is null)
 2253                    {
 2254                        break;
 2255                    }
 2256
 02257                    item = owner;
 2258                }
 2259                else
 2260                {
 362261                    item = parent;
 2262                }
 2263            }
 2264
 7932265            if (item is null)
 2266            {
 02267                return new List<Folder>();
 2268            }
 2269
 7932270            return GetCollectionFoldersInternal(item, allUserRootChildren);
 2271        }
 2272
 2273        private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
 2274        {
 7932275            return allUserRootChildren
 7932276                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 7932277                .ToList();
 2278        }
 2279
 2280        public LibraryOptions GetLibraryOptions(BaseItem item)
 2281        {
 4962282            if (item is CollectionFolder collectionFolder)
 2283            {
 542284                return collectionFolder.GetLibraryOptions();
 2285            }
 2286
 2287            // List.Find is more performant than FirstOrDefault due to enumerator allocation
 4422288            return GetCollectionFolders(item)
 4422289                .Find(folder => folder is CollectionFolder) is CollectionFolder collectionFolder2
 4422290                ? collectionFolder2.GetLibraryOptions()
 4422291                : new LibraryOptions();
 2292        }
 2293
 2294        public CollectionType? GetContentType(BaseItem item)
 2295        {
 592296            var configuredContentType = GetConfiguredContentType(item, false);
 592297            if (configuredContentType is not null)
 2298            {
 02299                return configuredContentType;
 2300            }
 2301
 592302            configuredContentType = GetConfiguredContentType(item, true);
 592303            if (configuredContentType is not null)
 2304            {
 02305                return configuredContentType;
 2306            }
 2307
 592308            return GetInheritedContentType(item);
 2309        }
 2310
 2311        public CollectionType? GetInheritedContentType(BaseItem item)
 2312        {
 592313            var type = GetTopFolderContentType(item);
 2314
 592315            if (type is not null)
 2316            {
 02317                return type;
 2318            }
 2319
 592320            return item.GetParents()
 592321                .Select(GetConfiguredContentType)
 592322                .LastOrDefault(i => i is not null);
 2323        }
 2324
 2325        public CollectionType? GetConfiguredContentType(BaseItem item)
 2326        {
 02327            return GetConfiguredContentType(item, false);
 2328        }
 2329
 2330        public CollectionType? GetConfiguredContentType(string path)
 2331        {
 02332            return GetContentTypeOverride(path, false);
 2333        }
 2334
 2335        public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
 2336        {
 1182337            if (item is ICollectionFolder collectionFolder)
 2338            {
 02339                return collectionFolder.CollectionType;
 2340            }
 2341
 1182342            return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
 2343        }
 2344
 2345        private CollectionType? GetContentTypeOverride(string path, bool inherit)
 2346        {
 1362347            var nameValuePair = _configurationManager.Configuration.ContentTypes
 1362348                                    .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
 1362349                                                         || (inherit && !string.IsNullOrEmpty(i.Name)
 1362350                                                                     && _fileSystem.ContainsSubPath(i.Name, path)));
 1362351            if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType))
 2352            {
 02353                return collectionType;
 2354            }
 2355
 1362356            return null;
 2357        }
 2358
 2359        private CollectionType? GetTopFolderContentType(BaseItem item)
 2360        {
 592361            if (item is null)
 2362            {
 02363                return null;
 2364            }
 2365
 592366            while (!item.ParentId.IsEmpty())
 2367            {
 02368                var parent = item.GetParent();
 02369                if (parent is null || parent is AggregateFolder)
 2370                {
 2371                    break;
 2372                }
 2373
 02374                item = parent;
 2375            }
 2376
 592377            return GetUserRootFolder().Children
 592378                .OfType<ICollectionFolder>()
 592379                .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.
 592380                .Select(i => i.CollectionType)
 592381                .FirstOrDefault(i => i is not null);
 2382        }
 2383
 2384        public UserView GetNamedView(
 2385            User user,
 2386            string name,
 2387            CollectionType? viewType,
 2388            string sortName)
 2389        {
 02390            return GetNamedView(user, name, Guid.Empty, viewType, sortName);
 2391        }
 2392
 2393        public UserView GetNamedView(
 2394            string name,
 2395            CollectionType viewType,
 2396            string sortName)
 2397        {
 02398            var path = Path.Combine(
 02399                _configurationManager.ApplicationPaths.InternalMetadataPath,
 02400                "views",
 02401                _fileSystem.GetValidFilename(viewType.ToString()));
 2402
 02403            var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
 2404
 02405            var item = GetItemById(id) as UserView;
 2406
 02407            var refresh = false;
 2408
 02409            if (item is null || !string.Equals(item.Path, path, StringComparison.OrdinalIgnoreCase))
 2410            {
 02411                var info = Directory.CreateDirectory(path);
 02412                item = new UserView
 02413                {
 02414                    Path = path,
 02415                    Id = id,
 02416                    DateCreated = info.CreationTimeUtc,
 02417                    DateModified = info.LastWriteTimeUtc,
 02418                    Name = name,
 02419                    ViewType = viewType,
 02420                    ForcedSortName = sortName
 02421                };
 2422
 02423                CreateItem(item, null);
 2424
 02425                refresh = true;
 2426            }
 2427
 02428            if (refresh)
 2429            {
 02430                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResu
 02431                ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), Ref
 2432            }
 2433
 02434            return item;
 2435        }
 2436
 2437        public UserView GetNamedView(
 2438            User user,
 2439            string name,
 2440            Guid parentId,
 2441            CollectionType? viewType,
 2442            string sortName)
 2443        {
 02444            var parentIdString = parentId.IsEmpty()
 02445                ? null
 02446                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02447            var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdStrin
 2448
 02449            var id = GetNewItemId(idValues, typeof(UserView));
 2450
 02451            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2452
 02453            var item = GetItemById(id) as UserView;
 2454
 02455            var isNew = false;
 2456
 02457            if (item is null)
 2458            {
 02459                var info = Directory.CreateDirectory(path);
 02460                item = new UserView
 02461                {
 02462                    Path = path,
 02463                    Id = id,
 02464                    DateCreated = info.CreationTimeUtc,
 02465                    DateModified = info.LastWriteTimeUtc,
 02466                    Name = name,
 02467                    ViewType = viewType,
 02468                    ForcedSortName = sortName,
 02469                    UserId = user.Id,
 02470                    DisplayParentId = parentId
 02471                };
 2472
 02473                CreateItem(item, null);
 2474
 02475                isNew = true;
 2476            }
 2477
 02478            var lastRefreshedUtc = item.DateLastRefreshed;
 02479            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2480
 02481            if (!refresh && !item.DisplayParentId.IsEmpty())
 2482            {
 02483                var displayParent = GetItemById(item.DisplayParentId);
 02484                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 2485            }
 2486
 02487            if (refresh)
 2488            {
 02489                ProviderManager.QueueRefresh(
 02490                    item.Id,
 02491                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02492                    {
 02493                        // Need to force save to increment DateLastSaved
 02494                        ForceSave = true
 02495                    },
 02496                    RefreshPriority.Normal);
 2497            }
 2498
 02499            return item;
 2500        }
 2501
 2502        public UserView GetShadowView(
 2503            BaseItem parent,
 2504            CollectionType? viewType,
 2505            string sortName)
 2506        {
 02507            ArgumentNullException.ThrowIfNull(parent);
 2508
 02509            var name = parent.Name;
 02510            var parentId = parent.Id;
 2511
 02512            var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
 2513
 02514            var id = GetNewItemId(idValues, typeof(UserView));
 2515
 02516            var path = parent.Path;
 2517
 02518            var item = GetItemById(id) as UserView;
 2519
 02520            var isNew = false;
 2521
 02522            if (item is null)
 2523            {
 02524                var info = Directory.CreateDirectory(path);
 02525                item = new UserView
 02526                {
 02527                    Path = path,
 02528                    Id = id,
 02529                    DateCreated = info.CreationTimeUtc,
 02530                    DateModified = info.LastWriteTimeUtc,
 02531                    Name = name,
 02532                    ViewType = viewType,
 02533                    ForcedSortName = sortName,
 02534                    DisplayParentId = parentId
 02535                };
 2536
 02537                CreateItem(item, null);
 2538
 02539                isNew = true;
 2540            }
 2541
 02542            var lastRefreshedUtc = item.DateLastRefreshed;
 02543            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2544
 02545            if (!refresh && !item.DisplayParentId.IsEmpty())
 2546            {
 02547                var displayParent = GetItemById(item.DisplayParentId);
 02548                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 2549            }
 2550
 02551            if (refresh)
 2552            {
 02553                ProviderManager.QueueRefresh(
 02554                    item.Id,
 02555                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02556                    {
 02557                        // Need to force save to increment DateLastSaved
 02558                        ForceSave = true
 02559                    },
 02560                    RefreshPriority.Normal);
 2561            }
 2562
 02563            return item;
 2564        }
 2565
 2566        public UserView GetNamedView(
 2567            string name,
 2568            Guid parentId,
 2569            CollectionType? viewType,
 2570            string sortName,
 2571            string uniqueId)
 2572        {
 02573            ArgumentException.ThrowIfNullOrEmpty(name);
 2574
 02575            var parentIdString = parentId.IsEmpty()
 02576                ? null
 02577                : parentId.ToString("N", CultureInfo.InvariantCulture);
 02578            var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.E
 02579            if (!string.IsNullOrEmpty(uniqueId))
 2580            {
 02581                idValues += uniqueId;
 2582            }
 2583
 02584            var id = GetNewItemId(idValues, typeof(UserView));
 2585
 02586            var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N
 2587
 02588            var item = GetItemById(id) as UserView;
 2589
 02590            var isNew = false;
 2591
 02592            if (item is null)
 2593            {
 02594                var info = Directory.CreateDirectory(path);
 02595                item = new UserView
 02596                {
 02597                    Path = path,
 02598                    Id = id,
 02599                    DateCreated = info.CreationTimeUtc,
 02600                    DateModified = info.LastWriteTimeUtc,
 02601                    Name = name,
 02602                    ViewType = viewType,
 02603                    ForcedSortName = sortName,
 02604                    DisplayParentId = parentId
 02605                };
 2606
 02607                CreateItem(item, null);
 2608
 02609                isNew = true;
 2610            }
 2611
 02612            if (viewType != item.ViewType)
 2613            {
 02614                item.ViewType = viewType;
 02615                item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult
 2616            }
 2617
 02618            var lastRefreshedUtc = item.DateLastRefreshed;
 02619            var refresh = isNew || DateTime.UtcNow - lastRefreshedUtc >= _viewRefreshInterval;
 2620
 02621            if (!refresh && !item.DisplayParentId.IsEmpty())
 2622            {
 02623                var displayParent = GetItemById(item.DisplayParentId);
 02624                refresh = displayParent is not null && displayParent.DateLastSaved > lastRefreshedUtc;
 2625            }
 2626
 02627            if (refresh)
 2628            {
 02629                ProviderManager.QueueRefresh(
 02630                    item.Id,
 02631                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
 02632                    {
 02633                        // Need to force save to increment DateLastSaved
 02634                        ForceSave = true
 02635                    },
 02636                    RefreshPriority.Normal);
 2637            }
 2638
 02639            return item;
 2640        }
 2641
 2642        public BaseItem GetParentItem(Guid? parentId, Guid? userId)
 2643        {
 32644            if (parentId.HasValue)
 2645            {
 02646                return GetItemById(parentId.Value) ?? throw new ArgumentException($"Invalid parent id: {parentId.Value}"
 2647            }
 2648
 32649            if (!userId.IsNullOrEmpty())
 2650            {
 32651                return GetUserRootFolder();
 2652            }
 2653
 02654            return RootFolder;
 2655        }
 2656
 2657        /// <inheritdoc />
 2658        public void QueueLibraryScan()
 2659        {
 02660            _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>();
 02661        }
 2662
 2663        /// <inheritdoc />
 2664        public int? GetSeasonNumberFromPath(string path, Guid? parentId)
 2665        {
 02666            var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null;
 02667            return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber;
 2668        }
 2669
 2670        /// <inheritdoc />
 2671        public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
 2672        {
 02673            var series = episode.Series;
 02674            bool? isAbsoluteNaming = series is not null && string.Equals(series.DisplayOrder, "absolute", StringComparis
 02675            if (!isAbsoluteNaming.Value)
 2676            {
 2677                // In other words, no filter applied
 02678                isAbsoluteNaming = null;
 2679            }
 2680
 02681            var resolver = new EpisodeResolver(_namingOptions);
 2682
 02683            var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 2684
 02685            EpisodeInfo? episodeInfo = null;
 02686            if (episode.IsFileProtocol)
 2687            {
 02688                episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
 2689                // Resolve from parent folder if it's not the Season folder
 02690                var parent = episode.GetParent();
 02691                if (episodeInfo is null && parent.GetType() == typeof(Folder))
 2692                {
 02693                    episodeInfo = resolver.Resolve(parent.Path, true, null, null, isAbsoluteNaming);
 02694                    if (episodeInfo is not null)
 2695                    {
 2696                        // add the container
 02697                        episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
 2698                    }
 2699                }
 2700            }
 2701
 02702            var changed = false;
 02703            if (episodeInfo is null)
 2704            {
 02705                return changed;
 2706            }
 2707
 02708            if (episodeInfo.IsByDate)
 2709            {
 02710                if (episode.IndexNumber.HasValue)
 2711                {
 02712                    episode.IndexNumber = null;
 02713                    changed = true;
 2714                }
 2715
 02716                if (episode.IndexNumberEnd.HasValue)
 2717                {
 02718                    episode.IndexNumberEnd = null;
 02719                    changed = true;
 2720                }
 2721
 02722                if (!episode.PremiereDate.HasValue)
 2723                {
 02724                    if (episodeInfo.Year.HasValue && episodeInfo.Month.HasValue && episodeInfo.Day.HasValue)
 2725                    {
 02726                        episode.PremiereDate = new DateTime(episodeInfo.Year.Value, episodeInfo.Month.Value, episodeInfo
 2727                    }
 2728
 02729                    if (episode.PremiereDate.HasValue)
 2730                    {
 02731                        changed = true;
 2732                    }
 2733                }
 2734
 02735                if (!episode.ProductionYear.HasValue)
 2736                {
 02737                    episode.ProductionYear = episodeInfo.Year;
 2738
 02739                    if (episode.ProductionYear.HasValue)
 2740                    {
 02741                        changed = true;
 2742                    }
 2743                }
 2744            }
 2745            else
 2746            {
 02747                if (!episode.IndexNumber.HasValue || forceRefresh)
 2748                {
 02749                    if (episode.IndexNumber != episodeInfo.EpisodeNumber)
 2750                    {
 02751                        changed = true;
 2752                    }
 2753
 02754                    episode.IndexNumber = episodeInfo.EpisodeNumber;
 2755                }
 2756
 02757                if (!episode.IndexNumberEnd.HasValue || forceRefresh)
 2758                {
 02759                    if (episode.IndexNumberEnd != episodeInfo.EndingEpisodeNumber)
 2760                    {
 02761                        changed = true;
 2762                    }
 2763
 02764                    episode.IndexNumberEnd = episodeInfo.EndingEpisodeNumber;
 2765                }
 2766
 02767                if (!episode.ParentIndexNumber.HasValue || forceRefresh)
 2768                {
 02769                    if (episode.ParentIndexNumber != episodeInfo.SeasonNumber)
 2770                    {
 02771                        changed = true;
 2772                    }
 2773
 02774                    episode.ParentIndexNumber = episodeInfo.SeasonNumber;
 2775                }
 2776            }
 2777
 02778            if (!episode.ParentIndexNumber.HasValue)
 2779            {
 02780                var season = episode.Season;
 2781
 02782                if (season is not null)
 2783                {
 02784                    episode.ParentIndexNumber = season.IndexNumber;
 2785                }
 2786
 02787                if (episode.ParentIndexNumber.HasValue)
 2788                {
 02789                    changed = true;
 2790                }
 2791            }
 2792
 02793            return changed;
 2794        }
 2795
 2796        public ItemLookupInfo ParseName(string name)
 2797        {
 02798            var namingOptions = _namingOptions;
 02799            var result = VideoResolver.CleanDateTime(name, namingOptions);
 2800
 02801            return new ItemLookupInfo
 02802            {
 02803                Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName : result.Name
 02804                Year = result.Year
 02805            };
 2806        }
 2807
 2808        public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, ID
 2809        {
 2810            // Apply .ignore rules
 2811            var filtered = fileSystemChildren.Where(c => !DotIgnoreIgnoreRule.IsIgnored(c, owner)).ToList();
 2812            var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions, libraryRoot: owner.Co
 2813            if (ownerVideoInfo is null)
 2814            {
 2815                yield break;
 2816            }
 2817
 2818            var count = filtered.Count;
 2819            for (var i = 0; i < count; i++)
 2820            {
 2821                var current = filtered[i];
 2822                if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
 2823                {
 2824                    var filesInSubFolder = _fileSystem.GetFiles(current.FullName, null, false, false);
 2825                    var filesInSubFolderList = filesInSubFolder.ToList();
 2826
 2827                    bool subFolderIsMixedFolder = filesInSubFolderList.Count > 1;
 2828
 2829                    foreach (var file in filesInSubFolderList)
 2830                    {
 2831                        if (!_extraResolver.TryGetExtraTypeForOwner(file.FullName, ownerVideoInfo, out var extraType))
 2832                        {
 2833                            continue;
 2834                        }
 2835
 2836                        var extra = GetExtra(file, extraType.Value, subFolderIsMixedFolder);
 2837                        if (extra is not null)
 2838                        {
 2839                            yield return extra;
 2840                        }
 2841                    }
 2842                }
 2843                else if (!current.IsDirectory && _extraResolver.TryGetExtraTypeForOwner(current.FullName, ownerVideoInfo
 2844                {
 2845                    var extra = GetExtra(current, extraType.Value, false);
 2846                    if (extra is not null)
 2847                    {
 2848                        yield return extra;
 2849                    }
 2850                }
 2851            }
 2852
 2853            BaseItem? GetExtra(FileSystemMetadata file, ExtraType extraType, bool isInMixedFolder)
 2854            {
 2855                var extra = ResolvePath(_fileSystem.GetFileInfo(file.FullName), directoryService, _extraResolver.GetReso
 2856                if (extra is not Video && extra is not Audio)
 2857                {
 2858                    return null;
 2859                }
 2860
 2861                // Try to retrieve it from the db. If we don't find it, use the resolved version
 2862                var itemById = GetItemById(extra.Id);
 2863                if (itemById is not null)
 2864                {
 2865                    extra = itemById;
 2866                }
 2867
 2868                // Only update extra type if it is more specific then the currently known extra type
 2869                if (extra.ExtraType is null or ExtraType.Unknown || extraType != ExtraType.Unknown)
 2870                {
 2871                    extra.ExtraType = extraType;
 2872                }
 2873
 2874                extra.ParentId = Guid.Empty;
 2875                extra.OwnerId = owner.Id;
 2876                extra.IsInMixedFolder = isInMixedFolder;
 2877                return extra;
 2878            }
 2879        }
 2880
 2881        public string GetPathAfterNetworkSubstitution(string path, BaseItem? ownerItem)
 2882        {
 122883            foreach (var map in _configurationManager.Configuration.PathSubstitutions)
 2884            {
 02885                if (path.TryReplaceSubPath(map.From, map.To, out var newPath))
 2886                {
 02887                    return newPath;
 2888                }
 2889            }
 2890
 62891            return path;
 2892        }
 2893
 2894        public IReadOnlyList<PersonInfo> GetPeople(InternalPeopleQuery query)
 2895        {
 02896            return _peopleRepository.GetPeople(query);
 2897        }
 2898
 2899        public IReadOnlyList<PersonInfo> GetPeople(BaseItem item)
 2900        {
 62901            if (item.SupportsPeople)
 2902            {
 02903                var people = GetPeople(new InternalPeopleQuery
 02904                {
 02905                    ItemId = item.Id
 02906                });
 2907
 02908                if (people.Count > 0)
 2909                {
 02910                    return people;
 2911                }
 2912            }
 2913
 62914            return [];
 2915        }
 2916
 2917        public IReadOnlyList<Person> GetPeopleItems(InternalPeopleQuery query)
 2918        {
 02919            return _peopleRepository.GetPeopleNames(query)
 02920            .Select(i =>
 02921            {
 02922                try
 02923                {
 02924                    return GetPerson(i);
 02925                }
 02926                catch (Exception ex)
 02927                {
 02928                    _logger.LogError(ex, "Error getting person");
 02929                    return null;
 02930                }
 02931            })
 02932            .Where(i => i is not null)
 02933            .Where(i => query.User is null || i!.IsVisible(query.User))
 02934            .ToList()!; // null values are filtered out
 2935        }
 2936
 2937        public IReadOnlyList<string> GetPeopleNames(InternalPeopleQuery query)
 2938        {
 02939            return _peopleRepository.GetPeopleNames(query);
 2940        }
 2941
 2942        public void UpdatePeople(BaseItem item, List<PersonInfo> people)
 2943        {
 02944            UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
 02945        }
 2946
 2947        /// <inheritdoc />
 2948        public async Task UpdatePeopleAsync(BaseItem item, IReadOnlyList<PersonInfo> people, CancellationToken cancellat
 2949        {
 2950            if (!item.SupportsPeople)
 2951            {
 2952                return;
 2953            }
 2954
 2955            if (people is not null)
 2956            {
 2957                people = people.Where(e => e is not null).ToArray();
 2958                _peopleRepository.UpdatePeople(item.Id, people);
 2959                await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
 2960            }
 2961        }
 2962
 2963        public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex, bool re
 2964        {
 2965            foreach (var url in image.Path.Split('|'))
 2966            {
 2967                try
 2968                {
 2969                    _logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
 2970
 2971                    await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).Configure
 2972
 2973                    await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwai
 2974
 2975                    return item.GetImageInfo(image.Type, imageIndex);
 2976                }
 2977                catch (HttpRequestException ex)
 2978                {
 2979                    if (ex.StatusCode.HasValue
 2980                        && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forb
 2981                    {
 2982                        _logger.LogDebug(ex, "Error downloading image {Url}", url);
 2983                        continue;
 2984                    }
 2985
 2986                    throw;
 2987                }
 2988            }
 2989
 2990            if (removeOnFailure)
 2991            {
 2992                // Remove this image to prevent it from retrying over and over
 2993                item.RemoveImage(image);
 2994                await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(fa
 2995            }
 2996
 2997            throw new InvalidOperationException("Unable to convert any images to local");
 2998        }
 2999
 3000        public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, b
 3001        {
 3002            if (string.IsNullOrWhiteSpace(name))
 3003            {
 3004                throw new ArgumentNullException(nameof(name));
 3005            }
 3006
 3007            name = _fileSystem.GetValidFilename(name.Trim());
 3008
 3009            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3010
 3011            var existingNameCount = 1; // first numbered name will be 2
 3012            var virtualFolderPath = Path.Combine(rootFolderPath, name);
 3013            var originalName = name;
 3014            while (Directory.Exists(virtualFolderPath))
 3015            {
 3016                existingNameCount++;
 3017                name = originalName + existingNameCount;
 3018                virtualFolderPath = Path.Combine(rootFolderPath, name);
 3019            }
 3020
 3021            var mediaPathInfos = options.PathInfos;
 3022            if (mediaPathInfos is not null)
 3023            {
 3024                var invalidpath = mediaPathInfos.FirstOrDefault(i => !Directory.Exists(i.Path));
 3025                if (invalidpath is not null)
 3026                {
 3027                    throw new ArgumentException("The specified path does not exist: " + invalidpath.Path + ".");
 3028                }
 3029            }
 3030
 3031            LibraryMonitor.Stop();
 3032
 3033            try
 3034            {
 3035                Directory.CreateDirectory(virtualFolderPath);
 3036
 3037                if (collectionType is not null)
 3038                {
 3039                    var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collect
 3040
 3041                    FileHelper.CreateEmpty(path);
 3042                }
 3043
 3044                CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
 3045
 3046                if (mediaPathInfos is not null)
 3047                {
 3048                    foreach (var path in mediaPathInfos)
 3049                    {
 3050                        AddMediaPathInternal(name, path, false);
 3051                    }
 3052                }
 3053            }
 3054            finally
 3055            {
 3056                await ValidateTopLibraryFolders(CancellationToken.None).ConfigureAwait(false);
 3057
 3058                if (refreshLibrary)
 3059                {
 3060                    StartScanInBackground();
 3061                }
 3062                else
 3063                {
 3064                    // Need to add a delay here or directory watchers may still pick up the changes
 3065                    await Task.Delay(1000).ConfigureAwait(false);
 3066                    LibraryMonitor.Start();
 3067                }
 3068            }
 3069        }
 3070
 3071        private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
 3072        {
 3073            foreach (var person in people)
 3074            {
 3075                cancellationToken.ThrowIfCancellationRequested();
 3076
 3077                var itemUpdateType = ItemUpdateType.MetadataDownload;
 3078                var saveEntity = false;
 3079                var createEntity = false;
 3080                var personEntity = GetPerson(person.Name);
 3081
 3082                if (personEntity is null)
 3083                {
 3084                    try
 3085                    {
 3086                        var path = Person.GetPath(person.Name);
 3087                        var info = Directory.CreateDirectory(path);
 3088                        personEntity = new Person()
 3089                        {
 3090                            Name = person.Name,
 3091                            Id = GetItemByNameId<Person>(path),
 3092                            DateCreated = info.CreationTimeUtc,
 3093                            DateModified = info.LastWriteTimeUtc,
 3094                            Path = path
 3095                        };
 3096
 3097                        personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
 3098                        saveEntity = true;
 3099                        createEntity = true;
 3100                    }
 3101                    catch (Exception ex)
 3102                    {
 3103                        _logger.LogWarning(ex, "Failed to create person {Name}", person.Name);
 3104                        continue;
 3105                    }
 3106                }
 3107
 3108                foreach (var id in person.ProviderIds)
 3109                {
 3110                    if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase)
 3111                    {
 3112                        personEntity.SetProviderId(id.Key, id.Value);
 3113                        saveEntity = true;
 3114                    }
 3115                }
 3116
 3117                if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
 3118                {
 3119                    personEntity.SetImage(
 3120                        new ItemImageInfo
 3121                        {
 3122                            Path = person.ImageUrl,
 3123                            Type = ImageType.Primary
 3124                        },
 3125                        0);
 3126
 3127                    saveEntity = true;
 3128                    itemUpdateType = ItemUpdateType.ImageUpdate;
 3129                }
 3130
 3131                if (saveEntity)
 3132                {
 3133                    if (createEntity)
 3134                    {
 3135                        CreateItems([personEntity], null, CancellationToken.None);
 3136                    }
 3137
 3138                    await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
 3139                    personEntity.DateLastSaved = DateTime.UtcNow;
 3140
 3141                    CreateItems([personEntity], null, CancellationToken.None);
 3142                }
 3143            }
 3144        }
 3145
 3146        private void StartScanInBackground()
 3147        {
 33148            Task.Run(() =>
 33149            {
 33150                // No need to start if scanning the library because it will handle it
 33151                ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
 33152            });
 33153        }
 3154
 3155        public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3156        {
 13157            AddMediaPathInternal(virtualFolderName, mediaPath, true);
 03158        }
 3159
 3160        private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
 3161        {
 13162            ArgumentNullException.ThrowIfNull(pathInfo);
 3163
 13164            var path = pathInfo.Path;
 3165
 13166            if (string.IsNullOrWhiteSpace(path))
 3167            {
 03168                throw new ArgumentException(nameof(path));
 3169            }
 3170
 13171            if (!Directory.Exists(path))
 3172            {
 13173                throw new FileNotFoundException("The path does not exist.");
 3174            }
 3175
 03176            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03177            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3178
 03179            var shortcutFilename = Path.GetFileNameWithoutExtension(path);
 3180
 03181            var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3182
 03183            while (File.Exists(lnk))
 3184            {
 03185                shortcutFilename += "1";
 03186                lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
 3187            }
 3188
 03189            _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
 3190
 03191            RemoveContentTypeOverrides(path);
 3192
 03193            if (saveLibraryOptions)
 3194            {
 03195                var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3196
 03197                libraryOptions.PathInfos = [.. libraryOptions.PathInfos, pathInfo];
 3198
 03199                SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3200
 03201                CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 3202            }
 03203        }
 3204
 3205        public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
 3206        {
 03207            ArgumentNullException.ThrowIfNull(mediaPath);
 3208
 03209            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 03210            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3211
 03212            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3213
 03214            SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
 3215
 03216            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03217        }
 3218
 3219        private void SyncLibraryOptionsToLocations(string virtualFolderPath, LibraryOptions options)
 3220        {
 03221            var topLibraryFolders = GetUserRootFolder().Children.ToList();
 03222            var info = GetVirtualFolderInfo(virtualFolderPath, topLibraryFolders, null);
 3223
 03224            if (info.Locations.Length > 0 && info.Locations.Length != options.PathInfos.Length)
 3225            {
 03226                var list = options.PathInfos.ToList();
 3227
 03228                foreach (var location in info.Locations)
 3229                {
 03230                    if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
 3231                    {
 03232                        list.Add(new MediaPathInfo(location));
 3233                    }
 3234                }
 3235
 03236                options.PathInfos = list.ToArray();
 3237            }
 03238        }
 3239
 3240        public async Task RemoveVirtualFolder(string name, bool refreshLibrary)
 3241        {
 3242            if (string.IsNullOrWhiteSpace(name))
 3243            {
 3244                throw new ArgumentNullException(nameof(name));
 3245            }
 3246
 3247            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 3248
 3249            var path = Path.Combine(rootFolderPath, name);
 3250
 3251            if (!Directory.Exists(path))
 3252            {
 3253                throw new FileNotFoundException("The media folder does not exist");
 3254            }
 3255
 3256            LibraryMonitor.Stop();
 3257
 3258            try
 3259            {
 3260                Directory.Delete(path, true);
 3261            }
 3262            finally
 3263            {
 3264                CollectionFolder.OnCollectionFolderChange();
 3265
 3266                if (refreshLibrary)
 3267                {
 3268                    await ValidateTopLibraryFolders(CancellationToken.None, true).ConfigureAwait(false);
 3269
 3270                    StartScanInBackground();
 3271                }
 3272                else
 3273                {
 3274                    // Need to add a delay here or directory watchers may still pick up the changes
 3275                    await Task.Delay(1000).ConfigureAwait(false);
 3276                    LibraryMonitor.Start();
 3277                }
 3278            }
 3279        }
 3280
 3281        private void RemoveContentTypeOverrides(string path)
 3282        {
 03283            if (string.IsNullOrWhiteSpace(path))
 3284            {
 03285                throw new ArgumentNullException(nameof(path));
 3286            }
 3287
 03288            List<NameValuePair>? removeList = null;
 3289
 03290            foreach (var contentType in _configurationManager.Configuration.ContentTypes)
 3291            {
 03292                if (string.IsNullOrWhiteSpace(contentType.Name)
 03293                    || _fileSystem.AreEqual(path, contentType.Name)
 03294                    || _fileSystem.ContainsSubPath(path, contentType.Name))
 3295                {
 03296                    (removeList ??= new()).Add(contentType);
 3297                }
 3298            }
 3299
 03300            if (removeList is not null)
 3301            {
 03302                _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
 03303                    .Except(removeList)
 03304                    .ToArray();
 3305
 03306                _configurationManager.SaveConfiguration();
 3307            }
 03308        }
 3309
 3310        public void RemoveMediaPath(string virtualFolderName, string mediaPath)
 3311        {
 13312            ArgumentException.ThrowIfNullOrEmpty(mediaPath);
 3313
 13314            var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 13315            var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
 3316
 13317            if (!Directory.Exists(virtualFolderPath))
 3318            {
 13319                throw new FileNotFoundException(
 13320                    string.Format(CultureInfo.InvariantCulture, "The media collection {0} does not exist", virtualFolder
 3321            }
 3322
 03323            var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
 03324                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCa
 03325                .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, String
 3326
 03327            if (!string.IsNullOrEmpty(shortcut))
 3328            {
 03329                _fileSystem.DeleteFile(shortcut);
 3330            }
 3331
 03332            var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
 3333
 03334            libraryOptions.PathInfos = libraryOptions
 03335                .PathInfos
 03336                .Where(i => !string.Equals(i.Path, mediaPath, StringComparison.Ordinal))
 03337                .ToArray();
 3338
 03339            CollectionFolder.SaveLibraryOptions(virtualFolderPath, libraryOptions);
 03340        }
 3341
 3342        private static bool ItemIsVisible(BaseItem? item, User? user)
 3343        {
 213344            if (item is null)
 3345            {
 213346                return false;
 3347            }
 3348
 03349            if (user is null)
 3350            {
 03351                return true;
 3352            }
 3353
 03354            return item is UserRootFolder || item.IsVisibleStandalone(user);
 3355        }
 3356    }
 3357}

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)