< 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
65%
Covered lines: 229
Uncovered lines: 119
Coverable lines: 348
Total lines: 1310
Line coverage: 65.8%
Branch coverage
64%
Covered branches: 236
Total branches: 366
Branch coverage: 64.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 8/13/2025 - 12:11:40 AM Line coverage: 67.9% (233/343) Branch coverage: 67.1% (235/350) Total lines: 12899/7/2025 - 12:11:13 AM Line coverage: 67.9% (233/343) Branch coverage: 67.3% (237/352) Total lines: 128910/28/2025 - 12:11:27 AM Line coverage: 66.1% (227/343) Branch coverage: 66.1% (233/352) Total lines: 128911/4/2025 - 12:11:59 AM Line coverage: 66.1% (227/343) Branch coverage: 66.1% (233/352) Total lines: 129411/18/2025 - 12:11:25 AM Line coverage: 65.8% (229/348) Branch coverage: 64.4% (236/366) Total lines: 1310 8/13/2025 - 12:11:40 AM Line coverage: 67.9% (233/343) Branch coverage: 67.1% (235/350) Total lines: 12899/7/2025 - 12:11:13 AM Line coverage: 67.9% (233/343) Branch coverage: 67.3% (237/352) Total lines: 128910/28/2025 - 12:11:27 AM Line coverage: 66.1% (227/343) Branch coverage: 66.1% (233/352) Total lines: 128911/4/2025 - 12:11:59 AM Line coverage: 66.1% (227/343) Branch coverage: 66.1% (233/352) Total lines: 129411/18/2025 - 12:11:25 AM Line coverage: 65.8% (229/348) Branch coverage: 64.4% (236/366) Total lines: 1310

Metrics

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        {
 54640            ServerConfigurationManager = serverConfigurationManager;
 54641            Logger = logger;
 54642            ProviderManager = providerManager;
 54643            FileSystem = fileSystem;
 54644            LibraryManager = libraryManager;
 54645            ExternalDataManager = externalDataManager;
 54646            ItemRepository = itemRepository;
 54647            ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
 54648        }
 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
 3566        protected virtual bool EnableUpdatingPremiereDateFromChildren => false;
 67
 3568        protected virtual bool EnableUpdatingGenresFromChildren => false;
 69
 3570        protected virtual bool EnableUpdatingStudiosFromChildren => false;
 71
 3572        protected virtual bool EnableUpdatingOfficialRatingFromChildren => false;
 73
 50474        public virtual int Order => 0;
 75
 76        private FileSystemMetadata TryGetFileSystemMetadata(string path, IDirectoryService directoryService)
 77        {
 78            try
 79            {
 3680                return directoryService.GetFileSystemEntry(path);
 81            }
 082            catch (Exception ex)
 83            {
 084                Logger.LogError(ex, "Error getting file {Path}", path);
 085                return null;
 86            }
 3687        }
 88
 89        public virtual async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, 
 90        {
 91            var itemOfType = (TItemType)item;
 92            var updateType = ItemUpdateType.None;
 93
 94            var libraryOptions = LibraryManager.GetLibraryOptions(item);
 95            var isFirstRefresh = item.DateLastRefreshed == DateTime.MinValue;
 96            var hasRefreshedMetadata = true;
 97            var hasRefreshedImages = true;
 98
 99            var requiresRefresh = libraryOptions.AutomaticRefreshIntervalDays > 0 && (DateTime.UtcNow - item.DateLastRef
 100
 101            if (!requiresRefresh && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
 102            {
 103                // TODO: If this returns true, should we instead just change metadata refresh mode to Full?
 104                requiresRefresh = item.RequiresRefresh();
 105
 106                if (requiresRefresh)
 107                {
 108                    Logger.LogDebug("Refreshing {Type} {Item} because item.RequiresRefresh() returned true", typeof(TIte
 109                }
 110            }
 111
 112            if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
 113            {
 114                if (ImageProvider.RemoveImages(item))
 115                {
 116                    updateType |= ItemUpdateType.ImageUpdate;
 117                }
 118            }
 119
 120            var localImagesFailed = false;
 121            var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
 122
 123            // Only validate already registered images if we are replacing and saving locally
 124            if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages)
 125            {
 126                item.ValidateImages();
 127            }
 128            else
 129            {
 130                // Run full image validation and register new local images
 131                try
 132                {
 133                    if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptio
 134                    {
 135                        updateType |= ItemUpdateType.ImageUpdate;
 136                    }
 137                }
 138                catch (Exception ex)
 139                {
 140                    localImagesFailed = true;
 141                    Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
 142                }
 143            }
 144
 145            var metadataResult = new MetadataResult<TItemType>
 146            {
 147                Item = itemOfType
 148            };
 149
 150            var beforeSaveResult = await BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || r
 151                .ConfigureAwait(false);
 152            updateType |= beforeSaveResult;
 153
 154            if (!isFirstRefresh)
 155            {
 156                updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metad
 157            }
 158
 159            // Next run metadata providers
 160            if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
 161            {
 162                var providers = GetProviders(item, libraryOptions, refreshOptions, isFirstRefresh, requiresRefresh)
 163                    .ToList();
 164
 165                if (providers.Count > 0 || isFirstRefresh || requiresRefresh)
 166                {
 167                    if (item.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata))
 168                    {
 169                        updateType |= ItemUpdateType.MetadataImport;
 170                    }
 171                }
 172
 173                if (providers.Count > 0)
 174                {
 175                    var id = itemOfType.GetLookupInfo();
 176
 177                    if (refreshOptions.SearchResult is not null)
 178                    {
 179                        ApplySearchResult(id, refreshOptions.SearchResult);
 180                    }
 181
 182                    id.IsAutomated = refreshOptions.IsAutomated;
 183
 184                    var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any();
 185                    var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider
 186
 187                    updateType |= result.UpdateType;
 188                    if (result.Failures > 0)
 189                    {
 190                        hasRefreshedMetadata = false;
 191                    }
 192                }
 193            }
 194
 195            // Next run remote image providers, but only if local image providers didn't throw an exception
 196            if (!localImagesFailed && refreshOptions.ImageRefreshMode > MetadataRefreshMode.ValidationOnly)
 197            {
 198                var providers = GetNonLocalImageProviders(item, allImageProviders, refreshOptions).ToList();
 199
 200                if (providers.Count > 0)
 201                {
 202                    var result = await ImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions
 203
 204                    updateType |= result.UpdateType;
 205                    if (result.Failures > 0)
 206                    {
 207                        hasRefreshedImages = false;
 208                    }
 209                }
 210            }
 211
 212            if (hasRefreshedMetadata && hasRefreshedImages)
 213            {
 214                item.DateLastRefreshed = DateTime.UtcNow;
 215                updateType |= item.OnMetadataChanged();
 216            }
 217
 218            updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataR
 219
 220            await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false);
 221
 222            return updateType;
 223
 224            async Task<ItemUpdateType> SaveInternal(BaseItem item, MetadataRefreshOptions refreshOptions, ItemUpdateType
 225            {
 226                // Save if changes were made, or it's never been saved before
 227                if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.Rep
 228                {
 229                    if (item.IsFileProtocol)
 230                    {
 231                        var file = TryGetFileSystemMetadata(item.Path, refreshOptions.DirectoryService);
 232                        if (file is not null)
 233                        {
 234                            item.DateModified = file.LastWriteTimeUtc;
 235
 236                            if (!file.IsDirectory)
 237                            {
 238                                item.Size = file.Length;
 239                            }
 240                        }
 241                    }
 242
 243                    // If any of these properties are set then make sure the updateType is not None, just to force every
 244                    if (refreshOptions.ForceSave || refreshOptions.ReplaceAllMetadata)
 245                    {
 246                        updateType |= ItemUpdateType.MetadataDownload;
 247                    }
 248
 249                    // Save to database
 250                    await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
 251                }
 252
 253                return updateType;
 254            }
 255        }
 256
 257        private void ApplySearchResult(ItemLookupInfo lookupInfo, RemoteSearchResult result)
 258        {
 259            // Episode and Season do not support Identify, so the search results are the Series'
 260            switch (lookupInfo)
 261            {
 262                case EpisodeInfo episodeInfo:
 0263                    episodeInfo.SeriesProviderIds = result.ProviderIds;
 0264                    episodeInfo.ProviderIds.Clear();
 0265                    break;
 266                case SeasonInfo seasonInfo:
 0267                    seasonInfo.SeriesProviderIds = result.ProviderIds;
 0268                    seasonInfo.ProviderIds.Clear();
 0269                    break;
 270                default:
 0271                    lookupInfo.ProviderIds = result.ProviderIds;
 0272                    lookupInfo.Name = result.Name;
 0273                    lookupInfo.Year = result.ProductionYear;
 274                    break;
 275            }
 0276        }
 277
 278        protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken ca
 279        {
 280            await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
 281            if (result.Item.SupportsPeople && result.People is not null)
 282            {
 283                var baseItem = result.Item;
 284
 285                await LibraryManager.UpdatePeopleAsync(baseItem, result.People, cancellationToken).ConfigureAwait(false)
 286            }
 287        }
 288
 289        protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationT
 290        {
 56291            item.AfterMetadataRefresh();
 56292            return Task.CompletedTask;
 293        }
 294
 295        /// <summary>
 296        /// Before the save.
 297        /// </summary>
 298        /// <param name="item">The item.</param>
 299        /// <param name="isFullRefresh">if set to <c>true</c> [is full refresh].</param>
 300        /// <param name="currentUpdateType">Type of the current update.</param>
 301        /// <returns>ItemUpdateType.</returns>
 302        private async Task<ItemUpdateType> BeforeSave(TItemType item, bool isFullRefresh, ItemUpdateType currentUpdateTy
 303        {
 304            var updateType = BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
 305
 306            updateType |= item.OnMetadataChanged();
 307
 308            if (updateType == ItemUpdateType.None)
 309            {
 310                if (!await ItemRepository.ItemExistsAsync(item.Id).ConfigureAwait(false))
 311                {
 312                    return ItemUpdateType.MetadataImport;
 313                }
 314            }
 315
 316            return updateType;
 317        }
 318
 319        protected virtual ItemUpdateType BeforeSaveInternal(TItemType item, bool isFullRefresh, ItemUpdateType updateTyp
 320        {
 57321            if (EnableUpdateMetadataFromChildren(item, isFullRefresh, updateType))
 322            {
 0323                var children = GetChildrenForMetadataUpdates(item);
 0324                updateType = UpdateMetadataFromChildren(item, children, isFullRefresh, updateType);
 325            }
 326
 57327            var presentationUniqueKey = item.CreatePresentationUniqueKey();
 57328            if (!string.Equals(item.PresentationUniqueKey, presentationUniqueKey, StringComparison.Ordinal))
 329            {
 34330                item.PresentationUniqueKey = presentationUniqueKey;
 34331                updateType |= ItemUpdateType.MetadataImport;
 332            }
 333
 334            // Cleanup extracted files if source file was modified
 57335            var itemPath = item.Path;
 57336            if (!string.IsNullOrEmpty(itemPath))
 337            {
 57338                var info = FileSystem.GetFileSystemInfo(itemPath);
 57339                if (info.Exists && item.HasChanged(info.LastWriteTimeUtc))
 340                {
 0341                    Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", item.DateModified, in
 342
 0343                    item.DateModified = info.LastWriteTimeUtc;
 0344                    if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded)
 345                    {
 0346                        if (info.CreationTimeUtc > DateTime.MinValue)
 347                        {
 0348                            item.DateCreated = info.CreationTimeUtc;
 349                        }
 350                    }
 351
 0352                    if (item is Video video)
 353                    {
 0354                        Logger.LogInformation("File changed, pruning extracted data: {Path}", item.Path);
 0355                        ExternalDataManager.DeleteExternalItemDataAsync(video, CancellationToken.None).GetAwaiter().GetR
 356                    }
 357
 0358                    updateType |= ItemUpdateType.MetadataImport;
 359                }
 360            }
 361
 57362            return updateType;
 363        }
 364
 365        protected virtual bool EnableUpdateMetadataFromChildren(TItemType item, bool isFullRefresh, ItemUpdateType curre
 366        {
 57367            if (item is Folder folder)
 368            {
 57369                if (!isFullRefresh && currentUpdateType == ItemUpdateType.None)
 370                {
 22371                    return folder.SupportsDateLastMediaAdded;
 372                }
 373
 35374                if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
 375                {
 35376                    if (EnableUpdatingPremiereDateFromChildren || EnableUpdatingGenresFromChildren || EnableUpdatingStud
 377                    {
 0378                        return true;
 379                    }
 380
 35381                    if (folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks)
 382                    {
 0383                        return true;
 384                    }
 385                }
 386            }
 387
 35388            return false;
 389        }
 390
 391        protected virtual IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(TItemType item)
 392        {
 0393            if (item is Folder folder)
 394            {
 0395                return folder.GetRecursiveChildren();
 396            }
 397
 0398            return [];
 399        }
 400
 401        protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IReadOnlyList<BaseItem> children, bo
 402        {
 0403            var updateType = ItemUpdateType.None;
 404
 0405            if (item is Folder folder)
 406            {
 0407                if (folder.SupportsDateLastMediaAdded)
 408                {
 0409                    updateType |= UpdateDateLastMediaAdded(item, children);
 410                }
 411
 0412                if ((isFullRefresh || currentUpdateType > ItemUpdateType.None) && folder.SupportsCumulativeRunTimeTicks)
 413                {
 0414                    updateType |= UpdateCumulativeRunTimeTicks(item, children);
 415                }
 416            }
 417
 0418            if (!(isFullRefresh || currentUpdateType > ItemUpdateType.None) || item.IsLocked)
 419            {
 0420                return updateType;
 421            }
 422
 0423            if (EnableUpdatingPremiereDateFromChildren)
 424            {
 0425                updateType |= UpdatePremiereDate(item, children);
 426            }
 427
 0428            if (EnableUpdatingGenresFromChildren)
 429            {
 0430                updateType |= UpdateGenres(item, children);
 431            }
 432
 0433            if (EnableUpdatingStudiosFromChildren)
 434            {
 0435                updateType |= UpdateStudios(item, children);
 436            }
 437
 0438            if (EnableUpdatingOfficialRatingFromChildren)
 439            {
 0440                updateType |= UpdateOfficialRating(item, children);
 441            }
 442
 0443            return updateType;
 444        }
 445
 446        private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IReadOnlyList<BaseItem> children)
 447        {
 0448            if (item is Folder folder && folder.SupportsCumulativeRunTimeTicks)
 449            {
 0450                long ticks = 0;
 451
 0452                foreach (var child in children)
 453                {
 0454                    if (!child.IsFolder)
 455                    {
 0456                        ticks += child.RunTimeTicks ?? 0;
 457                    }
 458                }
 459
 0460                if (!folder.RunTimeTicks.HasValue || folder.RunTimeTicks.Value != ticks)
 461                {
 0462                    folder.RunTimeTicks = ticks;
 0463                    return ItemUpdateType.MetadataImport;
 464                }
 465            }
 466
 0467            return ItemUpdateType.None;
 468        }
 469
 470        private ItemUpdateType UpdateDateLastMediaAdded(TItemType item, IReadOnlyList<BaseItem> children)
 471        {
 0472            var updateType = ItemUpdateType.None;
 473
 0474            if (item is Folder folder && folder.SupportsDateLastMediaAdded)
 475            {
 0476                var dateLastMediaAdded = DateTime.MinValue;
 0477                var any = false;
 478
 0479                foreach (var child in children)
 480                {
 481                    // Exclude any folders and virtual items since they are only placeholders
 0482                    if (!child.IsFolder && !child.IsVirtualItem)
 483                    {
 0484                        var childDateCreated = child.DateCreated;
 0485                        if (childDateCreated > dateLastMediaAdded)
 486                        {
 0487                            dateLastMediaAdded = childDateCreated;
 488                        }
 489
 0490                        any = true;
 491                    }
 492                }
 493
 0494                if ((!folder.DateLastMediaAdded.HasValue && any) || folder.DateLastMediaAdded != dateLastMediaAdded)
 495                {
 0496                    folder.DateLastMediaAdded = dateLastMediaAdded;
 0497                    updateType = ItemUpdateType.MetadataImport;
 498                }
 499            }
 500
 0501            return updateType;
 502        }
 503
 504        private ItemUpdateType UpdatePremiereDate(TItemType item, IReadOnlyList<BaseItem> children)
 505        {
 0506            var updateType = ItemUpdateType.None;
 507
 0508            if (children.Count == 0)
 509            {
 0510                return updateType;
 511            }
 512
 0513            var date = children.Select(i => i.PremiereDate ?? DateTime.MaxValue).Min();
 514
 0515            var originalPremiereDate = item.PremiereDate;
 0516            var originalProductionYear = item.ProductionYear;
 517
 0518            if (date > DateTime.MinValue && date < DateTime.MaxValue)
 519            {
 0520                item.PremiereDate = date;
 0521                item.ProductionYear = date.Year;
 522            }
 523            else
 524            {
 0525                var year = children.Select(i => i.ProductionYear ?? 0).Min();
 526
 0527                if (year > 0)
 528                {
 0529                    item.ProductionYear = year;
 530                }
 531            }
 532
 0533            if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue)
 0534                || (originalProductionYear ?? -1) != (item.ProductionYear ?? -1))
 535            {
 0536                updateType |= ItemUpdateType.MetadataEdit;
 537            }
 538
 0539            return updateType;
 540        }
 541
 542        private ItemUpdateType UpdateGenres(TItemType item, IReadOnlyList<BaseItem> children)
 543        {
 0544            var updateType = ItemUpdateType.None;
 545
 0546            if (!item.LockedFields.Contains(MetadataField.Genres))
 547            {
 0548                var currentList = item.Genres;
 549
 0550                item.Genres = children.SelectMany(i => i.Genres)
 0551                    .Distinct(StringComparer.OrdinalIgnoreCase)
 0552                    .ToArray();
 553
 0554                if (currentList.Length != item.Genres.Length || !currentList.Order().SequenceEqual(item.Genres.Order(), 
 555                {
 0556                    updateType |= ItemUpdateType.MetadataEdit;
 557                }
 558            }
 559
 0560            return updateType;
 561        }
 562
 563        private ItemUpdateType UpdateStudios(TItemType item, IReadOnlyList<BaseItem> children)
 564        {
 0565            var updateType = ItemUpdateType.None;
 566
 0567            if (!item.LockedFields.Contains(MetadataField.Studios))
 568            {
 0569                var currentList = item.Studios;
 570
 0571                item.Studios = children.SelectMany(i => i.Studios)
 0572                    .Distinct(StringComparer.OrdinalIgnoreCase)
 0573                    .ToArray();
 574
 0575                if (currentList.Length != item.Studios.Length || !currentList.Order().SequenceEqual(item.Studios.Order()
 576                {
 0577                    updateType |= ItemUpdateType.MetadataEdit;
 578                }
 579            }
 580
 0581            return updateType;
 582        }
 583
 584        private ItemUpdateType UpdateOfficialRating(TItemType item, IReadOnlyList<BaseItem> children)
 585        {
 0586            var updateType = ItemUpdateType.None;
 587
 0588            if (!item.LockedFields.Contains(MetadataField.OfficialRating))
 589            {
 0590                if (item.UpdateRatingToItems(children))
 591                {
 0592                    updateType |= ItemUpdateType.MetadataEdit;
 593                }
 594            }
 595
 0596            return updateType;
 597        }
 598
 599        /// <summary>
 600        /// Gets the providers.
 601        /// </summary>
 602        /// <param name="item">A media item.</param>
 603        /// <param name="libraryOptions">The LibraryOptions to use.</param>
 604        /// <param name="options">The MetadataRefreshOptions to use.</param>
 605        /// <param name="isFirstRefresh">Specifies first refresh mode.</param>
 606        /// <param name="requiresRefresh">Specifies refresh mode.</param>
 607        /// <returns>IEnumerable{`0}.</returns>
 608        protected IEnumerable<IMetadataProvider> GetProviders(BaseItem item, LibraryOptions libraryOptions, MetadataRefr
 609        {
 610            // Get providers to refresh
 57611            var providers = ProviderManager.GetMetadataProviders<TItemType>(item, libraryOptions).ToList();
 612
 57613            var metadataRefreshMode = options.MetadataRefreshMode;
 614
 615            // Run all if either of these flags are true
 57616            var runAllProviders = options.ReplaceAllMetadata ||
 57617                metadataRefreshMode == MetadataRefreshMode.FullRefresh ||
 57618                (isFirstRefresh && metadataRefreshMode >= MetadataRefreshMode.Default) ||
 57619                (requiresRefresh && metadataRefreshMode >= MetadataRefreshMode.Default);
 620
 57621            if (!runAllProviders)
 622            {
 22623                var providersWithChanges = providers
 22624                    .Where(i =>
 22625                    {
 22626                        if (i is IHasItemChangeMonitor hasFileChangeMonitor)
 22627                        {
 22628                            return HasChanged(item, hasFileChangeMonitor, options.DirectoryService);
 22629                        }
 22630
 22631                        return false;
 22632                    })
 22633                    .ToList();
 634
 22635                if (providersWithChanges.Count == 0)
 636                {
 0637                    providers = new List<IMetadataProvider<TItemType>>();
 638                }
 639                else
 640                {
 22641                    var anyRemoteProvidersChanged = providersWithChanges.OfType<IRemoteMetadataProvider>()
 22642                        .Any();
 643
 22644                    var anyLocalProvidersChanged = providersWithChanges.OfType<ILocalMetadataProvider>()
 22645                        .Any();
 646
 22647                    var anyLocalPreRefreshProvidersChanged = providersWithChanges.OfType<IPreRefreshProvider>()
 22648                        .Any();
 649
 22650                    providers = providers.Where(i =>
 22651                    {
 22652                        // If any provider reports a change, always run local ones as well
 22653                        if (i is ILocalMetadataProvider)
 22654                        {
 22655                            return anyRemoteProvidersChanged || anyLocalProvidersChanged || anyLocalPreRefreshProvidersC
 22656                        }
 22657
 22658                        // If any remote providers changed, run them all so that priorities can be honored
 22659                        if (i is IRemoteMetadataProvider)
 22660                        {
 22661                            if (options.MetadataRefreshMode == MetadataRefreshMode.ValidationOnly)
 22662                            {
 22663                                return false;
 22664                            }
 22665
 22666                            return anyRemoteProvidersChanged;
 22667                        }
 22668
 22669                        // Run custom refresh providers if they report a change or any remote providers change
 22670                        return anyRemoteProvidersChanged || providersWithChanges.Contains(i);
 22671                    }).ToList();
 672                }
 673            }
 674
 57675            return providers;
 676        }
 677
 678        protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvide
 679        {
 680            // Get providers to refresh
 57681            var providers = allImageProviders.Where(i => i is not ILocalImageProvider);
 682
 57683            var dateLastImageRefresh = item.DateLastRefreshed;
 684
 685            // Run all if either of these flags are true
 57686            var runAllProviders = options.ImageRefreshMode == MetadataRefreshMode.FullRefresh || dateLastImageRefresh.Da
 687
 57688            if (!runAllProviders)
 689            {
 23690                providers = providers
 23691                    .Where(i =>
 23692                    {
 23693                        if (i is IHasItemChangeMonitor hasFileChangeMonitor)
 23694                        {
 23695                            return HasChanged(item, hasFileChangeMonitor, options.DirectoryService);
 23696                        }
 23697
 23698                        return false;
 23699                    });
 700            }
 701
 57702            return providers;
 703        }
 704
 705        public bool CanRefresh(BaseItem item)
 706        {
 1144707            return item is TItemType;
 708        }
 709
 710        public bool CanRefreshPrimary(Type type)
 711        {
 1410712            return type == typeof(TItemType);
 713        }
 714
 715        protected virtual async Task<RefreshResult> RefreshWithProviders(
 716            MetadataResult<TItemType> metadata,
 717            TIdType id,
 718            MetadataRefreshOptions options,
 719            ICollection<IMetadataProvider> providers,
 720            ItemImageProvider imageService,
 721            bool isSavingMetadata,
 722            CancellationToken cancellationToken)
 723        {
 724            var refreshResult = new RefreshResult
 725            {
 726                UpdateType = ItemUpdateType.None
 727            };
 728
 729            var item = metadata.Item;
 730
 731            var customProviders = providers.OfType<ICustomMetadataProvider<TItemType>>().ToList();
 732            var logName = !item.IsFileProtocol ? item.Name ?? item.Path : item.Path ?? item.Name;
 733
 734            foreach (var provider in customProviders.Where(i => i is IPreRefreshProvider))
 735            {
 736                await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwa
 737            }
 738
 739            if (item.IsLocked)
 740            {
 741                return refreshResult;
 742            }
 743
 744            var temp = new MetadataResult<TItemType>
 745            {
 746                Item = CreateNew()
 747            };
 748            temp.Item.Path = item.Path;
 749            temp.Item.Id = item.Id;
 750            temp.Item.ParentIndexNumber = item.ParentIndexNumber;
 751            temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
 752            temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
 753
 754            var foundImageTypes = new List<ImageType>();
 755
 756            // Do not execute local providers if we are identifying or replacing with local metadata saving enabled
 757            if (options.SearchResult is null && !(isSavingMetadata && options.ReplaceAllMetadata))
 758            {
 759                foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
 760                {
 761                    var providerName = provider.GetType().Name;
 762                    Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
 763
 764                    var itemInfo = new ItemInfo(item);
 765
 766                    try
 767                    {
 768                        var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken
 769
 770                        if (localItem.HasMetadata)
 771                        {
 772                            foreach (var remoteImage in localItem.RemoteImages)
 773                            {
 774                                try
 775                                {
 776                                    if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
 777                                        && !options.IsReplacingImage(remoteImage.Type))
 778                                    {
 779                                        continue;
 780                                    }
 781
 782                                    await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cance
 783                                    refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
 784
 785                                    // remember imagetype that has just been downloaded
 786                                    foundImageTypes.Add(remoteImage.Type);
 787                                }
 788                                catch (HttpRequestException ex)
 789                                {
 790                                    Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteIm
 791                                }
 792                            }
 793
 794                            if (foundImageTypes.Count > 0)
 795                            {
 796                                imageService.UpdateReplaceImages(options, foundImageTypes);
 797                            }
 798
 799                            if (imageService.MergeImages(item, localItem.Images, options))
 800                            {
 801                                refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
 802                            }
 803
 804                            MergeData(localItem, temp, [], false, true);
 805                            refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
 806
 807                            break;
 808                        }
 809
 810                        Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
 811                    }
 812                    catch (OperationCanceledException)
 813                    {
 814                        throw;
 815                    }
 816                    catch (Exception ex)
 817                    {
 818                        Logger.LogError(ex, "Error in {Provider}", provider.Name);
 819
 820                        // If a local provider fails, consider that a failure
 821                        refreshResult.ErrorMessage = ex.Message;
 822                    }
 823                }
 824            }
 825
 826            var isLocalLocked = temp.Item.IsLocked;
 827            if (!isLocalLocked && (options.ReplaceAllMetadata || options.MetadataRefreshMode > MetadataRefreshMode.Valid
 828            {
 829                var remoteResult = await ExecuteRemoteProviders(temp, logName, false, id, providers.OfType<IRemoteMetada
 830                    .ConfigureAwait(false);
 831
 832                refreshResult.UpdateType |= remoteResult.UpdateType;
 833                refreshResult.ErrorMessage = remoteResult.ErrorMessage;
 834                refreshResult.Failures += remoteResult.Failures;
 835            }
 836
 837            if (providers.Any(i => i is not ICustomMetadataProvider))
 838            {
 839                if (refreshResult.UpdateType > ItemUpdateType.None)
 840                {
 841                    if (!options.RemoveOldMetadata)
 842                    {
 843                        // Add existing metadata to provider result if it does not exist there
 844                        MergeData(metadata, temp, [], false, false);
 845                    }
 846
 847                    if (isLocalLocked)
 848                    {
 849                        MergeData(temp, metadata, item.LockedFields, true, true);
 850                    }
 851                    else
 852                    {
 853                        var shouldReplace = (options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly && options
 854                            // Case for Scan for new and updated files
 855                            || (options.MetadataRefreshMode == MetadataRefreshMode.Default && !options.ReplaceAllMetadat
 856                        MergeData(temp, metadata, item.LockedFields, shouldReplace, true);
 857                    }
 858                }
 859            }
 860
 861            foreach (var provider in customProviders.Where(i => i is not IPreRefreshProvider))
 862            {
 863                await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwa
 864            }
 865
 866            return refreshResult;
 867        }
 868
 869        private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, string logName
 870        {
 871            Logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, logName);
 872
 873            try
 874            {
 875                refreshResult.UpdateType |= await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(f
 876            }
 877            catch (OperationCanceledException)
 878            {
 879                throw;
 880            }
 881            catch (Exception ex)
 882            {
 883                refreshResult.ErrorMessage = ex.Message;
 884                Logger.LogError(ex, "Error in {Provider}", provider.Name);
 885            }
 886        }
 887
 888        protected virtual TItemType CreateNew()
 889        {
 57890            return new TItemType();
 891        }
 892
 893        private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, bool re
 894        {
 895            var refreshResult = new RefreshResult();
 896
 897            if (id is not null)
 898            {
 899                MergeNewData(temp.Item, id);
 900            }
 901
 902            foreach (var provider in providers)
 903            {
 904                var providerName = provider.GetType().Name;
 905                Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
 906
 907                try
 908                {
 909                    var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
 910
 911                    if (result.HasMetadata)
 912                    {
 913                        result.Provider = provider.Name;
 914
 915                        MergeData(result, temp, [], replaceData, false);
 916                        MergeNewData(temp.Item, id);
 917
 918                        refreshResult.UpdateType |= ItemUpdateType.MetadataDownload;
 919                    }
 920                    else
 921                    {
 922                        Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
 923                    }
 924                }
 925                catch (OperationCanceledException)
 926                {
 927                    throw;
 928                }
 929                catch (Exception ex)
 930                {
 931                    refreshResult.Failures++;
 932                    refreshResult.ErrorMessage = ex.Message;
 933                    Logger.LogError(ex, "Error in {Provider}", provider.Name);
 934                }
 935            }
 936
 937            return refreshResult;
 938        }
 939
 940        private void MergeNewData(TItemType source, TIdType lookupInfo)
 941        {
 942            // Copy new provider id's that may have been obtained
 114943            foreach (var providerId in source.ProviderIds)
 944            {
 0945                var key = providerId.Key;
 946
 947                // Don't replace existing Id's.
 0948                lookupInfo.ProviderIds.TryAdd(key, providerId.Value);
 949            }
 57950        }
 951
 952        private bool HasChanged(BaseItem item, IHasItemChangeMonitor changeMonitor, IDirectoryService directoryService)
 953        {
 954            try
 955            {
 22956                var hasChanged = changeMonitor.HasChanged(item, directoryService);
 957
 22958                if (hasChanged)
 959                {
 22960                    Logger.LogDebug("{Monitor} reports change to {Item}", changeMonitor.GetType().Name, item.Path ?? ite
 961                }
 962
 22963                return hasChanged;
 964            }
 0965            catch (Exception ex)
 966            {
 0967                Logger.LogError(ex, "Error in {Monitor}.HasChanged", changeMonitor.GetType().Name);
 0968                return false;
 969            }
 22970        }
 971
 972        /// <summary>
 973        /// Merges metadata from source into target.
 974        /// </summary>
 975        /// <param name="source">The source for new metadata.</param>
 976        /// <param name="target">The target to insert new metadata into.</param>
 977        /// <param name="lockedFields">The fields that are locked and should not be updated.</param>
 978        /// <param name="replaceData"><c>true</c> if existing data should be replaced.</param>
 979        /// <param name="mergeMetadataSettings"><c>true</c> if the metadata settings in target should be updated to matc
 980        /// <exception cref="ArgumentException">Thrown if source or target are null.</exception>
 981        protected virtual void MergeData(
 982            MetadataResult<TItemType> source,
 983            MetadataResult<TItemType> target,
 984            MetadataField[] lockedFields,
 985            bool replaceData,
 986            bool mergeMetadataSettings)
 987        {
 0988            MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 0989        }
 990
 991        internal static void MergeBaseItemData(
 992            MetadataResult<TItemType> sourceResult,
 993            MetadataResult<TItemType> targetResult,
 994            MetadataField[] lockedFields,
 995            bool replaceData,
 996            bool mergeMetadataSettings)
 997        {
 129998            var source = sourceResult.Item;
 129999            var target = targetResult.Item;
 1000
 1291001            ArgumentNullException.ThrowIfNull(sourceResult);
 1291002            ArgumentNullException.ThrowIfNull(targetResult);
 1003
 1291004            if (!lockedFields.Contains(MetadataField.Name))
 1005            {
 1261006                if (replaceData || string.IsNullOrEmpty(target.Name))
 1007                {
 1008                    // Safeguard against incoming data having an empty name
 1251009                    if (!string.IsNullOrWhiteSpace(source.Name))
 1010                    {
 31011                        target.Name = source.Name;
 1012                    }
 1013                }
 1014            }
 1015
 1291016            if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
 1017            {
 1281018                target.OriginalTitle = source.OriginalTitle;
 1019            }
 1020
 1291021            if (replaceData || !target.CommunityRating.HasValue)
 1022            {
 1281023                target.CommunityRating = source.CommunityRating;
 1024            }
 1025
 1291026            if (replaceData || !target.EndDate.HasValue)
 1027            {
 1281028                target.EndDate = source.EndDate;
 1029            }
 1030
 1291031            if (!lockedFields.Contains(MetadataField.Genres))
 1032            {
 1271033                if (replaceData || target.Genres.Length == 0)
 1034                {
 1261035                    target.Genres = source.Genres;
 1036                }
 1037            }
 1038
 1291039            if (replaceData || !target.IndexNumber.HasValue)
 1040            {
 1281041                target.IndexNumber = source.IndexNumber;
 1042            }
 1043
 1291044            if (!lockedFields.Contains(MetadataField.OfficialRating))
 1045            {
 1261046                if (replaceData || string.IsNullOrEmpty(target.OfficialRating))
 1047                {
 1251048                    target.OfficialRating = source.OfficialRating;
 1049                }
 1050            }
 1051
 1291052            if (replaceData || string.IsNullOrEmpty(target.CustomRating))
 1053            {
 1281054                target.CustomRating = source.CustomRating;
 1055            }
 1056
 1291057            if (replaceData || string.IsNullOrEmpty(target.Tagline))
 1058            {
 1281059                target.Tagline = source.Tagline;
 1060            }
 1061
 1291062            if (!lockedFields.Contains(MetadataField.Overview))
 1063            {
 1261064                if (replaceData || string.IsNullOrEmpty(target.Overview))
 1065                {
 1251066                    target.Overview = source.Overview;
 1067                }
 1068            }
 1069
 1291070            if (replaceData || !target.ParentIndexNumber.HasValue)
 1071            {
 1281072                target.ParentIndexNumber = source.ParentIndexNumber;
 1073            }
 1074
 1291075            if (!lockedFields.Contains(MetadataField.Cast))
 1076            {
 1281077                if (replaceData || targetResult.People is null || targetResult.People.Count == 0)
 1078                {
 1241079                    targetResult.People = sourceResult.People;
 1080                }
 41081                else if (sourceResult.People is not null && sourceResult.People.Count > 0)
 1082                {
 41083                    MergePeople(sourceResult.People, targetResult.People);
 1084                }
 1085            }
 1086
 1291087            if (replaceData || !target.PremiereDate.HasValue)
 1088            {
 1281089                target.PremiereDate = source.PremiereDate;
 1090            }
 1091
 1291092            if (replaceData || !target.ProductionYear.HasValue)
 1093            {
 1281094                target.ProductionYear = source.ProductionYear;
 1095            }
 1096
 1291097            if (!lockedFields.Contains(MetadataField.Runtime))
 1098            {
 1291099                if (replaceData || !target.RunTimeTicks.HasValue)
 1100                {
 1291101                    if (target is not Audio && target is not Video)
 1102                    {
 491103                        target.RunTimeTicks = source.RunTimeTicks;
 1104                    }
 1105                }
 1106            }
 1107
 1291108            if (!lockedFields.Contains(MetadataField.Studios))
 1109            {
 1271110                if (replaceData || target.Studios.Length == 0)
 1111                {
 1261112                    target.Studios = source.Studios;
 1113                }
 1114                else
 1115                {
 11116                    target.Studios = target.Studios.Concat(source.Studios).Distinct(StringComparer.OrdinalIgnoreCase).To
 1117                }
 1118            }
 1119
 1291120            if (!lockedFields.Contains(MetadataField.Tags))
 1121            {
 1271122                if (replaceData || target.Tags.Length == 0)
 1123                {
 1261124                    target.Tags = source.Tags;
 1125                }
 1126                else
 1127                {
 11128                    target.Tags = target.Tags.Concat(source.Tags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 1129                }
 1130            }
 1131
 1291132            if (!lockedFields.Contains(MetadataField.ProductionLocations))
 1133            {
 1271134                if (replaceData || target.ProductionLocations.Length == 0)
 1135                {
 1261136                    target.ProductionLocations = source.ProductionLocations;
 1137                }
 1138                else
 1139                {
 11140                    target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct(
 1141                }
 1142            }
 1143
 2661144            foreach (var id in source.ProviderIds)
 1145            {
 41146                var key = id.Key;
 1147
 1148                // Don't replace existing Id's.
 41149                if (replaceData)
 1150                {
 11151                    target.ProviderIds[key] = id.Value;
 1152                }
 1153                else
 1154                {
 31155                    target.ProviderIds.TryAdd(key, id.Value);
 1156                }
 1157            }
 1158
 1291159            if (replaceData || !target.CriticRating.HasValue)
 1160            {
 1281161                target.CriticRating = source.CriticRating;
 1162            }
 1163
 1291164            if (replaceData || target.RemoteTrailers.Count == 0)
 1165            {
 1281166                target.RemoteTrailers = source.RemoteTrailers;
 1167            }
 1168            else
 1169            {
 11170                target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).DistinctBy(t => t.Url).ToArr
 1171            }
 1172
 1291173            MergeAlbumArtist(source, target, replaceData);
 1291174            MergeVideoInfo(source, target, replaceData);
 1291175            MergeDisplayOrder(source, target, replaceData);
 1176
 1291177            if (replaceData || string.IsNullOrEmpty(target.ForcedSortName))
 1178            {
 1281179                var forcedSortName = source.ForcedSortName;
 1281180                if (!string.IsNullOrEmpty(forcedSortName))
 1181                {
 31182                    target.ForcedSortName = forcedSortName;
 1183                }
 1184            }
 1185
 1291186            if (mergeMetadataSettings)
 1187            {
 21188                if (replaceData || !target.IsLocked)
 1189                {
 21190                    target.IsLocked = target.IsLocked || source.IsLocked;
 1191                }
 1192
 21193                if (target.LockedFields.Length == 0)
 1194                {
 01195                    target.LockedFields = source.LockedFields;
 1196                }
 1197                else
 1198                {
 21199                    target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray();
 1200                }
 1201
 21202                if (source.DateCreated != DateTime.MinValue)
 1203                {
 11204                    target.DateCreated = source.DateCreated;
 1205                }
 1206
 21207                if (replaceData || source.DateModified != DateTime.MinValue)
 1208                {
 21209                    target.DateModified = source.DateModified;
 1210                }
 1211
 21212                if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode))
 1213                {
 21214                    target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
 1215                }
 1216
 21217                if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataLanguage))
 1218                {
 21219                    target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
 1220                }
 1221            }
 1291222        }
 1223
 1224        private static void MergePeople(IReadOnlyList<PersonInfo> source, IReadOnlyList<PersonInfo> target)
 1225        {
 41226            var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
 41227            var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
 1228
 161229            foreach (var name in targetByName.Select(g => g.Key))
 1230            {
 41231                var targetPeople = targetByName[name].ToArray();
 41232                var sourcePeople = sourceByName[name].ToArray();
 1233
 41234                if (sourcePeople.Length == 0)
 1235                {
 1236                    continue;
 1237                }
 1238
 121239                for (int i = 0; i < targetPeople.Length; i++)
 1240                {
 31241                    var person = targetPeople[i];
 31242                    var personInSource = i < sourcePeople.Length ? sourcePeople[i] : sourcePeople[0];
 1243
 101244                    foreach (var providerId in personInSource.ProviderIds)
 1245                    {
 21246                        person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
 1247                    }
 1248
 31249                    if (string.IsNullOrWhiteSpace(person.ImageUrl))
 1250                    {
 21251                        person.ImageUrl = personInSource.ImageUrl;
 1252                    }
 1253
 31254                    if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role))
 1255                    {
 01256                        person.Role = personInSource.Role;
 1257                    }
 1258
 31259                    if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue)
 1260                    {
 01261                        person.SortOrder = personInSource.SortOrder;
 1262                    }
 1263                }
 1264            }
 41265        }
 1266
 1267        private static void MergeDisplayOrder(BaseItem source, BaseItem target, bool replaceData)
 1268        {
 1291269            if (source is IHasDisplayOrder sourceHasDisplayOrder
 1291270                && target is IHasDisplayOrder targetHasDisplayOrder)
 1271            {
 491272                if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder))
 1273                {
 481274                    var displayOrder = sourceHasDisplayOrder.DisplayOrder;
 481275                    if (!string.IsNullOrWhiteSpace(displayOrder))
 1276                    {
 31277                        targetHasDisplayOrder.DisplayOrder = displayOrder;
 1278                    }
 1279                }
 1280            }
 1291281        }
 1282
 1283        private static void MergeAlbumArtist(BaseItem source, BaseItem target, bool replaceData)
 1284        {
 1291285            if (source is IHasAlbumArtist sourceHasAlbumArtist
 1291286                && target is IHasAlbumArtist targetHasAlbumArtist)
 1287            {
 281288                if (replaceData || targetHasAlbumArtist.AlbumArtists.Count == 0)
 1289                {
 271290                    targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
 1291                }
 11292                else if (sourceHasAlbumArtist.AlbumArtists.Count > 0)
 1293                {
 11294                    targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.Al
 1295                }
 1296            }
 1021297        }
 1298
 1299        private static void MergeVideoInfo(BaseItem source, BaseItem target, bool replaceData)
 1300        {
 1291301            if (source is Video sourceCast && target is Video targetCast)
 1302            {
 521303                if (sourceCast.Video3DFormat.HasValue && (replaceData || !targetCast.Video3DFormat.HasValue))
 1304                {
 21305                    targetCast.Video3DFormat = sourceCast.Video3DFormat;
 1306                }
 1307            }
 1291308        }
 1309    }
 1310}

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)
ApplySearchResult(MediaBrowser.Controller.Providers.ItemLookupInfo,MediaBrowser.Model.Providers.RemoteSearchResult)
AfterMetadataRefresh(TItemType,MediaBrowser.Controller.Providers.MetadataRefreshOptions,System.Threading.CancellationToken)
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.ImageRefreshOptions)
CanRefresh(MediaBrowser.Controller.Entities.BaseItem)
CanRefreshPrimary(System.Type)
CreateNew()
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)
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)