< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.Manager.ProviderManager
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/Manager/ProviderManager.cs
Line coverage
39%
Covered lines: 221
Uncovered lines: 345
Coverable lines: 566
Total lines: 1371
Line coverage: 39%
Branch coverage
45%
Covered branches: 102
Total branches: 224
Branch coverage: 45.5%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/16/2026 - 12:15:55 AM Line coverage: 31.4% (173/550) Branch coverage: 40% (85/212) Total lines: 13225/19/2026 - 12:15:13 AM Line coverage: 31.4% (173/550) Branch coverage: 41% (87/212) Total lines: 13225/20/2026 - 12:15:44 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13225/24/2026 - 12:15:27 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13225/25/2026 - 12:14:57 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13225/26/2026 - 12:15:12 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13225/27/2026 - 12:15:38 AM Line coverage: 31.4% (173/550) Branch coverage: 37.7% (80/212) Total lines: 13225/28/2026 - 12:15:50 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13225/30/2026 - 12:15:32 AM Line coverage: 31.4% (173/550) Branch coverage: 38.6% (82/212) Total lines: 13225/31/2026 - 12:15:44 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/2/2026 - 12:15:49 AM Line coverage: 31.4% (173/550) Branch coverage: 38.6% (82/212) Total lines: 13226/3/2026 - 12:16:02 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13226/4/2026 - 12:15:59 AM Line coverage: 31.4% (173/550) Branch coverage: 38.2% (81/212) Total lines: 13226/5/2026 - 12:16:38 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/6/2026 - 12:15:50 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13226/7/2026 - 12:16:09 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/8/2026 - 12:16:15 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13226/9/2026 - 12:16:23 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/10/2026 - 12:16:25 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13226/11/2026 - 12:16:04 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/13/2026 - 12:13:22 AM Line coverage: 31.4% (173/550) Branch coverage: 38.6% (82/212) Total lines: 13226/14/2026 - 12:16:28 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/19/2026 - 12:16:12 AM Line coverage: 31.4% (173/550) Branch coverage: 37.7% (80/212) Total lines: 13226/20/2026 - 12:13:11 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/24/2026 - 12:15:23 AM Line coverage: 31.4% (173/550) Branch coverage: 38.6% (82/212) Total lines: 13226/25/2026 - 12:12:37 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/26/2026 - 12:13:00 AM Line coverage: 31.4% (173/550) Branch coverage: 38.2% (81/212) Total lines: 13226/27/2026 - 12:15:59 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13227/3/2026 - 12:15:32 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13227/5/2026 - 12:16:51 AM Line coverage: 31.4% (173/550) Branch coverage: 37.7% (80/212) Total lines: 13227/6/2026 - 12:16:28 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13227/7/2026 - 12:16:09 AM Line coverage: 31.4% (173/550) Branch coverage: 38.2% (81/212) Total lines: 13227/8/2026 - 12:16:24 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13227/9/2026 - 12:13:25 AM Line coverage: 31.4% (173/550) Branch coverage: 38.2% (81/212) Total lines: 13227/10/2026 - 12:11:57 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13227/19/2026 - 12:16:13 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13227/21/2026 - 12:16:33 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13227/25/2026 - 12:14:14 AM Line coverage: 31.4% (173/550) Branch coverage: 38.2% (81/212) Total lines: 13227/26/2026 - 12:17:51 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13227/28/2026 - 12:13:49 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13227/29/2026 - 12:15:53 AM Line coverage: 31.4% (173/550) Branch coverage: 38.6% (82/212) Total lines: 13227/30/2026 - 12:15:56 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13228/2/2026 - 12:17:28 AM Line coverage: 31.4% (173/550) Branch coverage: 38.2% (81/212) Total lines: 13228/3/2026 - 12:16:46 AM Line coverage: 31.5% (175/554) Branch coverage: 37.6% (82/218) Total lines: 13388/9/2026 - 12:16:58 AM Line coverage: 32.2% (179/555) Branch coverage: 37.6% (82/218) Total lines: 13408/17/2026 - 12:18:27 AM Line coverage: 32.2% (179/555) Branch coverage: 38.5% (84/218) Total lines: 13408/18/2026 - 12:17:16 AM Line coverage: 32.2% (179/555) Branch coverage: 37.6% (82/218) Total lines: 134010/29/2026 - 12:17:11 AM Line coverage: 39% (221/566) Branch coverage: 47.3% (106/224) Total lines: 137110/30/2026 - 12:16:57 AM Line coverage: 39% (221/566) Branch coverage: 45.9% (103/224) Total lines: 137111/1/2026 - 12:15:27 AM Line coverage: 39% (221/566) Branch coverage: 46.8% (105/224) Total lines: 137111/2/2026 - 12:18:06 AM Line coverage: 39% (221/566) Branch coverage: 45.9% (103/224) Total lines: 137111/5/2026 - 12:17:36 AM Line coverage: 39% (221/566) Branch coverage: 45.5% (102/224) Total lines: 1371 5/16/2026 - 12:15:55 AM Line coverage: 31.4% (173/550) Branch coverage: 40% (85/212) Total lines: 13225/19/2026 - 12:15:13 AM Line coverage: 31.4% (173/550) Branch coverage: 41% (87/212) Total lines: 13225/20/2026 - 12:15:44 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13225/24/2026 - 12:15:27 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13225/25/2026 - 12:14:57 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13225/26/2026 - 12:15:12 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13225/27/2026 - 12:15:38 AM Line coverage: 31.4% (173/550) Branch coverage: 37.7% (80/212) Total lines: 13225/28/2026 - 12:15:50 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13225/30/2026 - 12:15:32 AM Line coverage: 31.4% (173/550) Branch coverage: 38.6% (82/212) Total lines: 13225/31/2026 - 12:15:44 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/2/2026 - 12:15:49 AM Line coverage: 31.4% (173/550) Branch coverage: 38.6% (82/212) Total lines: 13226/3/2026 - 12:16:02 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13226/4/2026 - 12:15:59 AM Line coverage: 31.4% (173/550) Branch coverage: 38.2% (81/212) Total lines: 13226/5/2026 - 12:16:38 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/6/2026 - 12:15:50 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13226/7/2026 - 12:16:09 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/8/2026 - 12:16:15 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13226/9/2026 - 12:16:23 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/10/2026 - 12:16:25 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13226/11/2026 - 12:16:04 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/13/2026 - 12:13:22 AM Line coverage: 31.4% (173/550) Branch coverage: 38.6% (82/212) Total lines: 13226/14/2026 - 12:16:28 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/19/2026 - 12:16:12 AM Line coverage: 31.4% (173/550) Branch coverage: 37.7% (80/212) Total lines: 13226/20/2026 - 12:13:11 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/24/2026 - 12:15:23 AM Line coverage: 31.4% (173/550) Branch coverage: 38.6% (82/212) Total lines: 13226/25/2026 - 12:12:37 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13226/26/2026 - 12:13:00 AM Line coverage: 31.4% (173/550) Branch coverage: 38.2% (81/212) Total lines: 13226/27/2026 - 12:15:59 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13227/3/2026 - 12:15:32 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13227/5/2026 - 12:16:51 AM Line coverage: 31.4% (173/550) Branch coverage: 37.7% (80/212) Total lines: 13227/6/2026 - 12:16:28 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13227/7/2026 - 12:16:09 AM Line coverage: 31.4% (173/550) Branch coverage: 38.2% (81/212) Total lines: 13227/8/2026 - 12:16:24 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13227/9/2026 - 12:13:25 AM Line coverage: 31.4% (173/550) Branch coverage: 38.2% (81/212) Total lines: 13227/10/2026 - 12:11:57 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13227/19/2026 - 12:16:13 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13227/21/2026 - 12:16:33 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13227/25/2026 - 12:14:14 AM Line coverage: 31.4% (173/550) Branch coverage: 38.2% (81/212) Total lines: 13227/26/2026 - 12:17:51 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13227/28/2026 - 12:13:49 AM Line coverage: 31.4% (173/550) Branch coverage: 36.7% (78/212) Total lines: 13227/29/2026 - 12:15:53 AM Line coverage: 31.4% (173/550) Branch coverage: 38.6% (82/212) Total lines: 13227/30/2026 - 12:15:56 AM Line coverage: 31.4% (173/550) Branch coverage: 37.2% (79/212) Total lines: 13228/2/2026 - 12:17:28 AM Line coverage: 31.4% (173/550) Branch coverage: 38.2% (81/212) Total lines: 13228/3/2026 - 12:16:46 AM Line coverage: 31.5% (175/554) Branch coverage: 37.6% (82/218) Total lines: 13388/9/2026 - 12:16:58 AM Line coverage: 32.2% (179/555) Branch coverage: 37.6% (82/218) Total lines: 13408/17/2026 - 12:18:27 AM Line coverage: 32.2% (179/555) Branch coverage: 38.5% (84/218) Total lines: 13408/18/2026 - 12:17:16 AM Line coverage: 32.2% (179/555) Branch coverage: 37.6% (82/218) Total lines: 134010/29/2026 - 12:17:11 AM Line coverage: 39% (221/566) Branch coverage: 47.3% (106/224) Total lines: 137110/30/2026 - 12:16:57 AM Line coverage: 39% (221/566) Branch coverage: 45.9% (103/224) Total lines: 137111/1/2026 - 12:15:27 AM Line coverage: 39% (221/566) Branch coverage: 46.8% (105/224) Total lines: 137111/2/2026 - 12:18:06 AM Line coverage: 39% (221/566) Branch coverage: 45.9% (103/224) Total lines: 137111/5/2026 - 12:17:36 AM Line coverage: 39% (221/566) Branch coverage: 45.5% (102/224) Total lines: 1371

Coverage delta

Coverage delta 10 -10

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
AddParts(...)100%11100%
RefreshSingleItem(...)100%44100%
SaveImage()0%342180%
SaveImage(...)100%210%
SaveImage()0%620%
SaveImage(...)100%210%
GetAvailableRemoteImages()0%2040%
GetImages()0%2040%
GetRemoteImageProviderInfo(...)100%210%
GetRemoteImageProviders(...)100%210%
GetImageProviders(...)100%11100%
GetImageProvidersInternal(...)100%44100%
CanRefreshImages(...)78.57%141492.85%
GetMetadataProviders(...)100%11100%
GetMetadataProviders(...)100%210%
GetLibraryPathForItem(...)87.5%88100%
GetMetadataSavers(...)100%11100%
GetMetadataProvidersInternal(...)100%11100%
GetOrCreateOrderedProviders(...)90%1010100%
CanRefreshMetadataForCache(...)75%151271.42%
CanRefreshMetadata(...)75%312476.92%
GetConfiguredOrder(...)100%22100%
GetDefaultOrder(...)100%22100%
GetAllMetadataPlugins()100%210%
GetPluginSummary()0%7280%
AddMetadataPlugins(...)0%156120%
AddImagePlugins(...)100%210%
GetMetadataOptions(...)100%22100%
SaveMetadataAsync(...)100%11100%
SaveMetadataAsync(...)100%210%
SaveMetadataAsync()12.5%43817.85%
IsSaverEnabledForItem(...)5.55%1771821.05%
GetRemoteSearchResults(...)0%620%
GetRemoteSearchResults()0%342180%
GetExternalIds(...)100%210%
GetExternalUrls(...)100%11100%
GetExternalIdInfos(...)100%210%
GetRefreshQueue()100%11100%
OnRefreshStart(...)50%2262.5%
OnRefreshComplete(...)50%2262.5%
GetRefreshProgress(...)0%620%
OnRefreshProgress(...)75%9875%
QueueRefresh(...)66.66%6681.81%
StartProcessingRefreshQueue()78.57%211467.74%
RefreshItem()75%5460%
RefreshCollectionFolderChildren()0%620%
RefreshArtist()100%210%
RefreshFullItem(...)100%210%
Dispose()100%11100%
Dispose(...)50%6690.9%
OnLibraryOptionsUpdated(...)100%22100%
OnConfigurationUpdated(...)100%11100%
ClearMetadataProviderCache()100%11100%

File(s)

/srv/git/jellyfin/MediaBrowser.Providers/Manager/ProviderManager.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.Globalization;
 5using System.IO;
 6using System.Linq;
 7using System.Net;
 8using System.Net.Http;
 9using System.Net.Mime;
 10using System.Text.Json;
 11using System.Threading;
 12using System.Threading.Tasks;
 13using AsyncKeyedLock;
 14using Jellyfin.Data.Enums;
 15using Jellyfin.Data.Events;
 16using Jellyfin.Extensions;
 17using Jellyfin.Extensions.Json;
 18using MediaBrowser.Common.Net;
 19using MediaBrowser.Controller;
 20using MediaBrowser.Controller.BaseItemManager;
 21using MediaBrowser.Controller.Configuration;
 22using MediaBrowser.Controller.Dto;
 23using MediaBrowser.Controller.Entities;
 24using MediaBrowser.Controller.Entities.Audio;
 25using MediaBrowser.Controller.Entities.Movies;
 26using MediaBrowser.Controller.Library;
 27using MediaBrowser.Controller.Lyrics;
 28using MediaBrowser.Controller.MediaSegments;
 29using MediaBrowser.Controller.Providers;
 30using MediaBrowser.Controller.Subtitles;
 31using MediaBrowser.Model.Configuration;
 32using MediaBrowser.Model.Entities;
 33using MediaBrowser.Model.Extensions;
 34using MediaBrowser.Model.IO;
 35using MediaBrowser.Model.Net;
 36using MediaBrowser.Model.Providers;
 37using MediaBrowser.Model.Querying;
 38using Microsoft.Extensions.Caching.Memory;
 39using Microsoft.Extensions.Logging;
 40using Book = MediaBrowser.Controller.Entities.Book;
 41using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 42using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
 43using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
 44using Season = MediaBrowser.Controller.Entities.TV.Season;
 45using Series = MediaBrowser.Controller.Entities.TV.Series;
 46
 47namespace MediaBrowser.Providers.Manager
 48{
 49    /// <summary>
 50    /// Class ProviderManager.
 51    /// </summary>
 52    public class ProviderManager : IProviderManager, IDisposable
 53    {
 9954        private readonly Lock _refreshQueueLock = new();
 55        private readonly ILogger<ProviderManager> _logger;
 56        private readonly IHttpClientFactory _httpClientFactory;
 57        private readonly ILibraryMonitor _libraryMonitor;
 58        private readonly IFileSystem _fileSystem;
 59        private readonly IServerApplicationPaths _appPaths;
 60        private readonly ILibraryManager _libraryManager;
 61        private readonly ISubtitleManager _subtitleManager;
 62        private readonly ILyricManager _lyricManager;
 63        private readonly IServerConfigurationManager _configurationManager;
 64        private readonly IBaseItemManager _baseItemManager;
 9965        private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = new();
 9966        private readonly CancellationTokenSource _disposeCancellationTokenSource = new();
 9967        private readonly PriorityQueue<(Guid ItemId, MetadataRefreshOptions RefreshOptions), RefreshPriority> _refreshQu
 68        private readonly IMemoryCache _memoryCache;
 69        private readonly IMediaSegmentManager _mediaSegmentManager;
 70        private readonly ISimilarItemsManager _similarItemsManager;
 9971        private readonly AsyncKeyedLocker<string> _imageSaveLock = new(o =>
 9972        {
 9973            o.PoolSize = 20;
 9974            o.PoolInitialFill = 1;
 9975        });
 76
 77        /// <summary>
 78        /// Cache for ordered metadata providers per library/item type combination.
 79        /// Key: (LibraryPath, ItemTypeName, IncludeDisabled, ForceEnableInternetMetadata), where
 80        /// LibraryPath is the collection folder path the library options are stored against.
 81        /// Value: Array of ordered metadata providers (before per-item filtering).
 82        /// </summary>
 9983        private readonly ConcurrentDictionary<MetadataProviderCacheKey, IMetadataProvider[]> _metadataProviderCache = ne
 84
 9985        private IImageProvider[] _imageProviders = [];
 9986        private IMetadataService[] _metadataServices = [];
 9987        private IMetadataProvider[] _metadataProviders = [];
 9988        private IMetadataSaver[] _savers = [];
 9989        private IExternalId[] _externalIds = [];
 9990        private IExternalUrlProvider[] _externalUrlProviders = [];
 91        private bool _isProcessingRefreshQueue;
 92        private bool _disposed;
 93
 94        /// <summary>
 95        /// Initializes a new instance of the <see cref="ProviderManager"/> class.
 96        /// </summary>
 97        /// <param name="httpClientFactory">The Http client factory.</param>
 98        /// <param name="subtitleManager">The subtitle manager.</param>
 99        /// <param name="configurationManager">The configuration manager.</param>
 100        /// <param name="libraryMonitor">The library monitor.</param>
 101        /// <param name="logger">The logger.</param>
 102        /// <param name="fileSystem">The filesystem.</param>
 103        /// <param name="appPaths">The server application paths.</param>
 104        /// <param name="libraryManager">The library manager.</param>
 105        /// <param name="baseItemManager">The BaseItem manager.</param>
 106        /// <param name="lyricManager">The lyric manager.</param>
 107        /// <param name="memoryCache">The memory cache.</param>
 108        /// <param name="mediaSegmentManager">The media segment manager.</param>
 109        /// <param name="similarItemsManager">The similar items manager.</param>
 110        public ProviderManager(
 111            IHttpClientFactory httpClientFactory,
 112            ISubtitleManager subtitleManager,
 113            IServerConfigurationManager configurationManager,
 114            ILibraryMonitor libraryMonitor,
 115            ILogger<ProviderManager> logger,
 116            IFileSystem fileSystem,
 117            IServerApplicationPaths appPaths,
 118            ILibraryManager libraryManager,
 119            IBaseItemManager baseItemManager,
 120            ILyricManager lyricManager,
 121            IMemoryCache memoryCache,
 122            IMediaSegmentManager mediaSegmentManager,
 123            ISimilarItemsManager similarItemsManager)
 124        {
 99125            _logger = logger;
 99126            _httpClientFactory = httpClientFactory;
 99127            _configurationManager = configurationManager;
 99128            _libraryMonitor = libraryMonitor;
 99129            _fileSystem = fileSystem;
 99130            _appPaths = appPaths;
 99131            _libraryManager = libraryManager;
 99132            _subtitleManager = subtitleManager;
 99133            _baseItemManager = baseItemManager;
 99134            _lyricManager = lyricManager;
 99135            _memoryCache = memoryCache;
 99136            _mediaSegmentManager = mediaSegmentManager;
 99137            _similarItemsManager = similarItemsManager;
 138
 99139            CollectionFolder.LibraryOptionsUpdated += OnLibraryOptionsUpdated;
 99140            _configurationManager.ConfigurationUpdated += OnConfigurationUpdated;
 99141        }
 142
 143        /// <inheritdoc/>
 144        public event EventHandler<GenericEventArgs<BaseItem>>? RefreshStarted;
 145
 146        /// <inheritdoc/>
 147        public event EventHandler<GenericEventArgs<BaseItem>>? RefreshCompleted;
 148
 149        /// <inheritdoc/>
 150        public event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>>? RefreshProgress;
 151
 152        /// <inheritdoc/>
 153        public void AddParts(
 154            IEnumerable<IImageProvider> imageProviders,
 155            IEnumerable<IMetadataService> metadataServices,
 156            IEnumerable<IMetadataProvider> metadataProviders,
 157            IEnumerable<IMetadataSaver> metadataSavers,
 158            IEnumerable<IExternalId> externalIds,
 159            IEnumerable<IExternalUrlProvider> externalUrlProviders)
 160        {
 97161            _imageProviders = imageProviders.ToArray();
 97162            _metadataServices = metadataServices.OrderBy(i => i.Order).ToArray();
 97163            _metadataProviders = metadataProviders.ToArray();
 97164            _externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray();
 97165            _externalUrlProviders = externalUrlProviders.OrderBy(i => i.Name).ToArray();
 166
 97167            _savers = metadataSavers.ToArray();
 168
 97169            ClearMetadataProviderCache();
 97170        }
 171
 172        /// <inheritdoc/>
 173        public Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken c
 174        {
 113175            var type = item.GetType();
 176
 113177            var service = _metadataServices.FirstOrDefault(current => current.CanRefreshPrimary(type))
 113178                ?? _metadataServices.FirstOrDefault(current => current.CanRefresh(item));
 179
 113180            if (service is null)
 181            {
 1182                _logger.LogError("Unable to find a metadata service for item of type {TypeName}", type.Name);
 1183                return Task.FromResult(ItemUpdateType.None);
 184            }
 185
 112186            return service.RefreshMetadata(item, options, cancellationToken);
 187        }
 188
 189        /// <inheritdoc/>
 190        public async Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancel
 191        {
 0192            using (await _imageSaveLock.LockAsync(url, cancellationToken).ConfigureAwait(false))
 193            {
 0194                if (_memoryCache.TryGetValue(url, out (string ContentType, byte[] ImageContents)? cachedValue)
 0195                    && cachedValue is not null)
 196                {
 0197                    var imageContents = cachedValue.Value.ImageContents;
 0198                    var cacheStream = new MemoryStream(imageContents, 0, imageContents.Length, false);
 0199                    await using (cacheStream.ConfigureAwait(false))
 200                    {
 0201                        await SaveImage(
 0202                            item,
 0203                            cacheStream,
 0204                            cachedValue.Value.ContentType,
 0205                            type,
 0206                            imageIndex,
 0207                            cancellationToken).ConfigureAwait(false);
 0208                        return;
 209                    }
 210                }
 211
 0212                var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
 0213                using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
 214
 0215                response.EnsureSuccessStatusCode();
 216
 0217                var contentType = response.Content.Headers.ContentType?.MediaType;
 218
 219                // Workaround for tvheadend channel icons
 220                // TODO: Isolate this hack into the tvh plugin
 0221                if (string.IsNullOrEmpty(contentType))
 222                {
 223                    // Special case for imagecache
 0224                    if (url.Contains("/imagecache/", StringComparison.OrdinalIgnoreCase))
 225                    {
 0226                        contentType = MediaTypeNames.Image.Png;
 227                    }
 228                }
 229
 230                // some providers don't correctly report media type, extract from url if no extension found
 0231                if (contentType is null || contentType.Equals(MediaTypeNames.Application.Octet, StringComparison.Ordinal
 232                {
 233                    // Strip query parameters from url to get actual path.
 0234                    contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path));
 235                }
 236
 0237                if (!contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
 238                {
 0239                    throw new HttpRequestException($"Request returned '{contentType}' instead of an image type", null, H
 240                }
 241
 0242                var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false)
 0243                var stream = new MemoryStream(responseBytes, 0, responseBytes.Length, false);
 0244                await using (stream.ConfigureAwait(false))
 245                {
 0246                    _memoryCache.Set(url, (contentType, responseBytes), TimeSpan.FromSeconds(10));
 247
 0248                    await SaveImage(
 0249                        item,
 0250                        stream,
 0251                        contentType,
 0252                        type,
 0253                        imageIndex,
 0254                        cancellationToken).ConfigureAwait(false);
 255                }
 0256            }
 0257        }
 258
 259        /// <inheritdoc/>
 260        public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, Cancellati
 261        {
 0262            return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, source, 
 263        }
 264
 265        /// <inheritdoc/>
 266        public async Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool
 267        {
 0268            if (string.IsNullOrWhiteSpace(source))
 269            {
 0270                throw new ArgumentNullException(nameof(source));
 271            }
 272
 273            try
 274            {
 0275                var fileStream = AsyncFile.OpenRead(source);
 0276                await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger)
 0277                    .SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken)
 0278                    .ConfigureAwait(false);
 0279            }
 280            finally
 281            {
 282                try
 283                {
 0284                    File.Delete(source);
 0285                }
 0286                catch (Exception ex)
 287                {
 0288                    _logger.LogError(ex, "Source file {Source} not found or in use, skip removing", source);
 0289                }
 290            }
 0291        }
 292
 293        /// <inheritdoc/>
 294        public Task SaveImage(Stream source, string mimeType, string path)
 295        {
 0296            return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger)
 0297                .SaveImage(source, path);
 298        }
 299
 300        /// <inheritdoc/>
 301        public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, 
 302        {
 0303            var providers = GetRemoteImageProviders(item, query.IncludeDisabledProviders);
 304
 0305            if (!string.IsNullOrEmpty(query.ProviderName))
 306            {
 0307                var providerName = query.ProviderName;
 308
 0309                providers = providers.Where(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase)
 310            }
 311
 0312            if (query.ImageType is not null)
 313            {
 0314                providers = providers.Where(i => i.GetSupportedImages(item).Contains(query.ImageType.Value));
 315            }
 316
 0317            var preferredLanguage = item.GetPreferredMetadataLanguage();
 318
 0319            var tasks = providers.Select(i => GetImages(item, i, preferredLanguage, query.IncludeAllLanguages, cancellat
 320
 0321            var results = await Task.WhenAll(tasks).ConfigureAwait(false);
 322
 0323            return results.SelectMany(i => i);
 0324        }
 325
 326        /// <summary>
 327        /// Gets the images.
 328        /// </summary>
 329        /// <param name="item">The item.</param>
 330        /// <param name="provider">The provider.</param>
 331        /// <param name="preferredLanguage">The preferred language.</param>
 332        /// <param name="includeAllLanguages">Whether to include all languages in results.</param>
 333        /// <param name="cancellationToken">The cancellation token.</param>
 334        /// <param name="type">The type.</param>
 335        /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
 336        private async Task<IEnumerable<RemoteImageInfo>> GetImages(
 337            BaseItem item,
 338            IRemoteImageProvider provider,
 339            string preferredLanguage,
 340            bool includeAllLanguages,
 341            CancellationToken cancellationToken,
 342            ImageType? type = null)
 343        {
 0344            bool hasPreferredLanguage = !string.IsNullOrWhiteSpace(preferredLanguage);
 345
 346            try
 347            {
 0348                var result = await provider.GetImages(item, cancellationToken).ConfigureAwait(false);
 349
 0350                if (type.HasValue)
 351                {
 0352                    result = result.Where(i => i.Type == type.Value);
 353                }
 354
 0355                if (!includeAllLanguages && hasPreferredLanguage)
 356                {
 357                    // Filter out languages that do not match the preferred languages.
 358                    //
 359                    // TODO: should exception case of "en" (English) eventually be removed?
 0360                    result = result.Where(i => string.IsNullOrWhiteSpace(i.Language) ||
 0361                                               string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgno
 0362                                               string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase));
 363                }
 364
 0365                return result.OrderByLanguageDescending(preferredLanguage);
 366            }
 0367            catch (OperationCanceledException)
 368            {
 0369                return Enumerable.Empty<RemoteImageInfo>();
 370            }
 0371            catch (Exception ex)
 372            {
 0373                _logger.LogError(ex, "{ProviderName} failed in GetImageInfos for type {ItemType} at {ItemPath}", provide
 0374                return Enumerable.Empty<RemoteImageInfo>();
 375            }
 0376        }
 377
 378        /// <inheritdoc/>
 379        public IEnumerable<ImageProviderInfo> GetRemoteImageProviderInfo(BaseItem item)
 380        {
 0381            return GetRemoteImageProviders(item, true).Select(i => new ImageProviderInfo(i.Name, i.GetSupportedImages(it
 382        }
 383
 384        private IEnumerable<IRemoteImageProvider> GetRemoteImageProviders(BaseItem item, bool includeDisabled)
 385        {
 0386            var options = GetMetadataOptions(item);
 0387            var libraryOptions = _libraryManager.GetLibraryOptions(item);
 388
 0389            return GetImageProvidersInternal(
 0390                item,
 0391                libraryOptions,
 0392                options,
 0393                new ImageRefreshOptions(new DirectoryService(_fileSystem)),
 0394                includeDisabled).OfType<IRemoteImageProvider>();
 395        }
 396
 397        /// <inheritdoc/>
 398        public IEnumerable<IImageProvider> GetImageProviders(BaseItem item, ImageRefreshOptions refreshOptions)
 399        {
 131400            return GetImageProvidersInternal(item, _libraryManager.GetLibraryOptions(item), GetMetadataOptions(item), re
 401        }
 402
 403        private IEnumerable<IImageProvider> GetImageProvidersInternal(BaseItem item, LibraryOptions libraryOptions, Meta
 404        {
 131405            var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
 131406            var fetcherOrder = typeOptions?.ImageFetcherOrder ?? options.ImageFetcherOrder;
 407
 131408            return _imageProviders.Where(i => CanRefreshImages(i, item, typeOptions, refreshOptions, includeDisabled))
 131409                .OrderBy(i => GetConfiguredOrder(fetcherOrder, i.Name))
 131410                .ThenBy(GetDefaultOrder);
 411        }
 412
 413        private bool CanRefreshImages(
 414            IImageProvider provider,
 415            BaseItem item,
 416            TypeOptions? libraryTypeOptions,
 417            ImageRefreshOptions refreshOptions,
 418            bool includeDisabled)
 419        {
 420            try
 421            {
 2188422                if (!provider.Supports(item))
 423                {
 1932424                    return false;
 425                }
 255426            }
 1427            catch (Exception ex)
 428            {
 1429                _logger.LogError(ex, "{ProviderName} failed in Supports for type {ItemType} at {ItemPath}", provider.Get
 1430                return false;
 431            }
 432
 255433            if (includeDisabled || provider is ILocalImageProvider)
 434            {
 248435                return true;
 436            }
 437
 7438            if (item.IsLocked && refreshOptions.ImageRefreshMode != MetadataRefreshMode.FullRefresh)
 439            {
 1440                return false;
 441            }
 442
 443            // Extras have no identity of their own in an online database, so remote artwork for them
 444            // is always some other item's. Local and dynamic providers still apply, so an extra can
 445            // keep an embedded thumbnail or an extracted frame.
 6446            if (item.ExtraType.HasValue && provider is IRemoteImageProvider)
 447            {
 0448                return false;
 449            }
 450
 6451            return _baseItemManager.IsImageFetcherEnabled(item, libraryTypeOptions, provider.Name);
 1933452        }
 453
 454        /// <inheritdoc />
 455        public IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions)
 456            where T : BaseItem
 457        {
 147458            var globalMetadataOptions = GetMetadataOptions(item);
 147459            var libraryPath = GetLibraryPathForItem(item);
 460
 147461            return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false, libraryPat
 462        }
 463
 464        /// <summary>
 465        /// Gets metadata providers for the specified item.
 466        /// </summary>
 467        /// <typeparam name="T">The item type.</typeparam>
 468        /// <param name="item">The item.</param>
 469        /// <param name="libraryOptions">The library options.</param>
 470        /// <param name="includeDisabled">Whether to include disabled providers.</param>
 471        /// <returns>The metadata providers.</returns>
 472        public IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions, b
 473            where T : BaseItem
 474        {
 0475            var globalMetadataOptions = GetMetadataOptions(item);
 0476            var libraryPath = GetLibraryPathForItem(item);
 477
 0478            return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, includeDisabled, false, 
 479        }
 480
 481        private string GetLibraryPathForItem(BaseItem item)
 482        {
 147483            if (item is CollectionFolder collectionFolder)
 484            {
 44485                return collectionFolder.Path ?? string.Empty;
 486            }
 487
 103488            return _libraryManager.GetCollectionFolders(item)
 103489                .Find(folder => folder is CollectionFolder)?.Path ?? string.Empty;
 490        }
 491
 492        /// <inheritdoc />
 493        public IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions)
 494        {
 47495            return _savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, ItemUpdateType.MetadataEdit, false)
 496        }
 497
 498        private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryO
 499            where T : BaseItem
 500        {
 147501            var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name);
 502
 147503            var orderedProviders = GetOrCreateOrderedProviders<T>(item.GetType().Name, libraryOptions, globalMetadataOpt
 504
 147505            return orderedProviders.Where(i => CanRefreshMetadata(i, item, typeOptions, includeDisabled, forceEnableInte
 506        }
 507
 508        private IMetadataProvider<T>[] GetOrCreateOrderedProviders<T>(
 509            string itemTypeName,
 510            LibraryOptions libraryOptions,
 511            MetadataOptions globalMetadataOptions,
 512            bool includeDisabled,
 513            bool forceEnableInternetMetadata,
 514            string libraryPath)
 515            where T : BaseItem
 516        {
 147517            var cacheKey = new MetadataProviderCacheKey(libraryPath, itemTypeName, includeDisabled, forceEnableInternetM
 147518            if (_metadataProviderCache.TryGetValue(cacheKey, out var cachedProviders))
 519            {
 62520                return cachedProviders.OfType<IMetadataProvider<T>>().ToArray();
 521            }
 522
 85523            var localMetadataReaderOrder = libraryOptions.LocalMetadataReaderOrder ?? globalMetadataOptions.LocalMetadat
 85524            var typeOptions = libraryOptions.GetTypeOptions(itemTypeName);
 85525            var metadataFetcherOrder = typeOptions?.MetadataFetcherOrder ?? globalMetadataOptions.MetadataFetcherOrder;
 526
 85527            var orderedProviders = _metadataProviders.OfType<IMetadataProvider<T>>()
 85528                .Where(i => CanRefreshMetadataForCache(i, typeOptions, includeDisabled, forceEnableInternetMetadata))
 85529                .OrderBy(i =>
 85530                    // local and remote providers will be interleaved in the final order
 85531                    // only relative order within a type matters: consumers of the list filter to one or the other
 85532                    i switch
 85533                    {
 85534                        ILocalMetadataProvider => GetConfiguredOrder(localMetadataReaderOrder, i.Name),
 85535                        IRemoteMetadataProvider => GetConfiguredOrder(metadataFetcherOrder, i.Name),
 85536                        // Default to end
 85537                        _ => int.MaxValue
 85538                    })
 85539                .ThenBy(GetDefaultOrder)
 85540                .ToArray();
 541
 85542            _metadataProviderCache.TryAdd(cacheKey, orderedProviders.Cast<IMetadataProvider>().ToArray());
 543
 85544            return orderedProviders;
 545        }
 546
 547        private static bool CanRefreshMetadataForCache(
 548            IMetadataProvider provider,
 549            TypeOptions? libraryTypeOptions,
 550            bool includeDisabled,
 551            bool forceEnableInternetMetadata)
 552        {
 152553            if (includeDisabled)
 554            {
 0555                return true;
 556            }
 557
 152558            if (forceEnableInternetMetadata || provider is not IRemoteMetadataProvider)
 559            {
 104560                return true;
 561            }
 562
 48563            if (libraryTypeOptions?.MetadataFetchers is { Length: > 0 } metadataFetchers)
 564            {
 0565                return metadataFetchers.Contains(provider.Name, StringComparer.OrdinalIgnoreCase);
 566            }
 567
 48568            return true;
 569        }
 570
 571        private bool CanRefreshMetadata(
 572            IMetadataProvider provider,
 573            BaseItem item,
 574            TypeOptions? libraryTypeOptions,
 575            bool includeDisabled,
 576            bool forceEnableInternetMetadata)
 577        {
 214578            if (!item.SupportsLocalMetadata && provider is ILocalMetadataProvider)
 579            {
 1580                return false;
 581            }
 582
 213583            if (includeDisabled)
 584            {
 0585                return true;
 586            }
 587
 588            // If locked only allow local providers
 213589            if (item.IsLocked && provider is not ILocalMetadataProvider && provider is not IForcedProvider)
 590            {
 3591                return false;
 592            }
 593
 210594            if (forceEnableInternetMetadata || provider is not IRemoteMetadataProvider)
 595            {
 163596                return true;
 597            }
 598
 599            // An extra is a local file belonging to another item and has no identity of its own in an
 600            // online database. Looking it up matches whatever the surrounding folder happens to be
 601            // called and overwrites the extra's name with a different item's title.
 47602            if (item.ExtraType.HasValue && provider is IRemoteMetadataProvider)
 603            {
 0604                return false;
 605            }
 606
 607            // Artists without a folder structure that are derived from metadata have no real path in the library,
 608            // so GetLibraryOptions returns null. Allow all providers through rather than blocking them.
 47609            if (item is MusicArtist && libraryTypeOptions is null)
 610            {
 0611                return true;
 612            }
 613
 47614            return _baseItemManager.IsMetadataFetcherEnabled(item, libraryTypeOptions, provider.Name);
 615        }
 616
 617        private static int GetConfiguredOrder(string[] order, string providerName)
 618        {
 327619            var index = Array.IndexOf(order, providerName);
 620
 327621            if (index != -1)
 622            {
 54623                return index;
 624            }
 625
 626            // default to end
 273627            return int.MaxValue;
 628        }
 629
 630        private static int GetDefaultOrder(object provider)
 631        {
 327632            if (provider is IHasOrder hasOrder)
 633            {
 229634                return hasOrder.Order;
 635            }
 636
 637            // after items that want to be first (~0) but before items that want to be last (~100)
 98638            return 50;
 639        }
 640
 641        /// <inheritdoc/>
 642        public MetadataPluginSummary[] GetAllMetadataPlugins()
 643        {
 0644            return new[]
 0645            {
 0646                GetPluginSummary<Movie>(),
 0647                GetPluginSummary<BoxSet>(),
 0648                GetPluginSummary<Book>(),
 0649                GetPluginSummary<Series>(),
 0650                GetPluginSummary<Season>(),
 0651                GetPluginSummary<Episode>(),
 0652                GetPluginSummary<MusicAlbum>(),
 0653                GetPluginSummary<MusicArtist>(),
 0654                GetPluginSummary<Audio>(),
 0655                GetPluginSummary<AudioBook>(),
 0656                GetPluginSummary<Studio>(),
 0657                GetPluginSummary<MusicVideo>(),
 0658                GetPluginSummary<Video>()
 0659            };
 660        }
 661
 662        private MetadataPluginSummary GetPluginSummary<T>()
 663            where T : BaseItem, new()
 664        {
 665            // Give it a dummy path just so that it looks like a file system item
 0666            var dummy = new T
 0667            {
 0668                Path = Path.Combine(_appPaths.InternalMetadataPath, "dummy"),
 0669                ParentId = Guid.NewGuid()
 0670            };
 671
 0672            var options = GetMetadataOptions(dummy);
 673
 0674            var summary = new MetadataPluginSummary
 0675            {
 0676                ItemType = typeof(T).Name
 0677            };
 678
 0679            var libraryOptions = new LibraryOptions();
 680
 0681            var imageProviders = GetImageProvidersInternal(
 0682                dummy,
 0683                libraryOptions,
 0684                options,
 0685                new ImageRefreshOptions(new DirectoryService(_fileSystem)),
 0686                true).ToList();
 687
 0688            var pluginList = summary.Plugins.ToList();
 689
 0690            AddMetadataPlugins(pluginList, dummy, libraryOptions, options);
 0691            AddImagePlugins(pluginList, imageProviders);
 692
 693            // Subtitle fetchers
 0694            var subtitleProviders = _subtitleManager.GetSupportedProviders(dummy);
 0695            pluginList.AddRange(subtitleProviders.Select(i => new MetadataPlugin
 0696            {
 0697                Name = i.Name,
 0698                Type = MetadataPluginType.SubtitleFetcher
 0699            }));
 700
 701            // Lyric fetchers
 0702            var lyricProviders = _lyricManager.GetSupportedProviders(dummy);
 0703            pluginList.AddRange(lyricProviders.Select(i => new MetadataPlugin
 0704            {
 0705                Name = i.Name,
 0706                Type = MetadataPluginType.LyricFetcher
 0707            }));
 708
 709            // Media segment providers
 0710            var mediaSegmentProviders = _mediaSegmentManager.GetSupportedProviders(dummy);
 0711            pluginList.AddRange(mediaSegmentProviders.Select(i => new MetadataPlugin
 0712            {
 0713                Name = i.Name,
 0714                Type = MetadataPluginType.MediaSegmentProvider
 0715            }));
 716
 717            // Similar items providers
 0718            var similarItemsProviders = _similarItemsManager.GetSimilarItemsProviders<T>();
 0719            pluginList.AddRange(similarItemsProviders.Select(i => new MetadataPlugin
 0720            {
 0721                Name = i.Name,
 0722                Type = i.Type
 0723            }));
 724
 0725            summary.Plugins = pluginList.ToArray();
 726
 0727            var supportedImageTypes = imageProviders.OfType<IRemoteImageProvider>()
 0728                .SelectMany(i => i.GetSupportedImages(dummy))
 0729                .ToList();
 730
 0731            supportedImageTypes.AddRange(imageProviders.OfType<IDynamicImageProvider>()
 0732                .SelectMany(i => i.GetSupportedImages(dummy)));
 733
 0734            summary.SupportedImageTypes = supportedImageTypes.Distinct().ToArray();
 735
 0736            return summary;
 737        }
 738
 739        private void AddMetadataPlugins<T>(List<MetadataPlugin> list, T item, LibraryOptions libraryOptions, MetadataOpt
 740            where T : BaseItem
 741        {
 0742            var libraryPath = GetLibraryPathForItem(item);
 0743            var providers = GetMetadataProvidersInternal<T>(item, libraryOptions, options, true, true, libraryPath).ToLi
 744
 745            // Locals
 0746            list.AddRange(providers.Where(i => i is ILocalMetadataProvider).Select(i => new MetadataPlugin
 0747            {
 0748                Name = i.Name,
 0749                Type = MetadataPluginType.LocalMetadataProvider
 0750            }));
 751
 752            // Fetchers
 0753            list.AddRange(providers.Where(i => i is IRemoteMetadataProvider).Select(i => new MetadataPlugin
 0754            {
 0755                Name = i.Name,
 0756                Type = MetadataPluginType.MetadataFetcher
 0757            }));
 758
 759            // Savers
 0760            list.AddRange(_savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, ItemUpdateType.MetadataEdit,
 0761            {
 0762                Name = i.Name,
 0763                Type = MetadataPluginType.MetadataSaver
 0764            }));
 0765        }
 766
 767        private void AddImagePlugins(List<MetadataPlugin> list, List<IImageProvider> imageProviders)
 768        {
 769            // Locals
 0770            list.AddRange(imageProviders.Where(i => i is ILocalImageProvider).Select(i => new MetadataPlugin
 0771            {
 0772                Name = i.Name,
 0773                Type = MetadataPluginType.LocalImageProvider
 0774            }));
 775
 776            // Fetchers
 0777            list.AddRange(imageProviders.Where(i => i is IDynamicImageProvider || (i is IRemoteImageProvider)).Select(i 
 0778            {
 0779                Name = i.Name,
 0780                Type = MetadataPluginType.ImageFetcher
 0781            }));
 0782        }
 783
 784        /// <inheritdoc/>
 785        public MetadataOptions GetMetadataOptions(BaseItem item)
 1830786            => _configurationManager.GetMetadataOptionsForType(item.GetType().Name) ?? new MetadataOptions();
 787
 788        /// <inheritdoc/>
 789        public Task SaveMetadataAsync(BaseItem item, ItemUpdateType updateType)
 147790            => SaveMetadataAsync(item, updateType, _savers);
 791
 792        /// <inheritdoc/>
 793        public Task SaveMetadataAsync(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers)
 0794            => SaveMetadataAsync(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparison.OrdinalIg
 795
 796        /// <summary>
 797        /// Saves the metadata.
 798        /// </summary>
 799        /// <param name="item">The item.</param>
 800        /// <param name="updateType">Type of the update.</param>
 801        /// <param name="savers">The savers.</param>
 802        private async Task SaveMetadataAsync(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> saver
 803        {
 147804            var libraryOptions = _libraryManager.GetLibraryOptions(item);
 147805            var applicableSavers = savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, updateType, false)).
 147806            if (applicableSavers.Count == 0)
 807            {
 147808                return;
 809            }
 810
 0811            foreach (var saver in applicableSavers)
 812            {
 0813                _logger.LogDebug("Saving {Item} to {Saver}", item.Path ?? item.Name, saver.Name);
 814
 0815                if (saver is IMetadataFileSaver fileSaver)
 816                {
 817                    string path;
 818
 819                    try
 820                    {
 0821                        path = fileSaver.GetSavePath(item);
 0822                    }
 0823                    catch (Exception ex)
 824                    {
 0825                        _logger.LogError(ex, "Error in {Saver} GetSavePath", saver.Name);
 0826                        continue;
 827                    }
 828
 829                    try
 830                    {
 0831                        _libraryMonitor.ReportFileSystemChangeBeginning(path);
 0832                        await saver.SaveAsync(item, CancellationToken.None).ConfigureAwait(false);
 0833                        item.DateLastSaved = DateTime.UtcNow;
 0834                    }
 0835                    catch (Exception ex)
 836                    {
 0837                        _logger.LogError(ex, "Error in metadata saver");
 0838                    }
 839                    finally
 840                    {
 0841                        _libraryMonitor.ReportFileSystemChangeComplete(path, false);
 842                    }
 0843                }
 844                else
 845                {
 846                    try
 847                    {
 0848                        await saver.SaveAsync(item, CancellationToken.None).ConfigureAwait(false);
 0849                        item.DateLastSaved = DateTime.UtcNow;
 0850                    }
 0851                    catch (Exception ex)
 852                    {
 0853                        _logger.LogError(ex, "Error in metadata saver");
 0854                    }
 855                }
 856            }
 147857        }
 858
 859        /// <summary>
 860        /// Determines whether [is saver enabled for item] [the specified saver].
 861        /// </summary>
 862        private bool IsSaverEnabledForItem(IMetadataSaver saver, BaseItem item, LibraryOptions libraryOptions, ItemUpdat
 863        {
 1552864            var options = GetMetadataOptions(item);
 865
 866            try
 867            {
 1552868                if (!saver.IsEnabledFor(item, updateType))
 869                {
 1552870                    return false;
 871                }
 872
 0873                if (!includeDisabled)
 874                {
 0875                    if (libraryOptions.MetadataSavers is null)
 876                    {
 0877                        if (options.DisabledMetadataSavers.Contains(saver.Name, StringComparison.OrdinalIgnoreCase))
 878                        {
 0879                            return false;
 880                        }
 881
 0882                        if (!item.IsSaveLocalMetadataEnabled())
 883                        {
 0884                            if (updateType >= ItemUpdateType.MetadataEdit)
 885                            {
 886                                // Manual edit occurred
 887                                // Even if save local is off, save locally anyway if the metadata file already exists
 0888                                if (saver is not IMetadataFileSaver fileSaver || !File.Exists(fileSaver.GetSavePath(item
 889                                {
 0890                                    return false;
 891                                }
 892                            }
 893                            else
 894                            {
 895                                // Manual edit did not occur
 896                                // Since local metadata saving is disabled, consider it disabled
 0897                                return false;
 898                            }
 899                        }
 900                    }
 901                    else
 902                    {
 0903                        if (!libraryOptions.MetadataSavers.Contains(saver.Name, StringComparison.OrdinalIgnoreCase))
 904                        {
 0905                            return false;
 906                        }
 907                    }
 908                }
 909
 0910                return true;
 911            }
 0912            catch (Exception ex)
 913            {
 0914                _logger.LogError(ex, "Error in {Saver}.IsEnabledFor", saver.Name);
 0915                return false;
 916            }
 1552917        }
 918
 919        /// <inheritdoc/>
 920        public Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TL
 921            where TItemType : BaseItem, new()
 922            where TLookupType : ItemLookupInfo
 923        {
 0924            BaseItem? referenceItem = null;
 925
 0926            if (!searchInfo.ItemId.IsEmpty())
 927            {
 0928                referenceItem = _libraryManager.GetItemById(searchInfo.ItemId);
 929            }
 930
 0931            return GetRemoteSearchResults<TItemType, TLookupType>(searchInfo, referenceItem, cancellationToken);
 932        }
 933
 934        private async Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQ
 935            where TItemType : BaseItem, new()
 936            where TLookupType : ItemLookupInfo
 937        {
 938            LibraryOptions libraryOptions;
 939
 0940            if (referenceItem is null)
 941            {
 942                // Give it a dummy path just so that it looks like a file system item
 0943                var dummy = new TItemType
 0944                {
 0945                    Path = Path.Combine(_appPaths.InternalMetadataPath, "dummy"),
 0946                    ParentId = Guid.NewGuid()
 0947                };
 948
 0949                dummy.SetParent(new Folder());
 950
 0951                referenceItem = dummy;
 0952                libraryOptions = new LibraryOptions();
 953            }
 954            else
 955            {
 0956                libraryOptions = _libraryManager.GetLibraryOptions(referenceItem);
 957            }
 958
 0959            var options = GetMetadataOptions(referenceItem);
 0960            var libraryPath = GetLibraryPathForItem(referenceItem);
 0961            var providers = GetMetadataProvidersInternal<TItemType>(referenceItem, libraryOptions, options, searchInfo.I
 0962                .OfType<IRemoteSearchProvider<TLookupType>>();
 963
 0964            if (!string.IsNullOrEmpty(searchInfo.SearchProviderName))
 965            {
 0966                providers = providers.Where(i => string.Equals(i.Name, searchInfo.SearchProviderName, StringComparison.O
 967            }
 968
 0969            if (string.IsNullOrWhiteSpace(searchInfo.SearchInfo.MetadataLanguage))
 970            {
 0971                searchInfo.SearchInfo.MetadataLanguage = _configurationManager.Configuration.PreferredMetadataLanguage;
 972            }
 973
 0974            if (string.IsNullOrWhiteSpace(searchInfo.SearchInfo.MetadataCountryCode))
 975            {
 0976                searchInfo.SearchInfo.MetadataCountryCode = _configurationManager.Configuration.MetadataCountryCode;
 977            }
 978
 0979            var resultList = new List<RemoteSearchResult>();
 980
 0981            foreach (var provider in providers)
 982            {
 983                try
 984                {
 0985                    var results = await provider.GetSearchResults(searchInfo.SearchInfo, cancellationToken).ConfigureAwa
 986
 0987                    foreach (var result in results)
 988                    {
 0989                        result.SearchProviderName = provider.Name;
 990
 0991                        var existingMatch = resultList.FirstOrDefault(i => i.ProviderIds.Any(p => string.Equals(result.G
 992
 0993                        if (existingMatch is null)
 994                        {
 0995                            resultList.Add(result);
 996                        }
 997                        else
 998                        {
 0999                            foreach (var providerId in result.ProviderIds)
 1000                            {
 01001                                existingMatch.ProviderIds.TryAdd(providerId.Key, providerId.Value);
 1002                            }
 1003
 01004                            if (string.IsNullOrWhiteSpace(existingMatch.ImageUrl))
 1005                            {
 01006                                existingMatch.ImageUrl = result.ImageUrl;
 1007                            }
 1008                        }
 1009                    }
 01010                }
 1011#pragma warning disable CA1031 // do not catch general exception types
 01012                catch (Exception ex)
 1013#pragma warning restore CA1031 // do not catch general exception types
 1014                {
 01015                    _logger.LogError(ex, "Provider {ProviderName} failed to retrieve search results", provider.Name);
 01016                }
 01017            }
 1018
 01019            return resultList;
 01020        }
 1021
 1022        private IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
 1023        {
 01024            return _externalIds.Where(i =>
 01025            {
 01026                try
 01027                {
 01028                    return i.Supports(item);
 01029                }
 01030                catch (Exception ex)
 01031                {
 01032                    _logger.LogError(ex, "Error in {Type}.Supports", i.GetType().Name);
 01033                    return false;
 01034                }
 01035            });
 1036        }
 1037
 1038        /// <inheritdoc/>
 1039        public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item)
 1040        {
 61041            return _externalUrlProviders
 61042                .SelectMany(p => p
 61043                    .GetExternalUrls(item)
 61044                    .Select(externalUrl => new ExternalUrl { Name = p.Name, Url = externalUrl }));
 1045        }
 1046
 1047        /// <inheritdoc/>
 1048        public IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item)
 1049        {
 01050            return GetExternalIds(item)
 01051                .Select(i => new ExternalIdInfo(
 01052                    name: i.ProviderName,
 01053                    key: i.Key,
 01054                    type: i.Type));
 1055        }
 1056
 1057        /// <inheritdoc/>
 1058        public HashSet<Guid> GetRefreshQueue()
 31059        {
 1060            lock (_refreshQueueLock)
 1061            {
 31062                return _refreshQueue.UnorderedItems.Select(x => x.Element.ItemId).ToHashSet();
 1063            }
 31064        }
 1065
 1066        /// <inheritdoc/>
 1067        public void OnRefreshStart(BaseItem item)
 1068        {
 201069            _logger.LogDebug("OnRefreshStart {Item:N}", item.Id);
 201070            _activeRefreshes[item.Id] = 0;
 1071            try
 1072            {
 201073                RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
 201074            }
 01075            catch (Exception ex)
 1076            {
 1077                // EventHandlers should never propagate exceptions, but we have little control over plugins...
 01078                _logger.LogError(ex, "Invoking {RefreshEvent} event handlers failed", nameof(RefreshStarted));
 01079            }
 201080        }
 1081
 1082        /// <inheritdoc/>
 1083        public void OnRefreshComplete(BaseItem item)
 1084        {
 201085            _logger.LogDebug("OnRefreshComplete {Item:N}", item.Id);
 201086            _activeRefreshes.TryRemove(item.Id, out _);
 1087
 1088            try
 1089            {
 201090                RefreshCompleted?.Invoke(this, new GenericEventArgs<BaseItem>(item));
 201091            }
 01092            catch (Exception ex)
 1093            {
 1094                // EventHandlers should never propagate exceptions, but we have little control over plugins...
 01095                _logger.LogError(ex, "Invoking {RefreshEvent} event handlers failed", nameof(RefreshCompleted));
 01096            }
 201097        }
 1098
 1099        /// <inheritdoc/>
 1100        public double? GetRefreshProgress(Guid id)
 1101        {
 01102            if (_activeRefreshes.TryGetValue(id, out double value))
 1103            {
 01104                return value;
 1105            }
 1106
 01107            return null;
 1108        }
 1109
 1110        /// <inheritdoc/>
 1111        public void OnRefreshProgress(BaseItem item, double progress)
 1112        {
 1001113            var id = item.Id;
 1001114            _logger.LogDebug("OnRefreshProgress {Id:N} {Progress}", id, progress);
 1115
 1001116            if (!_activeRefreshes.TryGetValue(id, out var current)
 1001117                || progress <= current
 1001118                || !_activeRefreshes.TryUpdate(id, progress, current))
 1119            {
 1120                // Item isn't currently refreshing, or update was received out-of-order, so don't trigger event.
 401121                return;
 1122            }
 1123
 1124            try
 1125            {
 601126                RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(
 601127            }
 01128            catch (Exception ex)
 1129            {
 1130                // EventHandlers should never propagate exceptions, but we have little control over plugins...
 01131                _logger.LogError(ex, "Invoking {RefreshEvent} event handlers failed", nameof(RefreshProgress));
 01132            }
 601133        }
 1134
 1135        /// <inheritdoc/>
 1136        public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority)
 1137        {
 22051138            if (itemId.IsEmpty())
 1139            {
 01140                throw new ArgumentException("Guid can't be empty", nameof(itemId));
 1141            }
 1142
 22051143            if (_disposed)
 1144            {
 01145                return;
 1146            }
 1147
 1148            // PriorityQueue is not thread safe and the processor dequeues concurrently, so every
 1149            // touch of the queue takes the lock.
 1150            lock (_refreshQueueLock)
 1151            {
 22051152                _refreshQueue.Enqueue((itemId, options), priority);
 1153
 22051154                if (_isProcessingRefreshQueue)
 1155                {
 21101156                    return;
 1157                }
 1158
 951159                _isProcessingRefreshQueue = true;
 951160            }
 1161
 951162            Task.Run(StartProcessingRefreshQueue);
 22051163        }
 1164
 1165        private async Task StartProcessingRefreshQueue()
 1166        {
 951167            var libraryManager = _libraryManager;
 1168
 951169            if (_disposed)
 01170            {
 1171                lock (_refreshQueueLock)
 1172                {
 01173                    _isProcessingRefreshQueue = false;
 01174                }
 1175
 01176                return;
 1177            }
 1178
 951179            var cancellationToken = _disposeCancellationTokenSource.Token;
 1180
 951181            libraryManager.ClearIgnoreRuleCache();
 1182
 1183            while (true)
 23001184            {
 1185                (Guid ItemId, MetadataRefreshOptions RefreshOptions) refreshItem;
 1186
 1187                // Dequeueing and standing down happen under one lock, otherwise a refresh queued
 1188                // just after the queue ran dry would see a processor that has already stopped.
 1189                lock (_refreshQueueLock)
 1190                {
 23001191                    if (_disposed
 23001192                        || cancellationToken.IsCancellationRequested
 23001193                        || !_refreshQueue.TryDequeue(out refreshItem, out _))
 1194                    {
 951195                        _isProcessingRefreshQueue = false;
 951196                        break;
 1197                    }
 22051198                }
 1199
 1200                try
 1201                {
 22051202                    var item = libraryManager.GetItemById(refreshItem.ItemId);
 22041203                    if (item is null)
 1204                    {
 21991205                        continue;
 1206                    }
 1207
 51208                    var task = item is MusicArtist artist
 51209                        ? RefreshArtist(artist, refreshItem.RefreshOptions, cancellationToken)
 51210                        : RefreshItem(item, refreshItem.RefreshOptions, cancellationToken);
 1211
 51212                    await task.ConfigureAwait(false);
 51213                }
 11214                catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 1215                {
 1216                    // Shutting down: the next pass sees the token and stands the processor down.
 01217                    continue;
 1218                }
 11219                catch (Exception ex)
 1220                {
 1221                    // Includes a provider that cancelled for its own reasons, such as an HTTP
 1222                    // timeout, which must not stop the queue draining.
 11223                    _logger.LogError(ex, "Error refreshing item");
 11224                }
 1225            }
 1226
 951227            if (!_disposed)
 1228            {
 951229                libraryManager.ClearIgnoreRuleCache();
 1230            }
 951231        }
 1232
 1233        private async Task RefreshItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToke
 1234        {
 51235            await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
 1236
 1237            // Collection folders don't validate their children so we'll have to simulate that here
 1238            switch (item)
 1239            {
 1240                case CollectionFolder collectionFolder:
 01241                    await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(f
 01242                    break;
 1243                case Folder folder:
 51244                    await folder.ValidateChildren(new Progress<double>(), options, cancellationToken: cancellationToken)
 1245                    break;
 1246            }
 51247        }
 1248
 1249        private async Task RefreshCollectionFolderChildren(MetadataRefreshOptions options, CollectionFolder collectionFo
 1250        {
 01251            foreach (var child in collectionFolder.GetPhysicalFolders())
 1252            {
 01253                await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
 1254
 01255                await child.ValidateChildren(new Progress<double>(), options, cancellationToken: cancellationToken).Conf
 01256            }
 01257        }
 1258
 1259        private async Task RefreshArtist(MusicArtist item, MetadataRefreshOptions options, CancellationToken cancellatio
 1260        {
 01261            var albums = _libraryManager
 01262                .GetItemList(new InternalItemsQuery
 01263                {
 01264                    IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
 01265                    ArtistIds = new[] { item.Id },
 01266                    DtoOptions = new DtoOptions(false)
 01267                    {
 01268                        EnableImages = false
 01269                    }
 01270                })
 01271                .OfType<MusicAlbum>();
 1272
 01273            var musicArtists = albums
 01274                .Select(i => i.MusicArtist)
 01275                .Where(i => i is not null)
 01276                .Distinct();
 1277
 01278            var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new Progress<double>(), options, t
 1279
 01280            await Task.WhenAll(musicArtistRefreshTasks).ConfigureAwait(false);
 1281
 1282            try
 1283            {
 01284                await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
 01285            }
 01286            catch (Exception ex)
 1287            {
 01288                _logger.LogError(ex, "Error refreshing library");
 01289            }
 01290        }
 1291
 1292        /// <inheritdoc/>
 1293        public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken)
 1294        {
 01295            return RefreshItem(item, options, cancellationToken);
 1296        }
 1297
 1298        /// <inheritdoc/>
 1299        public void Dispose()
 1300        {
 991301            Dispose(true);
 991302            GC.SuppressFinalize(this);
 991303        }
 1304
 1305        /// <summary>
 1306        /// Releases unmanaged and optionally managed resources.
 1307        /// </summary>
 1308        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release
 1309        protected virtual void Dispose(bool disposing)
 1310        {
 991311            if (_disposed)
 1312            {
 01313                return;
 1314            }
 1315
 991316            if (disposing)
 1317            {
 991318                CollectionFolder.LibraryOptionsUpdated -= OnLibraryOptionsUpdated;
 991319                _configurationManager.ConfigurationUpdated -= OnConfigurationUpdated;
 1320
 991321                if (!_disposeCancellationTokenSource.IsCancellationRequested)
 1322                {
 991323                    _disposeCancellationTokenSource.Cancel();
 1324                }
 1325
 991326                _disposeCancellationTokenSource.Dispose();
 991327                _imageSaveLock.Dispose();
 1328            }
 1329
 991330            _disposed = true;
 991331        }
 1332
 1333        private void OnLibraryOptionsUpdated(object? sender, LibraryOptionsUpdatedEventArgs e)
 1334        {
 61335            var keysToRemove = _metadataProviderCache.Keys
 61336                .Where(k => string.Equals(k.LibraryPath, e.LibraryPath, StringComparison.Ordinal))
 61337                .ToList();
 1338
 141339            foreach (var key in keysToRemove)
 1340            {
 11341                _metadataProviderCache.TryRemove(key, out _);
 1342            }
 1343
 61344            _logger.LogDebug("Invalidated metadata provider cache for library: {LibraryPath}", e.LibraryPath);
 61345        }
 1346
 1347        private void OnConfigurationUpdated(object? sender, EventArgs e)
 1348        {
 1251349            ClearMetadataProviderCache();
 1251350        }
 1351
 1352        internal void ClearMetadataProviderCache()
 1353        {
 2221354            _metadataProviderCache.Clear();
 2221355            _logger.LogDebug("Cleared entire metadata provider cache");
 2221356        }
 1357
 1358        /// <summary>
 1359        /// Cache key for metadata provider lookups.
 1360        /// </summary>
 1361        /// <param name="LibraryPath">The path of the collection folder providing the library options.</param>
 1362        /// <param name="ItemTypeName">The item type name.</param>
 1363        /// <param name="IncludeDisabled">Whether to include disabled providers.</param>
 1364        /// <param name="ForceEnableInternetMetadata">Whether internet metadata is force-enabled.</param>
 1365        private readonly record struct MetadataProviderCacheKey(
 1366            string LibraryPath,
 1367            string ItemTypeName,
 1368            bool IncludeDisabled,
 1369            bool ForceEnableInternetMetadata);
 1370    }
 1371}

Methods/Properties

.ctor(System.Net.Http.IHttpClientFactory,MediaBrowser.Controller.Subtitles.ISubtitleManager,MediaBrowser.Controller.Configuration.IServerConfigurationManager,MediaBrowser.Controller.Library.ILibraryMonitor,Microsoft.Extensions.Logging.ILogger`1<MediaBrowser.Providers.Manager.ProviderManager>,MediaBrowser.Model.IO.IFileSystem,MediaBrowser.Controller.IServerApplicationPaths,MediaBrowser.Controller.Library.ILibraryManager,MediaBrowser.Controller.BaseItemManager.IBaseItemManager,MediaBrowser.Controller.Lyrics.ILyricManager,Microsoft.Extensions.Caching.Memory.IMemoryCache,MediaBrowser.Controller.MediaSegments.IMediaSegmentManager,MediaBrowser.Controller.Library.ISimilarItemsManager)
AddParts(System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Providers.IImageProvider>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Providers.IMetadataService>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Providers.IMetadataProvider>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Library.IMetadataSaver>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Providers.IExternalId>,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Providers.IExternalUrlProvider>)
RefreshSingleItem(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Providers.MetadataRefreshOptions,System.Threading.CancellationToken)
SaveImage()
SaveImage(MediaBrowser.Controller.Entities.BaseItem,System.IO.Stream,System.String,MediaBrowser.Model.Entities.ImageType,System.Nullable`1<System.Int32>,System.Threading.CancellationToken)
SaveImage()
SaveImage(System.IO.Stream,System.String,System.String)
GetAvailableRemoteImages()
GetImages()
GetRemoteImageProviderInfo(MediaBrowser.Controller.Entities.BaseItem)
GetRemoteImageProviders(MediaBrowser.Controller.Entities.BaseItem,System.Boolean)
GetImageProviders(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Providers.ImageRefreshOptions)
GetImageProvidersInternal(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Configuration.LibraryOptions,MediaBrowser.Model.Configuration.MetadataOptions,MediaBrowser.Controller.Providers.ImageRefreshOptions,System.Boolean)
CanRefreshImages(MediaBrowser.Controller.Providers.IImageProvider,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Configuration.TypeOptions,MediaBrowser.Controller.Providers.ImageRefreshOptions,System.Boolean)
GetMetadataProviders(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Configuration.LibraryOptions)
GetMetadataProviders(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Configuration.LibraryOptions,System.Boolean)
GetLibraryPathForItem(MediaBrowser.Controller.Entities.BaseItem)
GetMetadataSavers(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Configuration.LibraryOptions)
GetMetadataProvidersInternal(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Configuration.LibraryOptions,MediaBrowser.Model.Configuration.MetadataOptions,System.Boolean,System.Boolean,System.String)
GetOrCreateOrderedProviders(System.String,MediaBrowser.Model.Configuration.LibraryOptions,MediaBrowser.Model.Configuration.MetadataOptions,System.Boolean,System.Boolean,System.String)
CanRefreshMetadataForCache(MediaBrowser.Controller.Providers.IMetadataProvider,MediaBrowser.Model.Configuration.TypeOptions,System.Boolean,System.Boolean)
CanRefreshMetadata(MediaBrowser.Controller.Providers.IMetadataProvider,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Configuration.TypeOptions,System.Boolean,System.Boolean)
GetConfiguredOrder(System.String[],System.String)
GetDefaultOrder(System.Object)
GetAllMetadataPlugins()
GetPluginSummary()
AddMetadataPlugins(System.Collections.Generic.List`1<MediaBrowser.Model.Configuration.MetadataPlugin>,T,MediaBrowser.Model.Configuration.LibraryOptions,MediaBrowser.Model.Configuration.MetadataOptions)
AddImagePlugins(System.Collections.Generic.List`1<MediaBrowser.Model.Configuration.MetadataPlugin>,System.Collections.Generic.List`1<MediaBrowser.Controller.Providers.IImageProvider>)
GetMetadataOptions(MediaBrowser.Controller.Entities.BaseItem)
SaveMetadataAsync(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Library.ItemUpdateType)
SaveMetadataAsync(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Library.ItemUpdateType,System.Collections.Generic.IEnumerable`1<System.String>)
SaveMetadataAsync()
IsSaverEnabledForItem(MediaBrowser.Controller.Library.IMetadataSaver,MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Configuration.LibraryOptions,MediaBrowser.Controller.Library.ItemUpdateType,System.Boolean)
GetRemoteSearchResults(MediaBrowser.Controller.Providers.RemoteSearchQuery`1<TLookupType>,System.Threading.CancellationToken)
GetRemoteSearchResults()
GetExternalIds(MediaBrowser.Model.Entities.IHasProviderIds)
GetExternalUrls(MediaBrowser.Controller.Entities.BaseItem)
GetExternalIdInfos(MediaBrowser.Model.Entities.IHasProviderIds)
GetRefreshQueue()
OnRefreshStart(MediaBrowser.Controller.Entities.BaseItem)
OnRefreshComplete(MediaBrowser.Controller.Entities.BaseItem)
GetRefreshProgress(System.Guid)
OnRefreshProgress(MediaBrowser.Controller.Entities.BaseItem,System.Double)
QueueRefresh(System.Guid,MediaBrowser.Controller.Providers.MetadataRefreshOptions,MediaBrowser.Controller.Providers.RefreshPriority)
StartProcessingRefreshQueue()
RefreshItem()
RefreshCollectionFolderChildren()
RefreshArtist()
RefreshFullItem(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Providers.MetadataRefreshOptions,System.Threading.CancellationToken)
Dispose()
Dispose(System.Boolean)
OnLibraryOptionsUpdated(System.Object,MediaBrowser.Controller.Entities.LibraryOptionsUpdatedEventArgs)
OnConfigurationUpdated(System.Object,System.EventArgs)
ClearMetadataProviderCache()