< 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: 227
Uncovered lines: 116
Coverable lines: 343
Total lines: 1294
Line coverage: 66.1%
Branch coverage
66%
Covered branches: 233
Total branches: 352
Branch coverage: 66.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 7/26/2025 - 12:11:20 AM Line coverage: 68.1% (235/345) Branch coverage: 67.1% (235/350) Total lines: 12917/29/2025 - 12:11:08 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: 1294 7/26/2025 - 12:11:20 AM Line coverage: 68.1% (235/345) Branch coverage: 67.1% (235/350) Total lines: 12917/29/2025 - 12:11:08 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: 1294

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
 3466        protected virtual bool EnableUpdatingPremiereDateFromChildren => false;
 67
 3468        protected virtual bool EnableUpdatingGenresFromChildren => false;
 69
 3470        protected virtual bool EnableUpdatingStudiosFromChildren => false;
 71
 3472        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            {
 6880                return directoryService.GetFileSystemEntry(path);
 81            }
 082            catch (Exception ex)
 83            {
 084                Logger.LogError(ex, "Error getting file {Path}", path);
 085                return null;
 86            }
 6887        }
 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            updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataR
 155
 156            // Next run metadata providers
 157            if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
 158            {
 159                var providers = GetProviders(item, libraryOptions, refreshOptions, isFirstRefresh, requiresRefresh)
 160                    .ToList();
 161
 162                if (providers.Count > 0 || isFirstRefresh || requiresRefresh)
 163                {
 164                    if (item.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata))
 165                    {
 166                        updateType |= ItemUpdateType.MetadataImport;
 167                    }
 168                }
 169
 170                if (providers.Count > 0)
 171                {
 172                    var id = itemOfType.GetLookupInfo();
 173
 174                    if (refreshOptions.SearchResult is not null)
 175                    {
 176                        ApplySearchResult(id, refreshOptions.SearchResult);
 177                    }
 178
 179                    id.IsAutomated = refreshOptions.IsAutomated;
 180
 181                    var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any();
 182                    var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider
 183
 184                    updateType |= result.UpdateType;
 185                    if (result.Failures > 0)
 186                    {
 187                        hasRefreshedMetadata = false;
 188                    }
 189                }
 190            }
 191
 192            // Next run remote image providers, but only if local image providers didn't throw an exception
 193            if (!localImagesFailed && refreshOptions.ImageRefreshMode > MetadataRefreshMode.ValidationOnly)
 194            {
 195                var providers = GetNonLocalImageProviders(item, allImageProviders, refreshOptions).ToList();
 196
 197                if (providers.Count > 0)
 198                {
 199                    var result = await ImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions
 200
 201                    updateType |= result.UpdateType;
 202                    if (result.Failures > 0)
 203                    {
 204                        hasRefreshedImages = false;
 205                    }
 206                }
 207            }
 208
 209            if (hasRefreshedMetadata && hasRefreshedImages)
 210            {
 211                item.DateLastRefreshed = DateTime.UtcNow;
 212                updateType |= item.OnMetadataChanged();
 213            }
 214
 215            updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataR
 216
 217            await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false);
 218
 219            return updateType;
 220
 221            async Task<ItemUpdateType> SaveInternal(BaseItem item, MetadataRefreshOptions refreshOptions, ItemUpdateType
 222            {
 223                // Save if changes were made, or it's never been saved before
 224                if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.Rep
 225                {
 226                    if (item.IsFileProtocol)
 227                    {
 228                        var file = TryGetFileSystemMetadata(item.Path, refreshOptions.DirectoryService);
 229                        if (file is not null)
 230                        {
 231                            item.DateModified = file.LastWriteTimeUtc;
 232
 233                            if (!file.IsDirectory)
 234                            {
 235                                item.Size = file.Length;
 236                            }
 237                        }
 238                    }
 239
 240                    // If any of these properties are set then make sure the updateType is not None, just to force every
 241                    if (refreshOptions.ForceSave || refreshOptions.ReplaceAllMetadata)
 242                    {
 243                        updateType |= ItemUpdateType.MetadataDownload;
 244                    }
 245
 246                    // Save to database
 247                    await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
 248                }
 249
 250                return updateType;
 251            }
 252        }
 253
 254        private void ApplySearchResult(ItemLookupInfo lookupInfo, RemoteSearchResult result)
 255        {
 256            // Episode and Season do not support Identify, so the search results are the Series'
 257            switch (lookupInfo)
 258            {
 259                case EpisodeInfo episodeInfo:
 0260                    episodeInfo.SeriesProviderIds = result.ProviderIds;
 0261                    episodeInfo.ProviderIds.Clear();
 0262                    break;
 263                case SeasonInfo seasonInfo:
 0264                    seasonInfo.SeriesProviderIds = result.ProviderIds;
 0265                    seasonInfo.ProviderIds.Clear();
 0266                    break;
 267                default:
 0268                    lookupInfo.ProviderIds = result.ProviderIds;
 0269                    lookupInfo.Name = result.Name;
 0270                    lookupInfo.Year = result.ProductionYear;
 271                    break;
 272            }
 0273        }
 274
 275        protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken ca
 276        {
 277            await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
 278            if (result.Item.SupportsPeople && result.People is not null)
 279            {
 280                var baseItem = result.Item;
 281
 282                await LibraryManager.UpdatePeopleAsync(baseItem, result.People, cancellationToken).ConfigureAwait(false)
 283            }
 284        }
 285
 286        protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationT
 287        {
 52288            item.AfterMetadataRefresh();
 52289            return Task.CompletedTask;
 290        }
 291
 292        /// <summary>
 293        /// Before the save.
 294        /// </summary>
 295        /// <param name="item">The item.</param>
 296        /// <param name="isFullRefresh">if set to <c>true</c> [is full refresh].</param>
 297        /// <param name="currentUpdateType">Type of the current update.</param>
 298        /// <returns>ItemUpdateType.</returns>
 299        private async Task<ItemUpdateType> BeforeSave(TItemType item, bool isFullRefresh, ItemUpdateType currentUpdateTy
 300        {
 301            var updateType = BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
 302
 303            updateType |= item.OnMetadataChanged();
 304
 305            if (updateType == ItemUpdateType.None)
 306            {
 307                if (!await ItemRepository.ItemExistsAsync(item.Id).ConfigureAwait(false))
 308                {
 309                    return ItemUpdateType.MetadataImport;
 310                }
 311            }
 312
 313            return updateType;
 314        }
 315
 316        protected virtual ItemUpdateType BeforeSaveInternal(TItemType item, bool isFullRefresh, ItemUpdateType updateTyp
 317        {
 52318            if (EnableUpdateMetadataFromChildren(item, isFullRefresh, updateType))
 319            {
 0320                if (isFullRefresh || updateType > ItemUpdateType.None)
 321                {
 0322                    var children = GetChildrenForMetadataUpdates(item);
 323
 0324                    updateType = UpdateMetadataFromChildren(item, children, isFullRefresh, updateType);
 325                }
 326            }
 327
 52328            var presentationUniqueKey = item.CreatePresentationUniqueKey();
 52329            if (!string.Equals(item.PresentationUniqueKey, presentationUniqueKey, StringComparison.Ordinal))
 330            {
 33331                item.PresentationUniqueKey = presentationUniqueKey;
 33332                updateType |= ItemUpdateType.MetadataImport;
 333            }
 334
 335            // Cleanup extracted files if source file was modified
 52336            var itemPath = item.Path;
 52337            if (!string.IsNullOrEmpty(itemPath))
 338            {
 52339                var info = FileSystem.GetFileSystemInfo(itemPath);
 52340                if (info.Exists && item.HasChanged(info.LastWriteTimeUtc))
 341                {
 0342                    Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", item.DateModified, in
 343
 0344                    item.DateModified = info.LastWriteTimeUtc;
 0345                    if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded)
 346                    {
 0347                        item.DateCreated = info.CreationTimeUtc;
 348                    }
 349
 0350                    if (item is Video video)
 351                    {
 0352                        Logger.LogInformation("File changed, pruning extracted data: {Path}", item.Path);
 0353                        ExternalDataManager.DeleteExternalItemDataAsync(video, CancellationToken.None).GetAwaiter().GetR
 354                    }
 355
 0356                    updateType |= ItemUpdateType.MetadataImport;
 357                }
 358            }
 359
 52360            return updateType;
 361        }
 362
 363        protected virtual bool EnableUpdateMetadataFromChildren(TItemType item, bool isFullRefresh, ItemUpdateType curre
 364        {
 52365            if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
 366            {
 34367                if (EnableUpdatingPremiereDateFromChildren || EnableUpdatingGenresFromChildren || EnableUpdatingStudiosF
 368                {
 0369                    return true;
 370                }
 371
 34372                if (item is Folder folder)
 373                {
 34374                    return folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks;
 375                }
 376            }
 377
 18378            return false;
 379        }
 380
 381        protected virtual IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(TItemType item)
 382        {
 0383            if (item is Folder folder)
 384            {
 0385                return folder.GetRecursiveChildren();
 386            }
 387
 0388            return [];
 389        }
 390
 391        protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IReadOnlyList<BaseItem> children, bo
 392        {
 0393            var updateType = ItemUpdateType.None;
 394
 0395            if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
 396            {
 0397                updateType |= UpdateCumulativeRunTimeTicks(item, children);
 0398                updateType |= UpdateDateLastMediaAdded(item, children);
 399
 400                // don't update user-changeable metadata for locked items
 0401                if (item.IsLocked)
 402                {
 0403                    return updateType;
 404                }
 405
 0406                if (EnableUpdatingPremiereDateFromChildren)
 407                {
 0408                    updateType |= UpdatePremiereDate(item, children);
 409                }
 410
 0411                if (EnableUpdatingGenresFromChildren)
 412                {
 0413                    updateType |= UpdateGenres(item, children);
 414                }
 415
 0416                if (EnableUpdatingStudiosFromChildren)
 417                {
 0418                    updateType |= UpdateStudios(item, children);
 419                }
 420
 0421                if (EnableUpdatingOfficialRatingFromChildren)
 422                {
 0423                    updateType |= UpdateOfficialRating(item, children);
 424                }
 425            }
 426
 0427            return updateType;
 428        }
 429
 430        private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IReadOnlyList<BaseItem> children)
 431        {
 0432            if (item is Folder folder && folder.SupportsCumulativeRunTimeTicks)
 433            {
 0434                long ticks = 0;
 435
 0436                foreach (var child in children)
 437                {
 0438                    if (!child.IsFolder)
 439                    {
 0440                        ticks += child.RunTimeTicks ?? 0;
 441                    }
 442                }
 443
 0444                if (!folder.RunTimeTicks.HasValue || folder.RunTimeTicks.Value != ticks)
 445                {
 0446                    folder.RunTimeTicks = ticks;
 0447                    return ItemUpdateType.MetadataImport;
 448                }
 449            }
 450
 0451            return ItemUpdateType.None;
 452        }
 453
 454        private ItemUpdateType UpdateDateLastMediaAdded(TItemType item, IReadOnlyList<BaseItem> children)
 455        {
 0456            var updateType = ItemUpdateType.None;
 457
 0458            if (item is Folder folder && folder.SupportsDateLastMediaAdded)
 459            {
 0460                var dateLastMediaAdded = DateTime.MinValue;
 0461                var any = false;
 462
 0463                foreach (var child in children)
 464                {
 465                    // Exclude any folders and virtual items since they are only placeholders
 0466                    if (!child.IsFolder && !child.IsVirtualItem)
 467                    {
 0468                        var childDateCreated = child.DateCreated;
 0469                        if (childDateCreated > dateLastMediaAdded)
 470                        {
 0471                            dateLastMediaAdded = childDateCreated;
 472                        }
 473
 0474                        any = true;
 475                    }
 476                }
 477
 0478                if ((!folder.DateLastMediaAdded.HasValue && any) || folder.DateLastMediaAdded != dateLastMediaAdded)
 479                {
 0480                    folder.DateLastMediaAdded = dateLastMediaAdded;
 0481                    updateType = ItemUpdateType.MetadataImport;
 482                }
 483            }
 484
 0485            return updateType;
 486        }
 487
 488        private ItemUpdateType UpdatePremiereDate(TItemType item, IReadOnlyList<BaseItem> children)
 489        {
 0490            var updateType = ItemUpdateType.None;
 491
 0492            if (children.Count == 0)
 493            {
 0494                return updateType;
 495            }
 496
 0497            var date = children.Select(i => i.PremiereDate ?? DateTime.MaxValue).Min();
 498
 0499            var originalPremiereDate = item.PremiereDate;
 0500            var originalProductionYear = item.ProductionYear;
 501
 0502            if (date > DateTime.MinValue && date < DateTime.MaxValue)
 503            {
 0504                item.PremiereDate = date;
 0505                item.ProductionYear = date.Year;
 506            }
 507            else
 508            {
 0509                var year = children.Select(i => i.ProductionYear ?? 0).Min();
 510
 0511                if (year > 0)
 512                {
 0513                    item.ProductionYear = year;
 514                }
 515            }
 516
 0517            if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue)
 0518                || (originalProductionYear ?? -1) != (item.ProductionYear ?? -1))
 519            {
 0520                updateType |= ItemUpdateType.MetadataEdit;
 521            }
 522
 0523            return updateType;
 524        }
 525
 526        private ItemUpdateType UpdateGenres(TItemType item, IReadOnlyList<BaseItem> children)
 527        {
 0528            var updateType = ItemUpdateType.None;
 529
 0530            if (!item.LockedFields.Contains(MetadataField.Genres))
 531            {
 0532                var currentList = item.Genres;
 533
 0534                item.Genres = children.SelectMany(i => i.Genres)
 0535                    .Distinct(StringComparer.OrdinalIgnoreCase)
 0536                    .ToArray();
 537
 0538                if (currentList.Length != item.Genres.Length || !currentList.Order().SequenceEqual(item.Genres.Order(), 
 539                {
 0540                    updateType |= ItemUpdateType.MetadataEdit;
 541                }
 542            }
 543
 0544            return updateType;
 545        }
 546
 547        private ItemUpdateType UpdateStudios(TItemType item, IReadOnlyList<BaseItem> children)
 548        {
 0549            var updateType = ItemUpdateType.None;
 550
 0551            if (!item.LockedFields.Contains(MetadataField.Studios))
 552            {
 0553                var currentList = item.Studios;
 554
 0555                item.Studios = children.SelectMany(i => i.Studios)
 0556                    .Distinct(StringComparer.OrdinalIgnoreCase)
 0557                    .ToArray();
 558
 0559                if (currentList.Length != item.Studios.Length || !currentList.Order().SequenceEqual(item.Studios.Order()
 560                {
 0561                    updateType |= ItemUpdateType.MetadataEdit;
 562                }
 563            }
 564
 0565            return updateType;
 566        }
 567
 568        private ItemUpdateType UpdateOfficialRating(TItemType item, IReadOnlyList<BaseItem> children)
 569        {
 0570            var updateType = ItemUpdateType.None;
 571
 0572            if (!item.LockedFields.Contains(MetadataField.OfficialRating))
 573            {
 0574                if (item.UpdateRatingToItems(children))
 575                {
 0576                    updateType |= ItemUpdateType.MetadataEdit;
 577                }
 578            }
 579
 0580            return updateType;
 581        }
 582
 583        /// <summary>
 584        /// Gets the providers.
 585        /// </summary>
 586        /// <param name="item">A media item.</param>
 587        /// <param name="libraryOptions">The LibraryOptions to use.</param>
 588        /// <param name="options">The MetadataRefreshOptions to use.</param>
 589        /// <param name="isFirstRefresh">Specifies first refresh mode.</param>
 590        /// <param name="requiresRefresh">Specifies refresh mode.</param>
 591        /// <returns>IEnumerable{`0}.</returns>
 592        protected IEnumerable<IMetadataProvider> GetProviders(BaseItem item, LibraryOptions libraryOptions, MetadataRefr
 593        {
 594            // Get providers to refresh
 52595            var providers = ProviderManager.GetMetadataProviders<TItemType>(item, libraryOptions).ToList();
 596
 52597            var metadataRefreshMode = options.MetadataRefreshMode;
 598
 599            // Run all if either of these flags are true
 52600            var runAllProviders = options.ReplaceAllMetadata ||
 52601                metadataRefreshMode == MetadataRefreshMode.FullRefresh ||
 52602                (isFirstRefresh && metadataRefreshMode >= MetadataRefreshMode.Default) ||
 52603                (requiresRefresh && metadataRefreshMode >= MetadataRefreshMode.Default);
 604
 52605            if (!runAllProviders)
 606            {
 18607                var providersWithChanges = providers
 18608                    .Where(i =>
 18609                    {
 18610                        if (i is IHasItemChangeMonitor hasFileChangeMonitor)
 18611                        {
 18612                            return HasChanged(item, hasFileChangeMonitor, options.DirectoryService);
 18613                        }
 18614
 18615                        return false;
 18616                    })
 18617                    .ToList();
 618
 18619                if (providersWithChanges.Count == 0)
 620                {
 0621                    providers = new List<IMetadataProvider<TItemType>>();
 622                }
 623                else
 624                {
 18625                    var anyRemoteProvidersChanged = providersWithChanges.OfType<IRemoteMetadataProvider>()
 18626                        .Any();
 627
 18628                    var anyLocalProvidersChanged = providersWithChanges.OfType<ILocalMetadataProvider>()
 18629                        .Any();
 630
 18631                    var anyLocalPreRefreshProvidersChanged = providersWithChanges.OfType<IPreRefreshProvider>()
 18632                        .Any();
 633
 18634                    providers = providers.Where(i =>
 18635                    {
 18636                        // If any provider reports a change, always run local ones as well
 18637                        if (i is ILocalMetadataProvider)
 18638                        {
 18639                            return anyRemoteProvidersChanged || anyLocalProvidersChanged || anyLocalPreRefreshProvidersC
 18640                        }
 18641
 18642                        // If any remote providers changed, run them all so that priorities can be honored
 18643                        if (i is IRemoteMetadataProvider)
 18644                        {
 18645                            if (options.MetadataRefreshMode == MetadataRefreshMode.ValidationOnly)
 18646                            {
 18647                                return false;
 18648                            }
 18649
 18650                            return anyRemoteProvidersChanged;
 18651                        }
 18652
 18653                        // Run custom refresh providers if they report a change or any remote providers change
 18654                        return anyRemoteProvidersChanged || providersWithChanges.Contains(i);
 18655                    }).ToList();
 656                }
 657            }
 658
 52659            return providers;
 660        }
 661
 662        protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvide
 663        {
 664            // Get providers to refresh
 52665            var providers = allImageProviders.Where(i => i is not ILocalImageProvider);
 666
 52667            var dateLastImageRefresh = item.DateLastRefreshed;
 668
 669            // Run all if either of these flags are true
 52670            var runAllProviders = options.ImageRefreshMode == MetadataRefreshMode.FullRefresh || dateLastImageRefresh.Da
 671
 52672            if (!runAllProviders)
 673            {
 19674                providers = providers
 19675                    .Where(i =>
 19676                    {
 19677                        if (i is IHasItemChangeMonitor hasFileChangeMonitor)
 19678                        {
 19679                            return HasChanged(item, hasFileChangeMonitor, options.DirectoryService);
 19680                        }
 19681
 19682                        return false;
 19683                    });
 684            }
 685
 52686            return providers;
 687        }
 688
 689        public bool CanRefresh(BaseItem item)
 690        {
 1092691            return item is TItemType;
 692        }
 693
 694        public bool CanRefreshPrimary(Type type)
 695        {
 1301696            return type == typeof(TItemType);
 697        }
 698
 699        protected virtual async Task<RefreshResult> RefreshWithProviders(
 700            MetadataResult<TItemType> metadata,
 701            TIdType id,
 702            MetadataRefreshOptions options,
 703            ICollection<IMetadataProvider> providers,
 704            ItemImageProvider imageService,
 705            bool isSavingMetadata,
 706            CancellationToken cancellationToken)
 707        {
 708            var refreshResult = new RefreshResult
 709            {
 710                UpdateType = ItemUpdateType.None
 711            };
 712
 713            var item = metadata.Item;
 714
 715            var customProviders = providers.OfType<ICustomMetadataProvider<TItemType>>().ToList();
 716            var logName = !item.IsFileProtocol ? item.Name ?? item.Path : item.Path ?? item.Name;
 717
 718            foreach (var provider in customProviders.Where(i => i is IPreRefreshProvider))
 719            {
 720                await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwa
 721            }
 722
 723            if (item.IsLocked)
 724            {
 725                return refreshResult;
 726            }
 727
 728            var temp = new MetadataResult<TItemType>
 729            {
 730                Item = CreateNew()
 731            };
 732            temp.Item.Path = item.Path;
 733            temp.Item.Id = item.Id;
 734            temp.Item.ParentIndexNumber = item.ParentIndexNumber;
 735            temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
 736            temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
 737
 738            var foundImageTypes = new List<ImageType>();
 739
 740            // Do not execute local providers if we are identifying or replacing with local metadata saving enabled
 741            if (options.SearchResult is null && !(isSavingMetadata && options.ReplaceAllMetadata))
 742            {
 743                foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
 744                {
 745                    var providerName = provider.GetType().Name;
 746                    Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
 747
 748                    var itemInfo = new ItemInfo(item);
 749
 750                    try
 751                    {
 752                        var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken
 753
 754                        if (localItem.HasMetadata)
 755                        {
 756                            foreach (var remoteImage in localItem.RemoteImages)
 757                            {
 758                                try
 759                                {
 760                                    if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
 761                                        && !options.IsReplacingImage(remoteImage.Type))
 762                                    {
 763                                        continue;
 764                                    }
 765
 766                                    await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cance
 767                                    refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
 768
 769                                    // remember imagetype that has just been downloaded
 770                                    foundImageTypes.Add(remoteImage.Type);
 771                                }
 772                                catch (HttpRequestException ex)
 773                                {
 774                                    Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteIm
 775                                }
 776                            }
 777
 778                            if (foundImageTypes.Count > 0)
 779                            {
 780                                imageService.UpdateReplaceImages(options, foundImageTypes);
 781                            }
 782
 783                            if (imageService.MergeImages(item, localItem.Images, options))
 784                            {
 785                                refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
 786                            }
 787
 788                            MergeData(localItem, temp, [], false, true);
 789                            refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
 790
 791                            break;
 792                        }
 793
 794                        Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
 795                    }
 796                    catch (OperationCanceledException)
 797                    {
 798                        throw;
 799                    }
 800                    catch (Exception ex)
 801                    {
 802                        Logger.LogError(ex, "Error in {Provider}", provider.Name);
 803
 804                        // If a local provider fails, consider that a failure
 805                        refreshResult.ErrorMessage = ex.Message;
 806                    }
 807                }
 808            }
 809
 810            var isLocalLocked = temp.Item.IsLocked;
 811            if (!isLocalLocked && (options.ReplaceAllMetadata || options.MetadataRefreshMode > MetadataRefreshMode.Valid
 812            {
 813                var remoteResult = await ExecuteRemoteProviders(temp, logName, false, id, providers.OfType<IRemoteMetada
 814                    .ConfigureAwait(false);
 815
 816                refreshResult.UpdateType |= remoteResult.UpdateType;
 817                refreshResult.ErrorMessage = remoteResult.ErrorMessage;
 818                refreshResult.Failures += remoteResult.Failures;
 819            }
 820
 821            if (providers.Any(i => i is not ICustomMetadataProvider))
 822            {
 823                if (refreshResult.UpdateType > ItemUpdateType.None)
 824                {
 825                    if (!options.RemoveOldMetadata)
 826                    {
 827                        // Add existing metadata to provider result if it does not exist there
 828                        MergeData(metadata, temp, [], false, false);
 829                    }
 830
 831                    if (isLocalLocked)
 832                    {
 833                        MergeData(temp, metadata, item.LockedFields, true, true);
 834                    }
 835                    else
 836                    {
 837                        var shouldReplace = (options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly && options
 838                            // Case for Scan for new and updated files
 839                            || (options.MetadataRefreshMode == MetadataRefreshMode.Default && !options.ReplaceAllMetadat
 840                        MergeData(temp, metadata, item.LockedFields, shouldReplace, true);
 841                    }
 842                }
 843            }
 844
 845            foreach (var provider in customProviders.Where(i => i is not IPreRefreshProvider))
 846            {
 847                await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwa
 848            }
 849
 850            return refreshResult;
 851        }
 852
 853        private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, string logName
 854        {
 855            Logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, logName);
 856
 857            try
 858            {
 859                refreshResult.UpdateType |= await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(f
 860            }
 861            catch (OperationCanceledException)
 862            {
 863                throw;
 864            }
 865            catch (Exception ex)
 866            {
 867                refreshResult.ErrorMessage = ex.Message;
 868                Logger.LogError(ex, "Error in {Provider}", provider.Name);
 869            }
 870        }
 871
 872        protected virtual TItemType CreateNew()
 873        {
 52874            return new TItemType();
 875        }
 876
 877        private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, bool re
 878        {
 879            var refreshResult = new RefreshResult();
 880
 881            if (id is not null)
 882            {
 883                MergeNewData(temp.Item, id);
 884            }
 885
 886            foreach (var provider in providers)
 887            {
 888                var providerName = provider.GetType().Name;
 889                Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
 890
 891                try
 892                {
 893                    var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
 894
 895                    if (result.HasMetadata)
 896                    {
 897                        result.Provider = provider.Name;
 898
 899                        MergeData(result, temp, [], replaceData, false);
 900                        MergeNewData(temp.Item, id);
 901
 902                        refreshResult.UpdateType |= ItemUpdateType.MetadataDownload;
 903                    }
 904                    else
 905                    {
 906                        Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
 907                    }
 908                }
 909                catch (OperationCanceledException)
 910                {
 911                    throw;
 912                }
 913                catch (Exception ex)
 914                {
 915                    refreshResult.Failures++;
 916                    refreshResult.ErrorMessage = ex.Message;
 917                    Logger.LogError(ex, "Error in {Provider}", provider.Name);
 918                }
 919            }
 920
 921            return refreshResult;
 922        }
 923
 924        private void MergeNewData(TItemType source, TIdType lookupInfo)
 925        {
 926            // Copy new provider id's that may have been obtained
 104927            foreach (var providerId in source.ProviderIds)
 928            {
 0929                var key = providerId.Key;
 930
 931                // Don't replace existing Id's.
 0932                lookupInfo.ProviderIds.TryAdd(key, providerId.Value);
 933            }
 52934        }
 935
 936        private bool HasChanged(BaseItem item, IHasItemChangeMonitor changeMonitor, IDirectoryService directoryService)
 937        {
 938            try
 939            {
 18940                var hasChanged = changeMonitor.HasChanged(item, directoryService);
 941
 18942                if (hasChanged)
 943                {
 18944                    Logger.LogDebug("{Monitor} reports change to {Item}", changeMonitor.GetType().Name, item.Path ?? ite
 945                }
 946
 18947                return hasChanged;
 948            }
 0949            catch (Exception ex)
 950            {
 0951                Logger.LogError(ex, "Error in {Monitor}.HasChanged", changeMonitor.GetType().Name);
 0952                return false;
 953            }
 18954        }
 955
 956        /// <summary>
 957        /// Merges metadata from source into target.
 958        /// </summary>
 959        /// <param name="source">The source for new metadata.</param>
 960        /// <param name="target">The target to insert new metadata into.</param>
 961        /// <param name="lockedFields">The fields that are locked and should not be updated.</param>
 962        /// <param name="replaceData"><c>true</c> if existing data should be replaced.</param>
 963        /// <param name="mergeMetadataSettings"><c>true</c> if the metadata settings in target should be updated to matc
 964        /// <exception cref="ArgumentException">Thrown if source or target are null.</exception>
 965        protected virtual void MergeData(
 966            MetadataResult<TItemType> source,
 967            MetadataResult<TItemType> target,
 968            MetadataField[] lockedFields,
 969            bool replaceData,
 970            bool mergeMetadataSettings)
 971        {
 0972            MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 0973        }
 974
 975        internal static void MergeBaseItemData(
 976            MetadataResult<TItemType> sourceResult,
 977            MetadataResult<TItemType> targetResult,
 978            MetadataField[] lockedFields,
 979            bool replaceData,
 980            bool mergeMetadataSettings)
 981        {
 129982            var source = sourceResult.Item;
 129983            var target = targetResult.Item;
 984
 129985            ArgumentNullException.ThrowIfNull(sourceResult);
 129986            ArgumentNullException.ThrowIfNull(targetResult);
 987
 129988            if (!lockedFields.Contains(MetadataField.Name))
 989            {
 126990                if (replaceData || string.IsNullOrEmpty(target.Name))
 991                {
 992                    // Safeguard against incoming data having an empty name
 125993                    if (!string.IsNullOrWhiteSpace(source.Name))
 994                    {
 3995                        target.Name = source.Name;
 996                    }
 997                }
 998            }
 999
 1291000            if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
 1001            {
 1281002                target.OriginalTitle = source.OriginalTitle;
 1003            }
 1004
 1291005            if (replaceData || !target.CommunityRating.HasValue)
 1006            {
 1281007                target.CommunityRating = source.CommunityRating;
 1008            }
 1009
 1291010            if (replaceData || !target.EndDate.HasValue)
 1011            {
 1281012                target.EndDate = source.EndDate;
 1013            }
 1014
 1291015            if (!lockedFields.Contains(MetadataField.Genres))
 1016            {
 1271017                if (replaceData || target.Genres.Length == 0)
 1018                {
 1261019                    target.Genres = source.Genres;
 1020                }
 1021            }
 1022
 1291023            if (replaceData || !target.IndexNumber.HasValue)
 1024            {
 1281025                target.IndexNumber = source.IndexNumber;
 1026            }
 1027
 1291028            if (!lockedFields.Contains(MetadataField.OfficialRating))
 1029            {
 1261030                if (replaceData || string.IsNullOrEmpty(target.OfficialRating))
 1031                {
 1251032                    target.OfficialRating = source.OfficialRating;
 1033                }
 1034            }
 1035
 1291036            if (replaceData || string.IsNullOrEmpty(target.CustomRating))
 1037            {
 1281038                target.CustomRating = source.CustomRating;
 1039            }
 1040
 1291041            if (replaceData || string.IsNullOrEmpty(target.Tagline))
 1042            {
 1281043                target.Tagline = source.Tagline;
 1044            }
 1045
 1291046            if (!lockedFields.Contains(MetadataField.Overview))
 1047            {
 1261048                if (replaceData || string.IsNullOrEmpty(target.Overview))
 1049                {
 1251050                    target.Overview = source.Overview;
 1051                }
 1052            }
 1053
 1291054            if (replaceData || !target.ParentIndexNumber.HasValue)
 1055            {
 1281056                target.ParentIndexNumber = source.ParentIndexNumber;
 1057            }
 1058
 1291059            if (!lockedFields.Contains(MetadataField.Cast))
 1060            {
 1281061                if (replaceData || targetResult.People is null || targetResult.People.Count == 0)
 1062                {
 1241063                    targetResult.People = sourceResult.People;
 1064                }
 41065                else if (sourceResult.People is not null && sourceResult.People.Count > 0)
 1066                {
 41067                    MergePeople(sourceResult.People, targetResult.People);
 1068                }
 1069            }
 1070
 1291071            if (replaceData || !target.PremiereDate.HasValue)
 1072            {
 1281073                target.PremiereDate = source.PremiereDate;
 1074            }
 1075
 1291076            if (replaceData || !target.ProductionYear.HasValue)
 1077            {
 1281078                target.ProductionYear = source.ProductionYear;
 1079            }
 1080
 1291081            if (!lockedFields.Contains(MetadataField.Runtime))
 1082            {
 1291083                if (replaceData || !target.RunTimeTicks.HasValue)
 1084                {
 1291085                    if (target is not Audio && target is not Video)
 1086                    {
 491087                        target.RunTimeTicks = source.RunTimeTicks;
 1088                    }
 1089                }
 1090            }
 1091
 1291092            if (!lockedFields.Contains(MetadataField.Studios))
 1093            {
 1271094                if (replaceData || target.Studios.Length == 0)
 1095                {
 1261096                    target.Studios = source.Studios;
 1097                }
 1098                else
 1099                {
 11100                    target.Studios = target.Studios.Concat(source.Studios).Distinct(StringComparer.OrdinalIgnoreCase).To
 1101                }
 1102            }
 1103
 1291104            if (!lockedFields.Contains(MetadataField.Tags))
 1105            {
 1271106                if (replaceData || target.Tags.Length == 0)
 1107                {
 1261108                    target.Tags = source.Tags;
 1109                }
 1110                else
 1111                {
 11112                    target.Tags = target.Tags.Concat(source.Tags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 1113                }
 1114            }
 1115
 1291116            if (!lockedFields.Contains(MetadataField.ProductionLocations))
 1117            {
 1271118                if (replaceData || target.ProductionLocations.Length == 0)
 1119                {
 1261120                    target.ProductionLocations = source.ProductionLocations;
 1121                }
 1122                else
 1123                {
 11124                    target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct(
 1125                }
 1126            }
 1127
 2661128            foreach (var id in source.ProviderIds)
 1129            {
 41130                var key = id.Key;
 1131
 1132                // Don't replace existing Id's.
 41133                if (replaceData)
 1134                {
 11135                    target.ProviderIds[key] = id.Value;
 1136                }
 1137                else
 1138                {
 31139                    target.ProviderIds.TryAdd(key, id.Value);
 1140                }
 1141            }
 1142
 1291143            if (replaceData || !target.CriticRating.HasValue)
 1144            {
 1281145                target.CriticRating = source.CriticRating;
 1146            }
 1147
 1291148            if (replaceData || target.RemoteTrailers.Count == 0)
 1149            {
 1281150                target.RemoteTrailers = source.RemoteTrailers;
 1151            }
 1152            else
 1153            {
 11154                target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).DistinctBy(t => t.Url).ToArr
 1155            }
 1156
 1291157            MergeAlbumArtist(source, target, replaceData);
 1291158            MergeVideoInfo(source, target, replaceData);
 1291159            MergeDisplayOrder(source, target, replaceData);
 1160
 1291161            if (replaceData || string.IsNullOrEmpty(target.ForcedSortName))
 1162            {
 1281163                var forcedSortName = source.ForcedSortName;
 1281164                if (!string.IsNullOrEmpty(forcedSortName))
 1165                {
 31166                    target.ForcedSortName = forcedSortName;
 1167                }
 1168            }
 1169
 1291170            if (mergeMetadataSettings)
 1171            {
 21172                if (replaceData || !target.IsLocked)
 1173                {
 21174                    target.IsLocked = target.IsLocked || source.IsLocked;
 1175                }
 1176
 21177                if (target.LockedFields.Length == 0)
 1178                {
 01179                    target.LockedFields = source.LockedFields;
 1180                }
 1181                else
 1182                {
 21183                    target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray();
 1184                }
 1185
 21186                if (source.DateCreated != DateTime.MinValue)
 1187                {
 11188                    target.DateCreated = source.DateCreated;
 1189                }
 1190
 21191                if (replaceData || source.DateModified != DateTime.MinValue)
 1192                {
 21193                    target.DateModified = source.DateModified;
 1194                }
 1195
 21196                if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode))
 1197                {
 21198                    target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
 1199                }
 1200
 21201                if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataLanguage))
 1202                {
 21203                    target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
 1204                }
 1205            }
 1291206        }
 1207
 1208        private static void MergePeople(IReadOnlyList<PersonInfo> source, IReadOnlyList<PersonInfo> target)
 1209        {
 41210            var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
 41211            var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
 1212
 161213            foreach (var name in targetByName.Select(g => g.Key))
 1214            {
 41215                var targetPeople = targetByName[name].ToArray();
 41216                var sourcePeople = sourceByName[name].ToArray();
 1217
 41218                if (sourcePeople.Length == 0)
 1219                {
 1220                    continue;
 1221                }
 1222
 121223                for (int i = 0; i < targetPeople.Length; i++)
 1224                {
 31225                    var person = targetPeople[i];
 31226                    var personInSource = i < sourcePeople.Length ? sourcePeople[i] : sourcePeople[0];
 1227
 101228                    foreach (var providerId in personInSource.ProviderIds)
 1229                    {
 21230                        person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
 1231                    }
 1232
 31233                    if (string.IsNullOrWhiteSpace(person.ImageUrl))
 1234                    {
 21235                        person.ImageUrl = personInSource.ImageUrl;
 1236                    }
 1237
 31238                    if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role))
 1239                    {
 01240                        person.Role = personInSource.Role;
 1241                    }
 1242
 31243                    if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue)
 1244                    {
 01245                        person.SortOrder = personInSource.SortOrder;
 1246                    }
 1247                }
 1248            }
 41249        }
 1250
 1251        private static void MergeDisplayOrder(BaseItem source, BaseItem target, bool replaceData)
 1252        {
 1291253            if (source is IHasDisplayOrder sourceHasDisplayOrder
 1291254                && target is IHasDisplayOrder targetHasDisplayOrder)
 1255            {
 491256                if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder))
 1257                {
 481258                    var displayOrder = sourceHasDisplayOrder.DisplayOrder;
 481259                    if (!string.IsNullOrWhiteSpace(displayOrder))
 1260                    {
 31261                        targetHasDisplayOrder.DisplayOrder = displayOrder;
 1262                    }
 1263                }
 1264            }
 1291265        }
 1266
 1267        private static void MergeAlbumArtist(BaseItem source, BaseItem target, bool replaceData)
 1268        {
 1291269            if (source is IHasAlbumArtist sourceHasAlbumArtist
 1291270                && target is IHasAlbumArtist targetHasAlbumArtist)
 1271            {
 281272                if (replaceData || targetHasAlbumArtist.AlbumArtists.Count == 0)
 1273                {
 271274                    targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
 1275                }
 11276                else if (sourceHasAlbumArtist.AlbumArtists.Count > 0)
 1277                {
 11278                    targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.Al
 1279                }
 1280            }
 1021281        }
 1282
 1283        private static void MergeVideoInfo(BaseItem source, BaseItem target, bool replaceData)
 1284        {
 1291285            if (source is Video sourceCast && target is Video targetCast)
 1286            {
 521287                if (sourceCast.Video3DFormat.HasValue && (replaceData || !targetCast.Video3DFormat.HasValue))
 1288                {
 21289                    targetCast.Video3DFormat = sourceCast.Video3DFormat;
 1290                }
 1291            }
 1291292        }
 1293    }
 1294}

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)