< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.Manager.MetadataService<T1, T2>
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/Manager/MetadataService.cs
Line coverage
66%
Covered lines: 411
Uncovered lines: 205
Coverable lines: 616
Total lines: 1515
Line coverage: 66.7%
Branch coverage
65%
Covered branches: 390
Total branches: 594
Branch coverage: 65.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/12/2026 - 12:15:57 AM Line coverage: 57.5% (310/539) Branch coverage: 64.1% (322/502) Total lines: 13205/20/2026 - 12:15:44 AM Line coverage: 57.5% (310/539) Branch coverage: 60.9% (306/502) Total lines: 13206/22/2026 - 12:15:37 AM Line coverage: 57.3% (311/542) Branch coverage: 60.8% (308/506) Total lines: 13287/18/2026 - 12:15:19 AM Line coverage: 57.2% (312/545) Branch coverage: 60.7% (310/510) Total lines: 13357/22/2026 - 12:16:22 AM Line coverage: 57.2% (312/545) Branch coverage: 60.7% (311/512) Total lines: 13357/27/2026 - 12:16:14 AM Line coverage: 57.2% (312/545) Branch coverage: 60.9% (312/512) Total lines: 13357/28/2026 - 12:13:49 AM Line coverage: 57.2% (312/545) Branch coverage: 60.7% (311/512) Total lines: 13357/29/2026 - 12:15:53 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 13407/30/2026 - 12:15:56 AM Line coverage: 57.4% (314/547) Branch coverage: 61% (315/516) Total lines: 13407/31/2026 - 12:16:28 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 13408/5/2026 - 12:17:09 AM Line coverage: 57.4% (314/547) Branch coverage: 61% (315/516) Total lines: 13408/7/2026 - 12:16:14 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 13408/8/2026 - 12:17:31 AM Line coverage: 57.4% (314/547) Branch coverage: 61% (315/516) Total lines: 13408/9/2026 - 12:16:58 AM Line coverage: 57.9% (317/547) Branch coverage: 61% (315/516) Total lines: 13408/10/2026 - 12:14:18 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 134010/29/2026 - 12:17:11 AM Line coverage: 66.7% (411/616) Branch coverage: 65.6% (390/594) Total lines: 151510/30/2026 - 12:16:57 AM Line coverage: 66.7% (411/616) Branch coverage: 65.8% (391/594) Total lines: 151510/31/2026 - 12:17:46 AM Line coverage: 66.7% (411/616) Branch coverage: 65.6% (390/594) Total lines: 1515 5/12/2026 - 12:15:57 AM Line coverage: 57.5% (310/539) Branch coverage: 64.1% (322/502) Total lines: 13205/20/2026 - 12:15:44 AM Line coverage: 57.5% (310/539) Branch coverage: 60.9% (306/502) Total lines: 13206/22/2026 - 12:15:37 AM Line coverage: 57.3% (311/542) Branch coverage: 60.8% (308/506) Total lines: 13287/18/2026 - 12:15:19 AM Line coverage: 57.2% (312/545) Branch coverage: 60.7% (310/510) Total lines: 13357/22/2026 - 12:16:22 AM Line coverage: 57.2% (312/545) Branch coverage: 60.7% (311/512) Total lines: 13357/27/2026 - 12:16:14 AM Line coverage: 57.2% (312/545) Branch coverage: 60.9% (312/512) Total lines: 13357/28/2026 - 12:13:49 AM Line coverage: 57.2% (312/545) Branch coverage: 60.7% (311/512) Total lines: 13357/29/2026 - 12:15:53 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 13407/30/2026 - 12:15:56 AM Line coverage: 57.4% (314/547) Branch coverage: 61% (315/516) Total lines: 13407/31/2026 - 12:16:28 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 13408/5/2026 - 12:17:09 AM Line coverage: 57.4% (314/547) Branch coverage: 61% (315/516) Total lines: 13408/7/2026 - 12:16:14 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 13408/8/2026 - 12:17:31 AM Line coverage: 57.4% (314/547) Branch coverage: 61% (315/516) Total lines: 13408/9/2026 - 12:16:58 AM Line coverage: 57.9% (317/547) Branch coverage: 61% (315/516) Total lines: 13408/10/2026 - 12:14:18 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 134010/29/2026 - 12:17:11 AM Line coverage: 66.7% (411/616) Branch coverage: 65.6% (390/594) Total lines: 151510/30/2026 - 12:16:57 AM Line coverage: 66.7% (411/616) Branch coverage: 65.8% (391/594) Total lines: 151510/31/2026 - 12:17:46 AM Line coverage: 66.7% (411/616) Branch coverage: 65.6% (390/594) Total lines: 1515

Coverage delta

Coverage delta 10 -10

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_EnableUpdatingPremiereDateFromChildren()100%11100%
get_EnableUpdatingGenresFromChildren()100%11100%
get_EnableUpdatingStudiosFromChildren()100%11100%
get_EnableUpdatingOfficialRatingFromChildren()100%11100%
get_Order()100%11100%
TryGetFileSystemMetadata(...)100%1140%
RefreshMetadata()71.66%656088.73%
ApplySearchResult(...)0%2040%
GetValidProviderIds(...)0%4260%
SaveItemAsync()66.66%7671.42%
AfterMetadataRefresh(...)100%11100%
BeforeSave()75%4485.71%
BeforeSaveInternal(...)62.5%191676.19%
EnableUpdateMetadataFromChildren(...)68.18%272277.77%
GetChildrenForMetadataUpdates(...)0%620%
UpdateMetadataFromChildren(...)0%600240%
UpdateCumulativeRunTimeTicks(...)0%156120%
UpdateDateLastMediaAdded(...)0%342180%
UpdatePremiereDate(...)0%420200%
UpdateGenres(...)0%7280%
UpdateStudios(...)0%7280%
UpdateOfficialRating(...)0%2040%
GetProviders(...)85.71%511442.85%
GetNonLocalImageProviders(...)80%101088.88%
CanRefresh(...)100%11100%
CanRefreshPrimary(...)100%11100%
RefreshWithProviders()73.52%2336867.07%
RunCustomProvider()100%1181.81%
CreateNew()100%11100%
ExecuteRemoteProviders()54.54%272278.37%
LogInvalidProviderIds(...)12.5%35825%
LogInvalidProviderIds(...)0%7280%
MergeNewData(...)100%88100%
HasChanged(...)25%6450%
MergeData(...)100%11100%
MergeBaseItemData(...)92.26%16816899%
RemoveInvalidProviderIds(...)100%1414100%
MergePeople(...)80.76%272688.88%
MergeDisplayOrder(...)90%1010100%
MergeAlbumArtist(...)80%1010100%
MergeVideoInfo(...)90%1010100%

File(s)

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

#LineLine coverage
 1#nullable disable
 2
 3#pragma warning disable CS1591
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Linq;
 8using System.Net.Http;
 9using System.Threading;
 10using System.Threading.Tasks;
 11using Jellyfin.Extensions;
 12using MediaBrowser.Controller.Configuration;
 13using MediaBrowser.Controller.Entities;
 14using MediaBrowser.Controller.Entities.Audio;
 15using MediaBrowser.Controller.IO;
 16using MediaBrowser.Controller.Library;
 17using MediaBrowser.Controller.Persistence;
 18using MediaBrowser.Controller.Providers;
 19using MediaBrowser.Model.Configuration;
 20using MediaBrowser.Model.Entities;
 21using MediaBrowser.Model.IO;
 22using MediaBrowser.Model.Providers;
 23using Microsoft.Extensions.Logging;
 24
 25namespace MediaBrowser.Providers.Manager
 26{
 27    public abstract class MetadataService<TItemType, TIdType> : IMetadataService
 28        where TItemType : BaseItem, IHasLookupInfo<TIdType>, new()
 29        where TIdType : ItemLookupInfo, new()
 30    {
 31        protected MetadataService(
 32            IServerConfigurationManager serverConfigurationManager,
 33            ILogger<MetadataService<TItemType, TIdType>> logger,
 34            IProviderManager providerManager,
 35            IFileSystem fileSystem,
 36            ILibraryManager libraryManager,
 37            IExternalDataManager externalDataManager,
 38            IItemRepository itemRepository)
 39        {
 72540            ServerConfigurationManager = serverConfigurationManager;
 72541            Logger = logger;
 72542            ProviderManager = providerManager;
 72543            FileSystem = fileSystem;
 72544            LibraryManager = libraryManager;
 72545            ExternalDataManager = externalDataManager;
 72546            ItemRepository = itemRepository;
 72547            ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
 72548        }
 49
 50        protected ItemImageProvider ImageProvider { get; }
 51
 52        protected IServerConfigurationManager ServerConfigurationManager { get; }
 53
 54        protected ILogger<MetadataService<TItemType, TIdType>> Logger { get; }
 55
 56        protected IProviderManager ProviderManager { get; }
 57
 58        protected IFileSystem FileSystem { get; }
 59
 60        protected ILibraryManager LibraryManager { get; }
 61
 62        protected IExternalDataManager ExternalDataManager { get; }
 63
 64        protected IItemRepository ItemRepository { get; }
 65
 4766        protected virtual bool EnableUpdatingPremiereDateFromChildren => false;
 67
 4768        protected virtual bool EnableUpdatingGenresFromChildren => false;
 69
 4770        protected virtual bool EnableUpdatingStudiosFromChildren => false;
 71
 4772        protected virtual bool EnableUpdatingOfficialRatingFromChildren => false;
 73
 64874        public virtual int Order => 0;
 75
 76        private FileSystemMetadata TryGetFileSystemMetadata(string path, IDirectoryService directoryService)
 77        {
 78            try
 79            {
 4780                return directoryService.GetFileSystemEntry(path);
 81            }
 082            catch (Exception ex)
 83            {
 084                Logger.LogError(ex, "Error getting file {Path}", path);
 085                return null;
 86            }
 4787        }
 88
 89        public virtual async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, 
 90        {
 11491            var itemOfType = (TItemType)item;
 11492            var updateType = ItemUpdateType.None;
 93
 11494            var libraryOptions = LibraryManager.GetLibraryOptions(item);
 11495            var isFirstRefresh = item.DateLastRefreshed == DateTime.MinValue;
 11496            var hasRefreshedMetadata = true;
 11497            var hasRefreshedImages = true;
 98
 11499            var requiresRefresh = libraryOptions.AutomaticRefreshIntervalDays > 0 && (DateTime.UtcNow - item.DateLastRef
 100
 114101            if (!requiresRefresh && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
 102            {
 103                // TODO: If this returns true, should we instead just change metadata refresh mode to Full?
 114104                requiresRefresh = item.RequiresRefresh();
 105
 114106                if (requiresRefresh)
 107                {
 23108                    Logger.LogDebug("Refreshing {Type} {Item} because item.RequiresRefresh() returned true", typeof(TIte
 109                }
 110            }
 111
 114112            if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
 113            {
 0114                if (ImageProvider.RemoveImages(item))
 115                {
 0116                    updateType |= ItemUpdateType.ImageUpdate;
 117                }
 118            }
 119
 114120            var localImagesFailed = false;
 114121            var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
 122
 123            // Only validate already registered images if we are replacing and saving locally
 114124            if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages)
 125            {
 0126                item.ValidateImages();
 127            }
 128            else
 129            {
 130                // Run full image validation and register new local images
 131                try
 132                {
 114133                    if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptio
 134                    {
 0135                        updateType |= ItemUpdateType.ImageUpdate;
 136                    }
 113137                }
 1138                catch (Exception ex)
 139                {
 1140                    localImagesFailed = true;
 1141                    Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
 1142                }
 143            }
 144
 114145            var metadataResult = new MetadataResult<TItemType>
 114146            {
 114147                Item = itemOfType
 114148            };
 149
 114150            var beforeSaveResult = await BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || r
 114151                .ConfigureAwait(false);
 114152            updateType |= beforeSaveResult;
 153
 114154            if (isFirstRefresh)
 155            {
 46156                await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, false, cancellationToken).ConfigureAw
 157            }
 158
 159            // Next run metadata providers
 114160            if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
 161            {
 114162                var providers = GetProviders(item, libraryOptions, refreshOptions, isFirstRefresh, requiresRefresh)
 114163                    .ToList();
 164
 114165                if (providers.Count > 0 || isFirstRefresh || requiresRefresh)
 166                {
 50167                    if (item.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata))
 168                    {
 35169                        updateType |= ItemUpdateType.MetadataImport;
 170                    }
 171                }
 172
 114173                if (providers.Count > 0)
 174                {
 50175                    var id = itemOfType.GetLookupInfo();
 176
 50177                    if (refreshOptions.SearchResult is not null)
 178                    {
 0179                        ApplySearchResult(id, refreshOptions.SearchResult);
 180                    }
 181
 50182                    id.IsAutomated = refreshOptions.IsAutomated;
 183
 50184                    var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any();
 50185                    var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider
 186
 50187                    updateType |= result.UpdateType;
 50188                    if (result.Failures > 0)
 189                    {
 3190                        hasRefreshedMetadata = false;
 191                    }
 192                }
 193            }
 194
 114195            if (localImagesFailed)
 196            {
 1197                hasRefreshedImages = false;
 198            }
 113199            else if (refreshOptions.ImageRefreshMode > MetadataRefreshMode.ValidationOnly)
 200            {
 201                // Next run remote image providers, now that local image providers didn't throw
 113202                var providers = GetNonLocalImageProviders(item, allImageProviders, refreshOptions).ToList();
 203
 113204                if (providers.Count > 0)
 205                {
 2206                    var result = await ImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions
 207
 2208                    updateType |= result.UpdateType;
 2209                    if (result.Failures > 0)
 210                    {
 1211                        hasRefreshedImages = false;
 212                    }
 213                }
 214            }
 215
 114216            var attemptedFetch = refreshOptions.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly
 114217                || refreshOptions.ImageRefreshMode > MetadataRefreshMode.ValidationOnly;
 218
 114219            var refreshStampNeedsSaving = false;
 220
 114221            if (hasRefreshedMetadata && hasRefreshedImages && attemptedFetch)
 222            {
 109223                item.DateLastRefreshed = DateTime.UtcNow;
 109224                updateType |= item.OnMetadataChanged();
 225
 226                // A full refresh queries every provider whether or not anything looks stale. When they all
 227                // come back empty the stamp is the only thing that changed, and without it nothing records
 228                // that the lookup happened, so the next pass repeats the same fruitless queries forever.
 109229                refreshStampNeedsSaving = refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh
 109230                    || refreshOptions.ImageRefreshMode == MetadataRefreshMode.FullRefresh;
 231            }
 232
 114233            updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, refreshSt
 234
 114235            await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false);
 236
 114237            return updateType;
 238
 239            async Task<ItemUpdateType> SaveInternal(BaseItem item, MetadataRefreshOptions refreshOptions, ItemUpdateType
 240            {
 241                // Save if changes were made, or it's never been saved before
 242                if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.Rep
 243                {
 244                    if (item.IsFileProtocol)
 245                    {
 246                        var file = TryGetFileSystemMetadata(item.Path, refreshOptions.DirectoryService);
 247                        if (file is not null)
 248                        {
 249                            item.DateModified = file.LastWriteTimeUtc;
 250
 251                            if (!file.IsDirectory)
 252                            {
 253                                item.Size = file.Length;
 254                            }
 255                        }
 256                    }
 257
 258                    // If any of these properties are set then make sure the updateType is not None, just to force every
 259                    if (refreshOptions.ForceSave || refreshOptions.ReplaceAllMetadata)
 260                    {
 261                        updateType |= ItemUpdateType.MetadataDownload;
 262                    }
 263
 264                    // Save to database
 265                    await SaveItemAsync(metadataResult, updateType, isFirstRefresh, cancellationToken).ConfigureAwait(fa
 266                }
 267
 268                return updateType;
 269            }
 114270        }
 271
 272        private void ApplySearchResult(ItemLookupInfo lookupInfo, RemoteSearchResult result)
 273        {
 274            // Episode and Season do not support Identify, so the search results are the Series'
 275            switch (lookupInfo)
 276            {
 277                case EpisodeInfo episodeInfo:
 0278                    episodeInfo.SeriesProviderIds = GetValidProviderIds(result.ProviderIds);
 0279                    episodeInfo.ProviderIds.Clear();
 0280                    break;
 281                case SeasonInfo seasonInfo:
 0282                    seasonInfo.SeriesProviderIds = GetValidProviderIds(result.ProviderIds);
 0283                    seasonInfo.ProviderIds.Clear();
 0284                    break;
 285                default:
 0286                    lookupInfo.SetProviderIds(result.ProviderIds);
 0287                    lookupInfo.Name = result.Name;
 0288                    lookupInfo.Year = result.ProductionYear;
 289                    break;
 290            }
 0291        }
 292
 293        private static Dictionary<string, string> GetValidProviderIds(IReadOnlyDictionary<string, string> providerIds)
 294        {
 0295            var validProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 0296            if (providerIds is null)
 297            {
 0298                return validProviderIds;
 299            }
 300
 0301            foreach (var (name, value) in providerIds)
 302            {
 0303                if (ProviderIdsExtensions.IsValidProviderId(name, value))
 304                {
 0305                    validProviderIds[name] = value;
 306                }
 307            }
 308
 0309            return validProviderIds;
 310        }
 311
 312        protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, bool reattachUserDat
 313        {
 96314            await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
 96315            if (reattachUserData)
 316            {
 46317                await result.Item.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
 318            }
 319
 96320            if (result.Item.SupportsPeople && result.People is not null)
 321            {
 0322                var baseItem = result.Item;
 323
 0324                await LibraryManager.UpdatePeopleAsync(baseItem, result.People, cancellationToken).ConfigureAwait(false)
 325            }
 96326        }
 327
 328        protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationT
 329        {
 114330            item.AfterMetadataRefresh();
 114331            return Task.CompletedTask;
 332        }
 333
 334        /// <summary>
 335        /// Before the save.
 336        /// </summary>
 337        /// <param name="item">The item.</param>
 338        /// <param name="isFullRefresh">if set to <c>true</c> [is full refresh].</param>
 339        /// <param name="currentUpdateType">Type of the current update.</param>
 340        /// <returns>ItemUpdateType.</returns>
 341        private async Task<ItemUpdateType> BeforeSave(TItemType item, bool isFullRefresh, ItemUpdateType currentUpdateTy
 342        {
 114343            var updateType = BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
 344
 114345            updateType |= item.OnMetadataChanged();
 346
 114347            if (updateType == ItemUpdateType.None)
 348            {
 68349                if (!await ItemRepository.ItemExistsAsync(item.Id).ConfigureAwait(false))
 350                {
 0351                    return ItemUpdateType.MetadataImport;
 352                }
 353            }
 354
 114355            return updateType;
 114356        }
 357
 358        protected virtual ItemUpdateType BeforeSaveInternal(TItemType item, bool isFullRefresh, ItemUpdateType updateTyp
 359        {
 120360            if (EnableUpdateMetadataFromChildren(item, isFullRefresh, updateType))
 361            {
 0362                var children = GetChildrenForMetadataUpdates(item);
 0363                updateType = UpdateMetadataFromChildren(item, children, isFullRefresh, updateType);
 364            }
 365
 120366            var presentationUniqueKey = item.CreatePresentationUniqueKey();
 120367            if (!string.Equals(item.PresentationUniqueKey, presentationUniqueKey, StringComparison.Ordinal))
 368            {
 52369                item.PresentationUniqueKey = presentationUniqueKey;
 52370                updateType |= ItemUpdateType.MetadataImport;
 371            }
 372
 373            // Cleanup extracted files if source file was modified
 120374            var itemPath = item.Path;
 120375            if (!string.IsNullOrEmpty(itemPath))
 376            {
 107377                var info = FileSystem.GetFileSystemInfo(itemPath);
 107378                if (info.Exists && item.HasChanged(info.LastWriteTimeUtc))
 379                {
 5380                    Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", item.DateModified, in
 381
 5382                    item.DateModified = info.LastWriteTimeUtc;
 5383                    if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded)
 384                    {
 5385                        if (info.CreationTimeUtc > DateTime.MinValue)
 386                        {
 0387                            item.DateCreated = info.CreationTimeUtc;
 388                        }
 389                    }
 390
 5391                    if (item is Video video)
 392                    {
 0393                        Logger.LogInformation("File changed, pruning extracted data: {Path}", item.Path);
 0394                        ExternalDataManager.DeleteExternalItemDataAsync(video, CancellationToken.None).GetAwaiter().GetR
 395                    }
 396
 5397                    updateType |= ItemUpdateType.MetadataImport;
 398                }
 399            }
 400
 120401            return updateType;
 402        }
 403
 404        protected virtual bool EnableUpdateMetadataFromChildren(TItemType item, bool isFullRefresh, ItemUpdateType curre
 405        {
 120406            if (item is Folder folder)
 407            {
 107408                if (!isFullRefresh && currentUpdateType == ItemUpdateType.None)
 409                {
 60410                    return folder.SupportsDateLastMediaAdded;
 411                }
 412
 47413                if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
 414                {
 47415                    if (EnableUpdatingPremiereDateFromChildren || EnableUpdatingGenresFromChildren || EnableUpdatingStud
 416                    {
 0417                        return true;
 418                    }
 419
 47420                    if (folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks)
 421                    {
 0422                        return true;
 423                    }
 424                }
 425            }
 426
 60427            return false;
 428        }
 429
 430        protected virtual IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(TItemType item)
 431        {
 0432            if (item is Folder folder)
 433            {
 0434                return folder.GetRecursiveChildren();
 435            }
 436
 0437            return [];
 438        }
 439
 440        protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IReadOnlyList<BaseItem> children, bo
 441        {
 0442            var updateType = ItemUpdateType.None;
 443
 0444            if (item is Folder folder)
 445            {
 0446                if (folder.SupportsDateLastMediaAdded)
 447                {
 0448                    updateType |= UpdateDateLastMediaAdded(item, children);
 449                }
 450
 0451                if ((isFullRefresh || currentUpdateType > ItemUpdateType.None) && folder.SupportsCumulativeRunTimeTicks)
 452                {
 0453                    updateType |= UpdateCumulativeRunTimeTicks(item, children);
 454                }
 455            }
 456
 0457            if (!(isFullRefresh || currentUpdateType > ItemUpdateType.None) || item.IsLocked)
 458            {
 0459                return updateType;
 460            }
 461
 0462            if (EnableUpdatingPremiereDateFromChildren)
 463            {
 0464                updateType |= UpdatePremiereDate(item, children);
 465            }
 466
 0467            if (EnableUpdatingGenresFromChildren)
 468            {
 0469                updateType |= UpdateGenres(item, children);
 470            }
 471
 0472            if (EnableUpdatingStudiosFromChildren)
 473            {
 0474                updateType |= UpdateStudios(item, children);
 475            }
 476
 0477            if (EnableUpdatingOfficialRatingFromChildren)
 478            {
 0479                updateType |= UpdateOfficialRating(item, children);
 480            }
 481
 0482            return updateType;
 483        }
 484
 485        private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IReadOnlyList<BaseItem> children)
 486        {
 0487            if (item is Folder folder && folder.SupportsCumulativeRunTimeTicks)
 488            {
 0489                long ticks = 0;
 490
 0491                foreach (var child in children)
 492                {
 0493                    if (!child.IsFolder)
 494                    {
 0495                        ticks += child.RunTimeTicks ?? 0;
 496                    }
 497                }
 498
 0499                if (!folder.RunTimeTicks.HasValue || folder.RunTimeTicks.Value != ticks)
 500                {
 0501                    folder.RunTimeTicks = ticks;
 0502                    return ItemUpdateType.MetadataImport;
 503                }
 504            }
 505
 0506            return ItemUpdateType.None;
 507        }
 508
 509        private ItemUpdateType UpdateDateLastMediaAdded(TItemType item, IReadOnlyList<BaseItem> children)
 510        {
 0511            var updateType = ItemUpdateType.None;
 512
 0513            if (item is Folder folder && folder.SupportsDateLastMediaAdded)
 514            {
 0515                var dateLastMediaAdded = DateTime.MinValue;
 0516                var any = false;
 517
 0518                foreach (var child in children)
 519                {
 520                    // Exclude any folders and virtual items since they are only placeholders
 0521                    if (!child.IsFolder && !child.IsVirtualItem)
 522                    {
 0523                        var childDateCreated = child.DateCreated;
 0524                        if (childDateCreated > dateLastMediaAdded)
 525                        {
 0526                            dateLastMediaAdded = childDateCreated;
 527                        }
 528
 0529                        any = true;
 530                    }
 531                }
 532
 0533                if ((!folder.DateLastMediaAdded.HasValue && any) || folder.DateLastMediaAdded != dateLastMediaAdded)
 534                {
 0535                    folder.DateLastMediaAdded = dateLastMediaAdded;
 0536                    updateType = ItemUpdateType.MetadataImport;
 537                }
 538            }
 539
 0540            return updateType;
 541        }
 542
 543        private ItemUpdateType UpdatePremiereDate(TItemType item, IReadOnlyList<BaseItem> children)
 544        {
 0545            var updateType = ItemUpdateType.None;
 546
 0547            if (children.Count == 0)
 548            {
 0549                return updateType;
 550            }
 551
 0552            var date = children.Select(i => i.PremiereDate ?? DateTime.MaxValue).Min();
 553
 0554            var originalPremiereDate = item.PremiereDate;
 0555            var originalProductionYear = item.ProductionYear;
 556
 0557            if (date > DateTime.MinValue && date < DateTime.MaxValue)
 558            {
 0559                item.PremiereDate = date;
 0560                item.ProductionYear = date.Year;
 561            }
 562            else
 563            {
 0564                var year = children.Select(i => i.ProductionYear ?? 0).Min();
 565
 0566                if (year > 0)
 567                {
 0568                    item.ProductionYear = year;
 569                }
 570            }
 571
 0572            if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue)
 0573                || (originalProductionYear ?? -1) != (item.ProductionYear ?? -1))
 574            {
 0575                updateType |= ItemUpdateType.MetadataEdit;
 576            }
 577
 0578            return updateType;
 579        }
 580
 581        private ItemUpdateType UpdateGenres(TItemType item, IReadOnlyList<BaseItem> children)
 582        {
 0583            var updateType = ItemUpdateType.None;
 584
 0585            if (!item.LockedFields.Contains(MetadataField.Genres))
 586            {
 0587                var currentList = item.Genres;
 588
 0589                item.Genres = children.SelectMany(i => i.Genres)
 0590                    .Distinct(StringComparer.OrdinalIgnoreCase)
 0591                    .ToArray();
 592
 0593                if (currentList.Length != item.Genres.Length || !currentList.Order().SequenceEqual(item.Genres.Order(), 
 594                {
 0595                    updateType |= ItemUpdateType.MetadataEdit;
 596                }
 597            }
 598
 0599            return updateType;
 600        }
 601
 602        private ItemUpdateType UpdateStudios(TItemType item, IReadOnlyList<BaseItem> children)
 603        {
 0604            var updateType = ItemUpdateType.None;
 605
 0606            if (!item.LockedFields.Contains(MetadataField.Studios))
 607            {
 0608                var currentList = item.Studios;
 609
 0610                item.Studios = children.SelectMany(i => i.Studios)
 0611                    .Distinct(StringComparer.OrdinalIgnoreCase)
 0612                    .ToArray();
 613
 0614                if (currentList.Length != item.Studios.Length || !currentList.Order().SequenceEqual(item.Studios.Order()
 615                {
 0616                    updateType |= ItemUpdateType.MetadataEdit;
 617                }
 618            }
 619
 0620            return updateType;
 621        }
 622
 623        private ItemUpdateType UpdateOfficialRating(TItemType item, IReadOnlyList<BaseItem> children)
 624        {
 0625            var updateType = ItemUpdateType.None;
 626
 0627            if (!item.LockedFields.Contains(MetadataField.OfficialRating))
 628            {
 0629                if (item.UpdateRatingToItems(children))
 630                {
 0631                    updateType |= ItemUpdateType.MetadataEdit;
 632                }
 633            }
 634
 0635            return updateType;
 636        }
 637
 638        /// <summary>
 639        /// Gets the providers.
 640        /// </summary>
 641        /// <param name="item">A media item.</param>
 642        /// <param name="libraryOptions">The LibraryOptions to use.</param>
 643        /// <param name="options">The MetadataRefreshOptions to use.</param>
 644        /// <param name="isFirstRefresh">Specifies first refresh mode.</param>
 645        /// <param name="requiresRefresh">Specifies refresh mode.</param>
 646        /// <returns>IEnumerable{`0}.</returns>
 647        protected IEnumerable<IMetadataProvider> GetProviders(BaseItem item, LibraryOptions libraryOptions, MetadataRefr
 648        {
 649            // Get providers to refresh
 114650            var providers = ProviderManager.GetMetadataProviders<TItemType>(item, libraryOptions).ToList();
 651
 114652            var metadataRefreshMode = options.MetadataRefreshMode;
 653
 654            // Run all if either of these flags are true
 114655            var runAllProviders = options.ReplaceAllMetadata ||
 114656                metadataRefreshMode == MetadataRefreshMode.FullRefresh ||
 114657                (isFirstRefresh && metadataRefreshMode >= MetadataRefreshMode.Default) ||
 114658                (requiresRefresh && metadataRefreshMode >= MetadataRefreshMode.Default);
 659
 114660            if (!runAllProviders)
 661            {
 61662                var providersWithChanges = providers
 61663                    .Where(i =>
 61664                    {
 61665                        if (i is IHasItemChangeMonitor hasFileChangeMonitor)
 61666                        {
 61667                            return HasChanged(item, hasFileChangeMonitor, options.DirectoryService);
 61668                        }
 61669
 61670                        return false;
 61671                    })
 61672                    .ToList();
 673
 61674                if (providersWithChanges.Count == 0)
 675                {
 61676                    providers = new List<IMetadataProvider<TItemType>>();
 677                }
 678                else
 679                {
 0680                    var anyRemoteProvidersChanged = providersWithChanges.OfType<IRemoteMetadataProvider>()
 0681                        .Any();
 682
 0683                    var anyLocalProvidersChanged = providersWithChanges.OfType<ILocalMetadataProvider>()
 0684                        .Any();
 685
 0686                    var anyLocalPreRefreshProvidersChanged = providersWithChanges.OfType<IPreRefreshProvider>()
 0687                        .Any();
 688
 0689                    providers = providers.Where(i =>
 0690                    {
 0691                        // If any provider reports a change, always run local ones as well
 0692                        if (i is ILocalMetadataProvider)
 0693                        {
 0694                            return anyRemoteProvidersChanged || anyLocalProvidersChanged || anyLocalPreRefreshProvidersC
 0695                        }
 0696
 0697                        // If any remote providers changed, run them all so that priorities can be honored
 0698                        if (i is IRemoteMetadataProvider)
 0699                        {
 0700                            if (options.MetadataRefreshMode == MetadataRefreshMode.ValidationOnly)
 0701                            {
 0702                                return false;
 0703                            }
 0704
 0705                            return anyRemoteProvidersChanged;
 0706                        }
 0707
 0708                        // Run custom refresh providers if they report a change or any remote providers change
 0709                        return anyRemoteProvidersChanged || providersWithChanges.Contains(i);
 0710                    }).ToList();
 711                }
 712            }
 713
 114714            return providers;
 715        }
 716
 717        protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvide
 718        {
 719            // Get providers to refresh
 113720            var providers = allImageProviders.Where(i => i is not ILocalImageProvider);
 721
 722            // When identifying, run the provider the user picked first so the correct image is used.
 113723            if (!string.IsNullOrEmpty(options.SearchResult?.SearchProviderName))
 724            {
 0725                providers = providers
 0726                    .OrderBy(i => string.Equals(i.Name, options.SearchResult.SearchProviderName, StringComparison.Ordina
 727            }
 728
 113729            var dateLastImageRefresh = item.DateLastRefreshed;
 730
 731            // Run all if either of these flags are true
 113732            var runAllProviders = options.ImageRefreshMode == MetadataRefreshMode.FullRefresh || dateLastImageRefresh.Da
 733
 113734            if (!runAllProviders)
 735            {
 62736                providers = providers
 62737                    .Where(i =>
 62738                    {
 62739                        if (i is IHasItemChangeMonitor hasFileChangeMonitor)
 62740                        {
 62741                            return HasChanged(item, hasFileChangeMonitor, options.DirectoryService);
 62742                        }
 62743
 62744                        return false;
 62745                    });
 746            }
 747
 113748            return providers;
 749        }
 750
 751        public bool CanRefresh(BaseItem item)
 752        {
 1508753            return item is TItemType;
 754        }
 755
 756        public bool CanRefreshPrimary(Type type)
 757        {
 2444758            return type == typeof(TItemType);
 759        }
 760
 761        protected virtual async Task<RefreshResult> RefreshWithProviders(
 762            MetadataResult<TItemType> metadata,
 763            TIdType id,
 764            MetadataRefreshOptions options,
 765            ICollection<IMetadataProvider> providers,
 766            ItemImageProvider imageService,
 767            bool isSavingMetadata,
 768            CancellationToken cancellationToken)
 769        {
 57770            var refreshResult = new RefreshResult
 57771            {
 57772                UpdateType = ItemUpdateType.None
 57773            };
 774
 57775            var item = metadata.Item;
 776
 57777            var customProviders = providers.OfType<ICustomMetadataProvider<TItemType>>().ToList();
 57778            var logName = !item.IsFileProtocol ? item.Name ?? item.Path : item.Path ?? item.Name;
 779
 114780            foreach (var provider in customProviders.Where(i => i is IPreRefreshProvider))
 781            {
 0782                await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwa
 783            }
 784
 57785            if (item.IsLocked)
 786            {
 0787                return refreshResult;
 788            }
 789
 57790            var temp = new MetadataResult<TItemType>
 57791            {
 57792                Item = CreateNew()
 57793            };
 57794            temp.Item.Path = item.Path;
 57795            temp.Item.Id = item.Id;
 57796            temp.Item.ParentIndexNumber = item.ParentIndexNumber;
 57797            temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
 57798            temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
 799
 57800            var foundImageTypes = new List<ImageType>();
 801
 802            // Do not execute local providers if we are identifying or replacing with local metadata saving enabled
 57803            if (options.SearchResult is null && !(isSavingMetadata && options.ReplaceAllMetadata))
 804            {
 115805                foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
 806                {
 1807                    var providerName = provider.GetType().Name;
 1808                    Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
 809
 1810                    var itemInfo = new ItemInfo(item);
 811
 812                    try
 813                    {
 1814                        var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken
 815
 1816                        if (localItem.HasMetadata)
 817                        {
 2818                            foreach (var remoteImage in localItem.RemoteImages)
 819                            {
 820                                try
 821                                {
 0822                                    if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
 0823                                        && !options.IsReplacingImage(remoteImage.Type))
 824                                    {
 0825                                        continue;
 826                                    }
 827
 0828                                    await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cance
 0829                                    refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
 830
 831                                    // remember imagetype that has just been downloaded
 0832                                    foundImageTypes.Add(remoteImage.Type);
 0833                                }
 0834                                catch (HttpRequestException ex)
 835                                {
 0836                                    Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteIm
 0837                                }
 0838                            }
 839
 1840                            if (foundImageTypes.Count > 0)
 841                            {
 0842                                imageService.UpdateReplaceImages(options, foundImageTypes);
 843                            }
 844
 1845                            if (imageService.MergeImages(item, localItem.Images, options))
 846                            {
 0847                                refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
 848                            }
 849
 1850                            MergeData(localItem, temp, [], false, true);
 1851                            refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
 852
 1853                            break;
 854                        }
 855
 0856                        Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
 0857                    }
 0858                    catch (OperationCanceledException)
 859                    {
 0860                        throw;
 861                    }
 0862                    catch (Exception ex)
 863                    {
 0864                        Logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, logName);
 865
 866                        // If a local provider fails, consider that a failure
 0867                        refreshResult.ErrorMessage = ex.Message;
 0868                    }
 0869                }
 870            }
 871
 57872            var hasRemoteMetadata = false;
 57873            var isLocalLocked = temp.Item.IsLocked;
 57874            if (!isLocalLocked && (options.ReplaceAllMetadata || options.MetadataRefreshMode > MetadataRefreshMode.Valid
 875            {
 57876                var remoteProviders = providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>();
 877
 878                // When identifying, run the provider the user picked first so the correct IDs are used.
 57879                if (!string.IsNullOrEmpty(options.SearchResult?.SearchProviderName))
 880                {
 0881                    remoteProviders = remoteProviders
 0882                        .OrderBy(i => string.Equals(i.Name, options.SearchResult.SearchProviderName, StringComparison.Or
 883                }
 884
 57885                var remoteResult = await ExecuteRemoteProviders(temp, logName, false, id, remoteProviders, cancellationT
 886
 57887                hasRemoteMetadata = remoteResult.UpdateType.HasFlag(ItemUpdateType.MetadataDownload);
 57888                refreshResult.UpdateType |= remoteResult.UpdateType;
 57889                refreshResult.ErrorMessage = remoteResult.ErrorMessage;
 57890                refreshResult.Failures += remoteResult.Failures;
 891            }
 892
 57893            if (providers.Any(i => i is not ICustomMetadataProvider))
 894            {
 8895                if (refreshResult.UpdateType > ItemUpdateType.None)
 896                {
 897                    // Erasing the old values is only safe when a remote provider returned something to
 898                    // replace them with. If every one of them failed there is no replacement, and wiping the
 899                    // item would turn a provider being temporarily unreachable into permanent data loss.
 900                    // A single failure is not enough: Identify asks for the erasure precisely because the
 901                    // previous match was wrong, and an unrelated provider throwing must not undo that.
 7902                    if (!options.RemoveOldMetadata || (refreshResult.Failures > 0 && !hasRemoteMetadata))
 903                    {
 904                        // Add existing metadata to provider result if it does not exist there
 5905                        MergeData(metadata, temp, [], false, false);
 906                    }
 907
 7908                    if (isLocalLocked)
 909                    {
 0910                        MergeData(temp, metadata, item.LockedFields, true, true);
 911                    }
 912                    else
 913                    {
 7914                        var shouldReplace = (options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly && options
 7915                            // Case for Scan for new and updated files
 7916                            || (options.MetadataRefreshMode == MetadataRefreshMode.Default && !options.ReplaceAllMetadat
 7917                        MergeData(temp, metadata, item.LockedFields, shouldReplace, true);
 918                    }
 919                }
 920            }
 921
 212922            foreach (var provider in customProviders.Where(i => i is not IPreRefreshProvider))
 923            {
 49924                await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwa
 925            }
 926
 57927            return refreshResult;
 57928        }
 929
 930        private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, string logName
 931        {
 49932            Logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, logName);
 933
 934            try
 935            {
 49936                refreshResult.UpdateType |= await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(f
 46937            }
 0938            catch (OperationCanceledException)
 939            {
 0940                throw;
 941            }
 3942            catch (Exception ex)
 943            {
 3944                refreshResult.Failures++;
 3945                refreshResult.ErrorMessage = ex.Message;
 3946                Logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, logName);
 3947            }
 49948        }
 949
 950        protected virtual TItemType CreateNew()
 951        {
 57952            return new TItemType();
 953        }
 954
 955        private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, bool re
 956        {
 57957            var refreshResult = new RefreshResult();
 57958            var preferredLanguage = id?.MetadataLanguage;
 959
 57960            var overviewIsFallback = false;
 57961            var taglineIsFallback = false;
 962
 57963            if (id is not null)
 964            {
 57965                MergeNewData(temp.Item, id);
 966            }
 967
 136968            foreach (var provider in providers)
 969            {
 11970                var providerName = provider.GetType().Name;
 11971                Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
 972
 973                try
 974                {
 11975                    var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
 976
 9977                    if (result.HasMetadata)
 978                    {
 7979                        result.Provider = provider.Name;
 980
 7981                        if (MetadataLanguageUtils.MatchesPreferredLanguage(result.ResultLanguage, preferredLanguage))
 982                        {
 7983                            if (overviewIsFallback && !string.IsNullOrEmpty(result.Item.Overview))
 984                            {
 0985                                temp.Item.Overview = null;
 0986                                overviewIsFallback = false;
 987                            }
 988
 7989                            if (taglineIsFallback && !string.IsNullOrEmpty(result.Item.Tagline))
 990                            {
 0991                                temp.Item.Tagline = null;
 0992                                taglineIsFallback = false;
 993                            }
 994                        }
 995                        else
 996                        {
 0997                            overviewIsFallback |= string.IsNullOrEmpty(temp.Item.Overview) && !string.IsNullOrEmpty(resu
 0998                            taglineIsFallback |= string.IsNullOrEmpty(temp.Item.Tagline) && !string.IsNullOrEmpty(result
 999                        }
 1000
 71001                        LogInvalidProviderIds(result, providerName, logName);
 1002
 71003                        MergeData(result, temp, [], replaceData, false);
 71004                        MergeNewData(temp.Item, id);
 1005
 71006                        refreshResult.UpdateType |= ItemUpdateType.MetadataDownload;
 1007                    }
 1008                    else
 1009                    {
 21010                        Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
 1011                    }
 91012                }
 01013                catch (OperationCanceledException)
 1014                {
 01015                    throw;
 1016                }
 21017                catch (Exception ex)
 1018                {
 21019                    refreshResult.Failures++;
 21020                    refreshResult.ErrorMessage = ex.Message;
 21021                    Logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, logName);
 21022                }
 111023            }
 1024
 571025            return refreshResult;
 571026        }
 1027
 1028        /// <summary>
 1029        /// Reports the ids a provider returned that cannot belong to the provider they are filed under.
 1030        /// </summary>
 1031        /// <remarks>
 1032        /// The ids are dropped when merging, this names the provider that produced them so the source of a
 1033        /// recurring bad id can be found.
 1034        /// </remarks>
 1035        private void LogInvalidProviderIds(MetadataResult<TItemType> result, string providerName, string logName)
 1036        {
 71037            if (!Logger.IsEnabled(LogLevel.Debug))
 1038            {
 71039                return;
 1040            }
 1041
 01042            LogInvalidProviderIds(result.Item?.ProviderIds, providerName, logName, null);
 1043
 01044            if (result.People is null)
 1045            {
 01046                return;
 1047            }
 1048
 01049            foreach (var person in result.People)
 1050            {
 01051                LogInvalidProviderIds(person.ProviderIds, providerName, logName, person.Name);
 1052            }
 01053        }
 1054
 1055        private void LogInvalidProviderIds(IReadOnlyDictionary<string, string> providerIds, string providerName, string 
 1056        {
 01057            if (providerIds is null)
 1058            {
 01059                return;
 1060            }
 1061
 01062            foreach (var (key, value) in providerIds)
 1063            {
 01064                if (ProviderIdsExtensions.IsValidProviderId(key, value))
 1065                {
 1066                    continue;
 1067                }
 1068
 01069                if (personName is null)
 1070                {
 01071                    Logger.LogDebug("Discarding {Key} id '{Value}' returned by {Provider} for {Item}", key, value, provi
 1072                }
 1073                else
 1074                {
 01075                    Logger.LogDebug("Discarding {Key} id '{Value}' returned by {Provider} for {Person} of {Item}", key, 
 1076                }
 1077            }
 01078        }
 1079
 1080        private void MergeNewData(TItemType source, TIdType lookupInfo)
 1081        {
 1082            // Copy new provider id's that may have been obtained
 1321083            foreach (var providerId in source.ProviderIds)
 1084            {
 21085                var key = providerId.Key;
 1086
 21087                if (!ProviderIdsExtensions.IsValidProviderId(key, providerId.Value))
 1088                {
 1089                    continue;
 1090                }
 1091
 1092                // Don't replace existing Id's, unless the one already there is unusable - handing that
 1093                // one to the providers that have yet to run is what makes them fail.
 21094                if (!lookupInfo.ProviderIds.TryGetValue(key, out var existingId)
 21095                    || !ProviderIdsExtensions.IsValidProviderId(key, existingId))
 1096                {
 21097                    lookupInfo.ProviderIds[key] = providerId.Value;
 1098                }
 1099            }
 641100        }
 1101
 1102        private bool HasChanged(BaseItem item, IHasItemChangeMonitor changeMonitor, IDirectoryService directoryService)
 1103        {
 1104            try
 1105            {
 601106                var hasChanged = changeMonitor.HasChanged(item, directoryService);
 1107
 601108                if (hasChanged)
 1109                {
 01110                    Logger.LogDebug("{Monitor} reports change to {Item}", changeMonitor.GetType().Name, item.Path ?? ite
 1111                }
 1112
 601113                return hasChanged;
 1114            }
 01115            catch (Exception ex)
 1116            {
 01117                Logger.LogError(ex, "Error in {Monitor}.HasChanged", changeMonitor.GetType().Name);
 01118                return false;
 1119            }
 601120        }
 1121
 1122        /// <summary>
 1123        /// Merges metadata from source into target.
 1124        /// </summary>
 1125        /// <param name="source">The source for new metadata.</param>
 1126        /// <param name="target">The target to insert new metadata into.</param>
 1127        /// <param name="lockedFields">The fields that are locked and should not be updated.</param>
 1128        /// <param name="replaceData"><c>true</c> if existing data should be replaced.</param>
 1129        /// <param name="mergeMetadataSettings"><c>true</c> if the metadata settings in target should be updated to matc
 1130        /// <exception cref="ArgumentException">Thrown if source or target are null.</exception>
 1131        protected virtual void MergeData(
 1132            MetadataResult<TItemType> source,
 1133            MetadataResult<TItemType> target,
 1134            MetadataField[] lockedFields,
 1135            bool replaceData,
 1136            bool mergeMetadataSettings)
 1137        {
 231138            MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 231139        }
 1140
 1141        internal static void MergeBaseItemData(
 1142            MetadataResult<TItemType> sourceResult,
 1143            MetadataResult<TItemType> targetResult,
 1144            MetadataField[] lockedFields,
 1145            bool replaceData,
 1146            bool mergeMetadataSettings)
 1147        {
 1521148            var source = sourceResult.Item;
 1521149            var target = targetResult.Item;
 1150
 1521151            ArgumentNullException.ThrowIfNull(sourceResult);
 1521152            ArgumentNullException.ThrowIfNull(targetResult);
 1153
 1521154            if (!lockedFields.Contains(MetadataField.Name))
 1155            {
 1491156                if (replaceData || string.IsNullOrEmpty(target.Name))
 1157                {
 1158                    // Safeguard against incoming data having an empty name
 1421159                    if (!string.IsNullOrWhiteSpace(source.Name))
 1160                    {
 161161                        target.Name = source.Name;
 1162                    }
 1163                }
 1164            }
 1165
 1521166            if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
 1167            {
 1511168                target.OriginalTitle = source.OriginalTitle;
 1169            }
 1170
 1521171            if (replaceData || string.IsNullOrEmpty(target.HomePageUrl))
 1172            {
 1521173                target.HomePageUrl = source.HomePageUrl;
 1174            }
 1175
 1521176            if (replaceData || string.IsNullOrEmpty(target.OriginalLanguage))
 1177            {
 1521178                target.OriginalLanguage = source.OriginalLanguage;
 1179            }
 1180
 1521181            if (replaceData || !target.CommunityRating.HasValue)
 1182            {
 1511183                target.CommunityRating = source.CommunityRating;
 1184            }
 1185
 1521186            if (replaceData || !target.EndDate.HasValue)
 1187            {
 1511188                target.EndDate = source.EndDate;
 1189            }
 1190
 1521191            if (!lockedFields.Contains(MetadataField.Genres))
 1192            {
 1501193                if (replaceData || target.Genres.Length == 0)
 1194                {
 1491195                    target.Genres = source.Genres;
 1196                }
 1197            }
 1198
 1521199            if (replaceData || !target.IndexNumber.HasValue)
 1200            {
 1511201                target.IndexNumber = source.IndexNumber;
 1202            }
 1203
 1521204            if (!lockedFields.Contains(MetadataField.OfficialRating))
 1205            {
 1491206                if (replaceData || string.IsNullOrEmpty(target.OfficialRating))
 1207                {
 1481208                    target.OfficialRating = source.OfficialRating;
 1209                }
 1210            }
 1211
 1521212            if (replaceData || string.IsNullOrEmpty(target.CustomRating))
 1213            {
 1511214                target.CustomRating = source.CustomRating;
 1215            }
 1216
 1521217            if (replaceData || string.IsNullOrEmpty(target.Tagline))
 1218            {
 1501219                target.Tagline = source.Tagline;
 1220            }
 1221
 1521222            if (!lockedFields.Contains(MetadataField.Overview))
 1223            {
 1491224                if (replaceData || string.IsNullOrEmpty(target.Overview))
 1225                {
 1481226                    target.Overview = source.Overview;
 1227                }
 1228            }
 1229
 1521230            if (replaceData || !target.ParentIndexNumber.HasValue)
 1231            {
 1481232                target.ParentIndexNumber = source.ParentIndexNumber;
 1233            }
 1234
 1521235            if (!lockedFields.Contains(MetadataField.Cast))
 1236            {
 1511237                RemoveInvalidProviderIds(sourceResult.People);
 1511238                RemoveInvalidProviderIds(targetResult.People);
 1239
 1511240                if (replaceData || targetResult.People is null || targetResult.People.Count == 0)
 1241                {
 1441242                    targetResult.People = sourceResult.People;
 1243                }
 71244                else if (sourceResult.People is not null && sourceResult.People.Count > 0)
 1245                {
 71246                    MergePeople(sourceResult.People, targetResult.People);
 1247                }
 1248            }
 1249
 1521250            if (replaceData || !target.PremiereDate.HasValue)
 1251            {
 1511252                target.PremiereDate = source.PremiereDate;
 1253            }
 1254
 1521255            if (replaceData || target.ProductionYear is null)
 1256            {
 1511257                target.ProductionYear = source.ProductionYear;
 1258            }
 1259
 1521260            if (!lockedFields.Contains(MetadataField.Runtime))
 1261            {
 1521262                if (replaceData || !target.RunTimeTicks.HasValue)
 1263                {
 1521264                    if (target is not Audio && target is not Video && target is not Book)
 1265                    {
 491266                        target.RunTimeTicks = source.RunTimeTicks;
 1267                    }
 1268                }
 1269            }
 1270
 1521271            if (!lockedFields.Contains(MetadataField.Studios))
 1272            {
 1501273                if (replaceData || target.Studios.Length == 0)
 1274                {
 1491275                    target.Studios = source.Studios;
 1276                }
 1277                else
 1278                {
 11279                    target.Studios = target.Studios.Concat(source.Studios).Distinct(StringComparer.OrdinalIgnoreCase).To
 1280                }
 1281            }
 1282
 1521283            if (!lockedFields.Contains(MetadataField.Tags))
 1284            {
 1501285                if (replaceData || target.Tags.Length == 0)
 1286                {
 1491287                    target.Tags = source.Tags;
 1288                }
 1289                else
 1290                {
 11291                    target.Tags = target.Tags.Concat(source.Tags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 1292                }
 1293            }
 1294
 1521295            if (!lockedFields.Contains(MetadataField.ProductionLocations))
 1296            {
 1501297                if (replaceData || target.ProductionLocations.Length == 0)
 1298                {
 1491299                    target.ProductionLocations = source.ProductionLocations;
 1300                }
 1301                else
 1302                {
 11303                    target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct(
 1304                }
 1305            }
 1306
 3221307            foreach (var id in source.ProviderIds)
 1308            {
 91309                var key = id.Key;
 1310
 1311                // An id that cannot belong to the provider it is filed under only breaks that provider on
 1312                // the next refresh, so never let one in - not even when replacing all metadata.
 91313                if (!ProviderIdsExtensions.IsValidProviderId(key, id.Value))
 1314                {
 1315                    continue;
 1316                }
 1317
 1318                // Don't replace existing Id's, unless the stored one is unusable - that one is the bad
 1319                // match the refresh is meant to repair.
 81320                if (replaceData
 81321                    || !target.ProviderIds.TryGetValue(key, out var existingId)
 81322                    || !ProviderIdsExtensions.IsValidProviderId(key, existingId))
 1323                {
 61324                    target.ProviderIds[key] = id.Value;
 1325                }
 1326            }
 1327
 1328            // A bad id no provider offered a replacement for still has to go, otherwise the item keeps
 1329            // failing the same way on every refresh.
 3041330            foreach (var key in target.ProviderIds
 1521331                .Where(id => !ProviderIdsExtensions.IsValidProviderId(id.Key, id.Value))
 1521332                .Select(id => id.Key)
 1521333                .ToArray())
 1334            {
 01335                target.ProviderIds.Remove(key);
 1336            }
 1337
 1521338            if (replaceData || !target.CriticRating.HasValue)
 1339            {
 1511340                target.CriticRating = source.CriticRating;
 1341            }
 1342
 1521343            if (replaceData || target.RemoteTrailers.Count == 0)
 1344            {
 1511345                target.RemoteTrailers = source.RemoteTrailers;
 1346            }
 1347            else
 1348            {
 11349                target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).DistinctBy(t => t.Url).ToArr
 1350            }
 1351
 1521352            MergeAlbumArtist(source, target, replaceData);
 1521353            MergeVideoInfo(source, target, replaceData);
 1521354            MergeDisplayOrder(source, target, replaceData);
 1355
 1521356            if (replaceData || string.IsNullOrEmpty(target.ForcedSortName))
 1357            {
 1511358                var forcedSortName = source.ForcedSortName;
 1511359                if (!string.IsNullOrEmpty(forcedSortName))
 1360                {
 31361                    target.ForcedSortName = forcedSortName;
 1362                }
 1363            }
 1364
 1521365            if (mergeMetadataSettings)
 1366            {
 121367                if (replaceData || !target.IsLocked)
 1368                {
 121369                    target.IsLocked = target.IsLocked || source.IsLocked;
 1370                }
 1371
 121372                if (target.LockedFields.Length == 0)
 1373                {
 101374                    target.LockedFields = source.LockedFields;
 1375                }
 1376                else
 1377                {
 21378                    target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray();
 1379                }
 1380
 121381                if (source.DateCreated != DateTime.MinValue)
 1382                {
 11383                    target.DateCreated = source.DateCreated;
 1384                }
 1385
 121386                if (replaceData || source.DateModified != DateTime.MinValue)
 1387                {
 81388                    target.DateModified = source.DateModified;
 1389                }
 1390
 121391                if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode))
 1392                {
 121393                    target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
 1394                }
 1395
 121396                if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataLanguage))
 1397                {
 121398                    target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
 1399                }
 1400            }
 1521401        }
 1402
 1403        private static void RemoveInvalidProviderIds(IReadOnlyList<PersonInfo> people)
 1404        {
 3021405            if (people is null)
 1406            {
 2771407                return;
 1408            }
 1409
 961410            foreach (var person in people)
 1411            {
 231412                if (person.ProviderIds is null || person.ProviderIds.Count == 0)
 1413                {
 1414                    continue;
 1415                }
 1416
 121417                var invalidKeys = person.ProviderIds
 121418                    .Where(id => !ProviderIdsExtensions.IsValidProviderId(id.Key, id.Value))
 121419                    .Select(id => id.Key)
 121420                    .ToArray();
 1421
 281422                foreach (var key in invalidKeys)
 1423                {
 21424                    person.ProviderIds.Remove(key);
 1425                }
 1426            }
 251427        }
 1428
 1429        private static void MergePeople(IReadOnlyList<PersonInfo> source, IReadOnlyList<PersonInfo> target)
 1430        {
 71431            var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
 71432            var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
 1433
 281434            foreach (var name in targetByName.Select(g => g.Key))
 1435            {
 71436                var targetPeople = targetByName[name].ToArray();
 71437                var sourcePeople = sourceByName[name].ToArray();
 1438
 71439                if (sourcePeople.Length == 0)
 1440                {
 1441                    continue;
 1442                }
 1443
 241444                for (int i = 0; i < targetPeople.Length; i++)
 1445                {
 61446                    var person = targetPeople[i];
 61447                    var personInSource = i < sourcePeople.Length ? sourcePeople[i] : sourcePeople[0];
 1448
 181449                    foreach (var providerId in personInSource.ProviderIds)
 1450                    {
 31451                        person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
 1452                    }
 1453
 61454                    if (string.IsNullOrWhiteSpace(person.ImageUrl))
 1455                    {
 51456                        person.ImageUrl = personInSource.ImageUrl;
 1457                    }
 1458
 61459                    if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role))
 1460                    {
 01461                        person.Role = personInSource.Role;
 1462                    }
 1463
 61464                    if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue)
 1465                    {
 01466                        person.SortOrder = personInSource.SortOrder;
 1467                    }
 1468                }
 1469            }
 71470        }
 1471
 1472        private static void MergeDisplayOrder(BaseItem source, BaseItem target, bool replaceData)
 1473        {
 1521474            if (source is IHasDisplayOrder sourceHasDisplayOrder
 1521475                && target is IHasDisplayOrder targetHasDisplayOrder)
 1476            {
 491477                if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder))
 1478                {
 481479                    var displayOrder = sourceHasDisplayOrder.DisplayOrder;
 481480                    if (!string.IsNullOrWhiteSpace(displayOrder))
 1481                    {
 31482                        targetHasDisplayOrder.DisplayOrder = displayOrder;
 1483                    }
 1484                }
 1485            }
 1521486        }
 1487
 1488        private static void MergeAlbumArtist(BaseItem source, BaseItem target, bool replaceData)
 1489        {
 1521490            if (source is IHasAlbumArtist sourceHasAlbumArtist
 1521491                && target is IHasAlbumArtist targetHasAlbumArtist)
 1492            {
 281493                if (replaceData || targetHasAlbumArtist.AlbumArtists.Count == 0)
 1494                {
 271495                    targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
 1496                }
 11497                else if (sourceHasAlbumArtist.AlbumArtists.Count > 0)
 1498                {
 11499                    targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.Al
 1500                }
 1501            }
 1251502        }
 1503
 1504        private static void MergeVideoInfo(BaseItem source, BaseItem target, bool replaceData)
 1505        {
 1521506            if (source is Video sourceCast && target is Video targetCast)
 1507            {
 751508                if (sourceCast.Video3DFormat.HasValue && (replaceData || !targetCast.Video3DFormat.HasValue))
 1509                {
 21510                    targetCast.Video3DFormat = sourceCast.Video3DFormat;
 1511                }
 1512            }
 1521513        }
 1514    }
 1515}

Methods/Properties

.ctor(MediaBrowser.Controller.Configuration.IServerConfigurationManager,Microsoft.Extensions.Logging.ILogger`1<MediaBrowser.Providers.Manager.MetadataService`2<TItemType,TIdType>>,MediaBrowser.Controller.Providers.IProviderManager,MediaBrowser.Model.IO.IFileSystem,MediaBrowser.Controller.Library.ILibraryManager,MediaBrowser.Controller.IO.IExternalDataManager,MediaBrowser.Controller.Persistence.IItemRepository)
get_EnableUpdatingPremiereDateFromChildren()
get_EnableUpdatingGenresFromChildren()
get_EnableUpdatingStudiosFromChildren()
get_EnableUpdatingOfficialRatingFromChildren()
get_Order()
TryGetFileSystemMetadata(System.String,MediaBrowser.Controller.Providers.IDirectoryService)
RefreshMetadata()
ApplySearchResult(MediaBrowser.Controller.Providers.ItemLookupInfo,MediaBrowser.Model.Providers.RemoteSearchResult)
GetValidProviderIds(System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>)
SaveItemAsync()
AfterMetadataRefresh(TItemType,MediaBrowser.Controller.Providers.MetadataRefreshOptions,System.Threading.CancellationToken)
BeforeSave()
BeforeSaveInternal(TItemType,System.Boolean,MediaBrowser.Controller.Library.ItemUpdateType)
EnableUpdateMetadataFromChildren(TItemType,System.Boolean,MediaBrowser.Controller.Library.ItemUpdateType)
GetChildrenForMetadataUpdates(TItemType)
UpdateMetadataFromChildren(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,System.Boolean,MediaBrowser.Controller.Library.ItemUpdateType)
UpdateCumulativeRunTimeTicks(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
UpdateDateLastMediaAdded(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
UpdatePremiereDate(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
UpdateGenres(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
UpdateStudios(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
UpdateOfficialRating(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
GetProviders(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Configuration.LibraryOptions,MediaBrowser.Controller.Providers.MetadataRefreshOptions,System.Boolean,System.Boolean)
GetNonLocalImageProviders(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Providers.IImageProvider>,MediaBrowser.Controller.Providers.MetadataRefreshOptions)
CanRefresh(MediaBrowser.Controller.Entities.BaseItem)
CanRefreshPrimary(System.Type)
RefreshWithProviders()
RunCustomProvider()
CreateNew()
ExecuteRemoteProviders()
LogInvalidProviderIds(MediaBrowser.Controller.Providers.MetadataResult`1<TItemType>,System.String,System.String)
LogInvalidProviderIds(System.Collections.Generic.IReadOnlyDictionary`2<System.String,System.String>,System.String,System.String,System.String)
MergeNewData(TItemType,TIdType)
HasChanged(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Providers.IHasItemChangeMonitor,MediaBrowser.Controller.Providers.IDirectoryService)
MergeData(MediaBrowser.Controller.Providers.MetadataResult`1<TItemType>,MediaBrowser.Controller.Providers.MetadataResult`1<TItemType>,MediaBrowser.Model.Entities.MetadataField[],System.Boolean,System.Boolean)
MergeBaseItemData(MediaBrowser.Controller.Providers.MetadataResult`1<TItemType>,MediaBrowser.Controller.Providers.MetadataResult`1<TItemType>,MediaBrowser.Model.Entities.MetadataField[],System.Boolean,System.Boolean)
RemoveInvalidProviderIds(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.PersonInfo>)
MergePeople(System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.PersonInfo>,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.PersonInfo>)
MergeDisplayOrder(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem,System.Boolean)
MergeAlbumArtist(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem,System.Boolean)
MergeVideoInfo(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Controller.Entities.BaseItem,System.Boolean)