< 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
57%
Covered lines: 317
Uncovered lines: 230
Coverable lines: 547
Total lines: 1340
Line coverage: 57.9%
Branch coverage
61%
Covered branches: 315
Total branches: 516
Branch coverage: 61%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 5/1/2026 - 12:13:05 AM Line coverage: 56.7% (305/537) Branch coverage: 62.8% (313/498) Total lines: 13155/5/2026 - 12:15:44 AM Line coverage: 57.3% (308/537) Branch coverage: 63.8% (318/498) Total lines: 13155/8/2026 - 12:15:13 AM Line coverage: 57.5% (310/539) Branch coverage: 64.1% (322/502) Total lines: 13205/20/2026 - 12:15:44 AM Line coverage: 57.5% (310/539) Branch coverage: 60.9% (306/502) Total lines: 13206/22/2026 - 12:15:37 AM Line coverage: 57.3% (311/542) Branch coverage: 60.8% (308/506) Total lines: 13287/18/2026 - 12:15:19 AM Line coverage: 57.2% (312/545) Branch coverage: 60.7% (310/510) Total lines: 13357/22/2026 - 12:16:22 AM Line coverage: 57.2% (312/545) Branch coverage: 60.7% (311/512) Total lines: 13357/27/2026 - 12:16:14 AM Line coverage: 57.2% (312/545) Branch coverage: 60.9% (312/512) Total lines: 13357/28/2026 - 12:13:49 AM Line coverage: 57.2% (312/545) Branch coverage: 60.7% (311/512) Total lines: 13357/29/2026 - 12:15:53 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 13407/30/2026 - 12:15:56 AM Line coverage: 57.4% (314/547) Branch coverage: 61% (315/516) Total lines: 13407/31/2026 - 12:16:28 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 13408/5/2026 - 12:17:09 AM Line coverage: 57.4% (314/547) Branch coverage: 61% (315/516) Total lines: 13408/7/2026 - 12:16:14 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 13408/8/2026 - 12:17:31 AM Line coverage: 57.4% (314/547) Branch coverage: 61% (315/516) Total lines: 13408/9/2026 - 12:16:58 AM Line coverage: 57.9% (317/547) Branch coverage: 61% (315/516) Total lines: 1340 5/1/2026 - 12:13:05 AM Line coverage: 56.7% (305/537) Branch coverage: 62.8% (313/498) Total lines: 13155/5/2026 - 12:15:44 AM Line coverage: 57.3% (308/537) Branch coverage: 63.8% (318/498) Total lines: 13155/8/2026 - 12:15:13 AM Line coverage: 57.5% (310/539) Branch coverage: 64.1% (322/502) Total lines: 13205/20/2026 - 12:15:44 AM Line coverage: 57.5% (310/539) Branch coverage: 60.9% (306/502) Total lines: 13206/22/2026 - 12:15:37 AM Line coverage: 57.3% (311/542) Branch coverage: 60.8% (308/506) Total lines: 13287/18/2026 - 12:15:19 AM Line coverage: 57.2% (312/545) Branch coverage: 60.7% (310/510) Total lines: 13357/22/2026 - 12:16:22 AM Line coverage: 57.2% (312/545) Branch coverage: 60.7% (311/512) Total lines: 13357/27/2026 - 12:16:14 AM Line coverage: 57.2% (312/545) Branch coverage: 60.9% (312/512) Total lines: 13357/28/2026 - 12:13:49 AM Line coverage: 57.2% (312/545) Branch coverage: 60.7% (311/512) Total lines: 13357/29/2026 - 12:15:53 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 13407/30/2026 - 12:15:56 AM Line coverage: 57.4% (314/547) Branch coverage: 61% (315/516) Total lines: 13407/31/2026 - 12:16:28 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 13408/5/2026 - 12:17:09 AM Line coverage: 57.4% (314/547) Branch coverage: 61% (315/516) Total lines: 13408/7/2026 - 12:16:14 AM Line coverage: 57.4% (314/547) Branch coverage: 60.8% (314/516) Total lines: 13408/8/2026 - 12:17:31 AM Line coverage: 57.4% (314/547) Branch coverage: 61% (315/516) Total lines: 13408/9/2026 - 12:16:58 AM Line coverage: 57.9% (317/547) Branch coverage: 61% (315/516) Total lines: 1340

Coverage delta

Coverage delta 4 -4

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        {
 57540            ServerConfigurationManager = serverConfigurationManager;
 57541            Logger = logger;
 57542            ProviderManager = providerManager;
 57543            FileSystem = fileSystem;
 57544            LibraryManager = libraryManager;
 57545            ExternalDataManager = externalDataManager;
 57546            ItemRepository = itemRepository;
 57547            ImageProvider = new ItemImageProvider(Logger, ProviderManager, FileSystem);
 57548        }
 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
 3666        protected virtual bool EnableUpdatingPremiereDateFromChildren => false;
 67
 3668        protected virtual bool EnableUpdatingGenresFromChildren => false;
 69
 3670        protected virtual bool EnableUpdatingStudiosFromChildren => false;
 71
 3672        protected virtual bool EnableUpdatingOfficialRatingFromChildren => false;
 73
 52874        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        {
 6191            var itemOfType = (TItemType)item;
 6192            var updateType = ItemUpdateType.None;
 93
 6194            var libraryOptions = LibraryManager.GetLibraryOptions(item);
 6195            var isFirstRefresh = item.DateLastRefreshed == DateTime.MinValue;
 6196            var hasRefreshedMetadata = true;
 6197            var hasRefreshedImages = true;
 98
 6199            var requiresRefresh = libraryOptions.AutomaticRefreshIntervalDays > 0 && (DateTime.UtcNow - item.DateLastRef
 100
 61101            if (!requiresRefresh && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
 102            {
 103                // TODO: If this returns true, should we instead just change metadata refresh mode to Full?
 61104                requiresRefresh = item.RequiresRefresh();
 105
 61106                if (requiresRefresh)
 107                {
 18108                    Logger.LogDebug("Refreshing {Type} {Item} because item.RequiresRefresh() returned true", typeof(TIte
 109                }
 110            }
 111
 61112            if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
 113            {
 0114                if (ImageProvider.RemoveImages(item))
 115                {
 0116                    updateType |= ItemUpdateType.ImageUpdate;
 117                }
 118            }
 119
 61120            var localImagesFailed = false;
 61121            var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList();
 122
 123            // Only validate already registered images if we are replacing and saving locally
 61124            if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages)
 125            {
 0126                item.ValidateImages();
 127            }
 128            else
 129            {
 130                // Run full image validation and register new local images
 131                try
 132                {
 61133                    if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptio
 134                    {
 0135                        updateType |= ItemUpdateType.ImageUpdate;
 136                    }
 61137                }
 0138                catch (Exception ex)
 139                {
 0140                    localImagesFailed = true;
 0141                    Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
 0142                }
 143            }
 144
 61145            var metadataResult = new MetadataResult<TItemType>
 61146            {
 61147                Item = itemOfType
 61148            };
 149
 61150            var beforeSaveResult = await BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || r
 61151                .ConfigureAwait(false);
 61152            updateType |= beforeSaveResult;
 153
 61154            if (isFirstRefresh)
 155            {
 35156                await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, false, cancellationToken).ConfigureAw
 157            }
 158
 159            // Next run metadata providers
 61160            if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
 161            {
 61162                var providers = GetProviders(item, libraryOptions, refreshOptions, isFirstRefresh, requiresRefresh)
 61163                    .ToList();
 164
 61165                if (providers.Count > 0 || isFirstRefresh || requiresRefresh)
 166                {
 36167                    if (item.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata))
 168                    {
 34169                        updateType |= ItemUpdateType.MetadataImport;
 170                    }
 171                }
 172
 61173                if (providers.Count > 0)
 174                {
 36175                    var id = itemOfType.GetLookupInfo();
 176
 36177                    if (refreshOptions.SearchResult is not null)
 178                    {
 0179                        ApplySearchResult(id, refreshOptions.SearchResult);
 180                    }
 181
 36182                    id.IsAutomated = refreshOptions.IsAutomated;
 183
 36184                    var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any();
 36185                    var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider
 186
 36187                    updateType |= result.UpdateType;
 36188                    if (result.Failures > 0)
 189                    {
 0190                        hasRefreshedMetadata = false;
 191                    }
 192                }
 193            }
 194
 195            // Next run remote image providers, but only if local image providers didn't throw an exception
 61196            if (!localImagesFailed && refreshOptions.ImageRefreshMode > MetadataRefreshMode.ValidationOnly)
 197            {
 61198                var providers = GetNonLocalImageProviders(item, allImageProviders, refreshOptions).ToList();
 199
 61200                if (providers.Count > 0)
 201                {
 0202                    var result = await ImageProvider.RefreshImages(itemOfType, libraryOptions, providers, refreshOptions
 203
 0204                    updateType |= result.UpdateType;
 0205                    if (result.Failures > 0)
 206                    {
 0207                        hasRefreshedImages = false;
 208                    }
 209                }
 210            }
 211
 61212            if (hasRefreshedMetadata && hasRefreshedImages)
 213            {
 61214                item.DateLastRefreshed = DateTime.UtcNow;
 61215                updateType |= item.OnMetadataChanged();
 216            }
 217
 61218            updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataR
 219
 60220            await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false);
 221
 60222            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, isFirstRefresh, cancellationToken).ConfigureAwait(fa
 251                }
 252
 253                return updateType;
 254            }
 60255        }
 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, bool reattachUserDat
 279        {
 71280            await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
 71281            if (reattachUserData)
 282            {
 35283                await result.Item.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
 284            }
 285
 70286            if (result.Item.SupportsPeople && result.People is not null)
 287            {
 0288                var baseItem = result.Item;
 289
 0290                await LibraryManager.UpdatePeopleAsync(baseItem, result.People, cancellationToken).ConfigureAwait(false)
 291            }
 70292        }
 293
 294        protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationT
 295        {
 60296            item.AfterMetadataRefresh();
 60297            return Task.CompletedTask;
 298        }
 299
 300        /// <summary>
 301        /// Before the save.
 302        /// </summary>
 303        /// <param name="item">The item.</param>
 304        /// <param name="isFullRefresh">if set to <c>true</c> [is full refresh].</param>
 305        /// <param name="currentUpdateType">Type of the current update.</param>
 306        /// <returns>ItemUpdateType.</returns>
 307        private async Task<ItemUpdateType> BeforeSave(TItemType item, bool isFullRefresh, ItemUpdateType currentUpdateTy
 308        {
 61309            var updateType = BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
 310
 61311            updateType |= item.OnMetadataChanged();
 312
 61313            if (updateType == ItemUpdateType.None)
 314            {
 26315                if (!await ItemRepository.ItemExistsAsync(item.Id).ConfigureAwait(false))
 316                {
 0317                    return ItemUpdateType.MetadataImport;
 318                }
 319            }
 320
 61321            return updateType;
 61322        }
 323
 324        protected virtual ItemUpdateType BeforeSaveInternal(TItemType item, bool isFullRefresh, ItemUpdateType updateTyp
 325        {
 61326            if (EnableUpdateMetadataFromChildren(item, isFullRefresh, updateType))
 327            {
 0328                var children = GetChildrenForMetadataUpdates(item);
 0329                updateType = UpdateMetadataFromChildren(item, children, isFullRefresh, updateType);
 330            }
 331
 61332            var presentationUniqueKey = item.CreatePresentationUniqueKey();
 61333            if (!string.Equals(item.PresentationUniqueKey, presentationUniqueKey, StringComparison.Ordinal))
 334            {
 35335                item.PresentationUniqueKey = presentationUniqueKey;
 35336                updateType |= ItemUpdateType.MetadataImport;
 337            }
 338
 339            // Cleanup extracted files if source file was modified
 61340            var itemPath = item.Path;
 61341            if (!string.IsNullOrEmpty(itemPath))
 342            {
 61343                var info = FileSystem.GetFileSystemInfo(itemPath);
 61344                if (info.Exists && item.HasChanged(info.LastWriteTimeUtc))
 345                {
 0346                    Logger.LogDebug("File modification time changed from {Then} to {Now}: {Path}", item.DateModified, in
 347
 0348                    item.DateModified = info.LastWriteTimeUtc;
 0349                    if (ServerConfigurationManager.GetMetadataConfiguration().UseFileCreationTimeForDateAdded)
 350                    {
 0351                        if (info.CreationTimeUtc > DateTime.MinValue)
 352                        {
 0353                            item.DateCreated = info.CreationTimeUtc;
 354                        }
 355                    }
 356
 0357                    if (item is Video video)
 358                    {
 0359                        Logger.LogInformation("File changed, pruning extracted data: {Path}", item.Path);
 0360                        ExternalDataManager.DeleteExternalItemDataAsync(video, CancellationToken.None).GetAwaiter().GetR
 361                    }
 362
 0363                    updateType |= ItemUpdateType.MetadataImport;
 364                }
 365            }
 366
 61367            return updateType;
 368        }
 369
 370        protected virtual bool EnableUpdateMetadataFromChildren(TItemType item, bool isFullRefresh, ItemUpdateType curre
 371        {
 61372            if (item is Folder folder)
 373            {
 61374                if (!isFullRefresh && currentUpdateType == ItemUpdateType.None)
 375                {
 25376                    return folder.SupportsDateLastMediaAdded;
 377                }
 378
 36379                if (isFullRefresh || currentUpdateType > ItemUpdateType.None)
 380                {
 36381                    if (EnableUpdatingPremiereDateFromChildren || EnableUpdatingGenresFromChildren || EnableUpdatingStud
 382                    {
 0383                        return true;
 384                    }
 385
 36386                    if (folder.SupportsDateLastMediaAdded || folder.SupportsCumulativeRunTimeTicks)
 387                    {
 0388                        return true;
 389                    }
 390                }
 391            }
 392
 36393            return false;
 394        }
 395
 396        protected virtual IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(TItemType item)
 397        {
 0398            if (item is Folder folder)
 399            {
 0400                return folder.GetRecursiveChildren();
 401            }
 402
 0403            return [];
 404        }
 405
 406        protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IReadOnlyList<BaseItem> children, bo
 407        {
 0408            var updateType = ItemUpdateType.None;
 409
 0410            if (item is Folder folder)
 411            {
 0412                if (folder.SupportsDateLastMediaAdded)
 413                {
 0414                    updateType |= UpdateDateLastMediaAdded(item, children);
 415                }
 416
 0417                if ((isFullRefresh || currentUpdateType > ItemUpdateType.None) && folder.SupportsCumulativeRunTimeTicks)
 418                {
 0419                    updateType |= UpdateCumulativeRunTimeTicks(item, children);
 420                }
 421            }
 422
 0423            if (!(isFullRefresh || currentUpdateType > ItemUpdateType.None) || item.IsLocked)
 424            {
 0425                return updateType;
 426            }
 427
 0428            if (EnableUpdatingPremiereDateFromChildren)
 429            {
 0430                updateType |= UpdatePremiereDate(item, children);
 431            }
 432
 0433            if (EnableUpdatingGenresFromChildren)
 434            {
 0435                updateType |= UpdateGenres(item, children);
 436            }
 437
 0438            if (EnableUpdatingStudiosFromChildren)
 439            {
 0440                updateType |= UpdateStudios(item, children);
 441            }
 442
 0443            if (EnableUpdatingOfficialRatingFromChildren)
 444            {
 0445                updateType |= UpdateOfficialRating(item, children);
 446            }
 447
 0448            return updateType;
 449        }
 450
 451        private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IReadOnlyList<BaseItem> children)
 452        {
 0453            if (item is Folder folder && folder.SupportsCumulativeRunTimeTicks)
 454            {
 0455                long ticks = 0;
 456
 0457                foreach (var child in children)
 458                {
 0459                    if (!child.IsFolder)
 460                    {
 0461                        ticks += child.RunTimeTicks ?? 0;
 462                    }
 463                }
 464
 0465                if (!folder.RunTimeTicks.HasValue || folder.RunTimeTicks.Value != ticks)
 466                {
 0467                    folder.RunTimeTicks = ticks;
 0468                    return ItemUpdateType.MetadataImport;
 469                }
 470            }
 471
 0472            return ItemUpdateType.None;
 473        }
 474
 475        private ItemUpdateType UpdateDateLastMediaAdded(TItemType item, IReadOnlyList<BaseItem> children)
 476        {
 0477            var updateType = ItemUpdateType.None;
 478
 0479            if (item is Folder folder && folder.SupportsDateLastMediaAdded)
 480            {
 0481                var dateLastMediaAdded = DateTime.MinValue;
 0482                var any = false;
 483
 0484                foreach (var child in children)
 485                {
 486                    // Exclude any folders and virtual items since they are only placeholders
 0487                    if (!child.IsFolder && !child.IsVirtualItem)
 488                    {
 0489                        var childDateCreated = child.DateCreated;
 0490                        if (childDateCreated > dateLastMediaAdded)
 491                        {
 0492                            dateLastMediaAdded = childDateCreated;
 493                        }
 494
 0495                        any = true;
 496                    }
 497                }
 498
 0499                if ((!folder.DateLastMediaAdded.HasValue && any) || folder.DateLastMediaAdded != dateLastMediaAdded)
 500                {
 0501                    folder.DateLastMediaAdded = dateLastMediaAdded;
 0502                    updateType = ItemUpdateType.MetadataImport;
 503                }
 504            }
 505
 0506            return updateType;
 507        }
 508
 509        private ItemUpdateType UpdatePremiereDate(TItemType item, IReadOnlyList<BaseItem> children)
 510        {
 0511            var updateType = ItemUpdateType.None;
 512
 0513            if (children.Count == 0)
 514            {
 0515                return updateType;
 516            }
 517
 0518            var date = children.Select(i => i.PremiereDate ?? DateTime.MaxValue).Min();
 519
 0520            var originalPremiereDate = item.PremiereDate;
 0521            var originalProductionYear = item.ProductionYear;
 522
 0523            if (date > DateTime.MinValue && date < DateTime.MaxValue)
 524            {
 0525                item.PremiereDate = date;
 0526                item.ProductionYear = date.Year;
 527            }
 528            else
 529            {
 0530                var year = children.Select(i => i.ProductionYear ?? 0).Min();
 531
 0532                if (year > 0)
 533                {
 0534                    item.ProductionYear = year;
 535                }
 536            }
 537
 0538            if ((originalPremiereDate ?? DateTime.MinValue) != (item.PremiereDate ?? DateTime.MinValue)
 0539                || (originalProductionYear ?? -1) != (item.ProductionYear ?? -1))
 540            {
 0541                updateType |= ItemUpdateType.MetadataEdit;
 542            }
 543
 0544            return updateType;
 545        }
 546
 547        private ItemUpdateType UpdateGenres(TItemType item, IReadOnlyList<BaseItem> children)
 548        {
 0549            var updateType = ItemUpdateType.None;
 550
 0551            if (!item.LockedFields.Contains(MetadataField.Genres))
 552            {
 0553                var currentList = item.Genres;
 554
 0555                item.Genres = children.SelectMany(i => i.Genres)
 0556                    .Distinct(StringComparer.OrdinalIgnoreCase)
 0557                    .ToArray();
 558
 0559                if (currentList.Length != item.Genres.Length || !currentList.Order().SequenceEqual(item.Genres.Order(), 
 560                {
 0561                    updateType |= ItemUpdateType.MetadataEdit;
 562                }
 563            }
 564
 0565            return updateType;
 566        }
 567
 568        private ItemUpdateType UpdateStudios(TItemType item, IReadOnlyList<BaseItem> children)
 569        {
 0570            var updateType = ItemUpdateType.None;
 571
 0572            if (!item.LockedFields.Contains(MetadataField.Studios))
 573            {
 0574                var currentList = item.Studios;
 575
 0576                item.Studios = children.SelectMany(i => i.Studios)
 0577                    .Distinct(StringComparer.OrdinalIgnoreCase)
 0578                    .ToArray();
 579
 0580                if (currentList.Length != item.Studios.Length || !currentList.Order().SequenceEqual(item.Studios.Order()
 581                {
 0582                    updateType |= ItemUpdateType.MetadataEdit;
 583                }
 584            }
 585
 0586            return updateType;
 587        }
 588
 589        private ItemUpdateType UpdateOfficialRating(TItemType item, IReadOnlyList<BaseItem> children)
 590        {
 0591            var updateType = ItemUpdateType.None;
 592
 0593            if (!item.LockedFields.Contains(MetadataField.OfficialRating))
 594            {
 0595                if (item.UpdateRatingToItems(children))
 596                {
 0597                    updateType |= ItemUpdateType.MetadataEdit;
 598                }
 599            }
 600
 0601            return updateType;
 602        }
 603
 604        /// <summary>
 605        /// Gets the providers.
 606        /// </summary>
 607        /// <param name="item">A media item.</param>
 608        /// <param name="libraryOptions">The LibraryOptions to use.</param>
 609        /// <param name="options">The MetadataRefreshOptions to use.</param>
 610        /// <param name="isFirstRefresh">Specifies first refresh mode.</param>
 611        /// <param name="requiresRefresh">Specifies refresh mode.</param>
 612        /// <returns>IEnumerable{`0}.</returns>
 613        protected IEnumerable<IMetadataProvider> GetProviders(BaseItem item, LibraryOptions libraryOptions, MetadataRefr
 614        {
 615            // Get providers to refresh
 61616            var providers = ProviderManager.GetMetadataProviders<TItemType>(item, libraryOptions).ToList();
 617
 61618            var metadataRefreshMode = options.MetadataRefreshMode;
 619
 620            // Run all if either of these flags are true
 61621            var runAllProviders = options.ReplaceAllMetadata ||
 61622                metadataRefreshMode == MetadataRefreshMode.FullRefresh ||
 61623                (isFirstRefresh && metadataRefreshMode >= MetadataRefreshMode.Default) ||
 61624                (requiresRefresh && metadataRefreshMode >= MetadataRefreshMode.Default);
 625
 61626            if (!runAllProviders)
 627            {
 25628                var providersWithChanges = providers
 25629                    .Where(i =>
 25630                    {
 25631                        if (i is IHasItemChangeMonitor hasFileChangeMonitor)
 25632                        {
 25633                            return HasChanged(item, hasFileChangeMonitor, options.DirectoryService);
 25634                        }
 25635
 25636                        return false;
 25637                    })
 25638                    .ToList();
 639
 25640                if (providersWithChanges.Count == 0)
 641                {
 25642                    providers = new List<IMetadataProvider<TItemType>>();
 643                }
 644                else
 645                {
 0646                    var anyRemoteProvidersChanged = providersWithChanges.OfType<IRemoteMetadataProvider>()
 0647                        .Any();
 648
 0649                    var anyLocalProvidersChanged = providersWithChanges.OfType<ILocalMetadataProvider>()
 0650                        .Any();
 651
 0652                    var anyLocalPreRefreshProvidersChanged = providersWithChanges.OfType<IPreRefreshProvider>()
 0653                        .Any();
 654
 0655                    providers = providers.Where(i =>
 0656                    {
 0657                        // If any provider reports a change, always run local ones as well
 0658                        if (i is ILocalMetadataProvider)
 0659                        {
 0660                            return anyRemoteProvidersChanged || anyLocalProvidersChanged || anyLocalPreRefreshProvidersC
 0661                        }
 0662
 0663                        // If any remote providers changed, run them all so that priorities can be honored
 0664                        if (i is IRemoteMetadataProvider)
 0665                        {
 0666                            if (options.MetadataRefreshMode == MetadataRefreshMode.ValidationOnly)
 0667                            {
 0668                                return false;
 0669                            }
 0670
 0671                            return anyRemoteProvidersChanged;
 0672                        }
 0673
 0674                        // Run custom refresh providers if they report a change or any remote providers change
 0675                        return anyRemoteProvidersChanged || providersWithChanges.Contains(i);
 0676                    }).ToList();
 677                }
 678            }
 679
 61680            return providers;
 681        }
 682
 683        protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvide
 684        {
 685            // Get providers to refresh
 61686            var providers = allImageProviders.Where(i => i is not ILocalImageProvider);
 687
 688            // When identifying, run the provider the user picked first so the correct image is used.
 61689            if (!string.IsNullOrEmpty(options.SearchResult?.SearchProviderName))
 690            {
 0691                providers = providers
 0692                    .OrderBy(i => string.Equals(i.Name, options.SearchResult.SearchProviderName, StringComparison.Ordina
 693            }
 694
 61695            var dateLastImageRefresh = item.DateLastRefreshed;
 696
 697            // Run all if either of these flags are true
 61698            var runAllProviders = options.ImageRefreshMode == MetadataRefreshMode.FullRefresh || dateLastImageRefresh.Da
 699
 61700            if (!runAllProviders)
 701            {
 26702                providers = providers
 26703                    .Where(i =>
 26704                    {
 26705                        if (i is IHasItemChangeMonitor hasFileChangeMonitor)
 26706                        {
 26707                            return HasChanged(item, hasFileChangeMonitor, options.DirectoryService);
 26708                        }
 26709
 26710                        return false;
 26711                    });
 712            }
 713
 61714            return providers;
 715        }
 716
 717        public bool CanRefresh(BaseItem item)
 718        {
 1144719            return item is TItemType;
 720        }
 721
 722        public bool CanRefreshPrimary(Type type)
 723        {
 1467724            return type == typeof(TItemType);
 725        }
 726
 727        protected virtual async Task<RefreshResult> RefreshWithProviders(
 728            MetadataResult<TItemType> metadata,
 729            TIdType id,
 730            MetadataRefreshOptions options,
 731            ICollection<IMetadataProvider> providers,
 732            ItemImageProvider imageService,
 733            bool isSavingMetadata,
 734            CancellationToken cancellationToken)
 735        {
 36736            var refreshResult = new RefreshResult
 36737            {
 36738                UpdateType = ItemUpdateType.None
 36739            };
 740
 36741            var item = metadata.Item;
 742
 36743            var customProviders = providers.OfType<ICustomMetadataProvider<TItemType>>().ToList();
 36744            var logName = !item.IsFileProtocol ? item.Name ?? item.Path : item.Path ?? item.Name;
 745
 72746            foreach (var provider in customProviders.Where(i => i is IPreRefreshProvider))
 747            {
 0748                await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwa
 749            }
 750
 36751            if (item.IsLocked)
 752            {
 0753                return refreshResult;
 754            }
 755
 36756            var temp = new MetadataResult<TItemType>
 36757            {
 36758                Item = CreateNew()
 36759            };
 36760            temp.Item.Path = item.Path;
 36761            temp.Item.Id = item.Id;
 36762            temp.Item.ParentIndexNumber = item.ParentIndexNumber;
 36763            temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode;
 36764            temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage;
 765
 36766            var foundImageTypes = new List<ImageType>();
 767
 768            // Do not execute local providers if we are identifying or replacing with local metadata saving enabled
 36769            if (options.SearchResult is null && !(isSavingMetadata && options.ReplaceAllMetadata))
 770            {
 72771                foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
 772                {
 0773                    var providerName = provider.GetType().Name;
 0774                    Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
 775
 0776                    var itemInfo = new ItemInfo(item);
 777
 778                    try
 779                    {
 0780                        var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken
 781
 0782                        if (localItem.HasMetadata)
 783                        {
 0784                            foreach (var remoteImage in localItem.RemoteImages)
 785                            {
 786                                try
 787                                {
 0788                                    if (item.ImageInfos.Any(x => x.Type == remoteImage.Type)
 0789                                        && !options.IsReplacingImage(remoteImage.Type))
 790                                    {
 0791                                        continue;
 792                                    }
 793
 0794                                    await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cance
 0795                                    refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
 796
 797                                    // remember imagetype that has just been downloaded
 0798                                    foundImageTypes.Add(remoteImage.Type);
 0799                                }
 0800                                catch (HttpRequestException ex)
 801                                {
 0802                                    Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteIm
 0803                                }
 0804                            }
 805
 0806                            if (foundImageTypes.Count > 0)
 807                            {
 0808                                imageService.UpdateReplaceImages(options, foundImageTypes);
 809                            }
 810
 0811                            if (imageService.MergeImages(item, localItem.Images, options))
 812                            {
 0813                                refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
 814                            }
 815
 0816                            MergeData(localItem, temp, [], false, true);
 0817                            refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
 818
 0819                            break;
 820                        }
 821
 0822                        Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
 0823                    }
 0824                    catch (OperationCanceledException)
 825                    {
 0826                        throw;
 827                    }
 0828                    catch (Exception ex)
 829                    {
 0830                        Logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, logName);
 831
 832                        // If a local provider fails, consider that a failure
 0833                        refreshResult.ErrorMessage = ex.Message;
 0834                    }
 0835                }
 836            }
 837
 36838            var isLocalLocked = temp.Item.IsLocked;
 36839            if (!isLocalLocked && (options.ReplaceAllMetadata || options.MetadataRefreshMode > MetadataRefreshMode.Valid
 840            {
 36841                var remoteProviders = providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>();
 842
 843                // When identifying, run the provider the user picked first so the correct IDs are used.
 36844                if (!string.IsNullOrEmpty(options.SearchResult?.SearchProviderName))
 845                {
 0846                    remoteProviders = remoteProviders
 0847                        .OrderBy(i => string.Equals(i.Name, options.SearchResult.SearchProviderName, StringComparison.Or
 848                }
 849
 36850                var remoteResult = await ExecuteRemoteProviders(temp, logName, false, id, remoteProviders, cancellationT
 851
 36852                refreshResult.UpdateType |= remoteResult.UpdateType;
 36853                refreshResult.ErrorMessage = remoteResult.ErrorMessage;
 36854                refreshResult.Failures += remoteResult.Failures;
 855            }
 856
 36857            if (providers.Any(i => i is not ICustomMetadataProvider))
 858            {
 0859                if (refreshResult.UpdateType > ItemUpdateType.None)
 860                {
 0861                    if (!options.RemoveOldMetadata)
 862                    {
 863                        // Add existing metadata to provider result if it does not exist there
 0864                        MergeData(metadata, temp, [], false, false);
 865                    }
 866
 0867                    if (isLocalLocked)
 868                    {
 0869                        MergeData(temp, metadata, item.LockedFields, true, true);
 870                    }
 871                    else
 872                    {
 0873                        var shouldReplace = (options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly && options
 0874                            // Case for Scan for new and updated files
 0875                            || (options.MetadataRefreshMode == MetadataRefreshMode.Default && !options.ReplaceAllMetadat
 0876                        MergeData(temp, metadata, item.LockedFields, shouldReplace, true);
 877                    }
 878                }
 879            }
 880
 144881            foreach (var provider in customProviders.Where(i => i is not IPreRefreshProvider))
 882            {
 36883                await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwa
 884            }
 885
 36886            return refreshResult;
 36887        }
 888
 889        private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, string logName
 890        {
 36891            Logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, logName);
 892
 893            try
 894            {
 36895                refreshResult.UpdateType |= await provider.FetchAsync(item, options, cancellationToken).ConfigureAwait(f
 36896            }
 0897            catch (OperationCanceledException)
 898            {
 0899                throw;
 900            }
 0901            catch (Exception ex)
 902            {
 0903                refreshResult.ErrorMessage = ex.Message;
 0904                Logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, logName);
 0905            }
 36906        }
 907
 908        protected virtual TItemType CreateNew()
 909        {
 36910            return new TItemType();
 911        }
 912
 913        private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, bool re
 914        {
 36915            var refreshResult = new RefreshResult();
 916
 36917            if (id is not null)
 918            {
 36919                MergeNewData(temp.Item, id);
 920            }
 921
 72922            foreach (var provider in providers)
 923            {
 0924                var providerName = provider.GetType().Name;
 0925                Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
 926
 927                try
 928                {
 0929                    var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false);
 930
 0931                    if (result.HasMetadata)
 932                    {
 0933                        result.Provider = provider.Name;
 934
 0935                        MergeData(result, temp, [], replaceData, false);
 0936                        MergeNewData(temp.Item, id);
 937
 0938                        refreshResult.UpdateType |= ItemUpdateType.MetadataDownload;
 939                    }
 940                    else
 941                    {
 0942                        Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
 943                    }
 0944                }
 0945                catch (OperationCanceledException)
 946                {
 0947                    throw;
 948                }
 0949                catch (Exception ex)
 950                {
 0951                    refreshResult.Failures++;
 0952                    refreshResult.ErrorMessage = ex.Message;
 0953                    Logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, logName);
 0954                }
 0955            }
 956
 36957            return refreshResult;
 36958        }
 959
 960        private void MergeNewData(TItemType source, TIdType lookupInfo)
 961        {
 962            // Copy new provider id's that may have been obtained
 72963            foreach (var providerId in source.ProviderIds)
 964            {
 0965                var key = providerId.Key;
 966
 967                // Don't replace existing Id's.
 0968                lookupInfo.ProviderIds.TryAdd(key, providerId.Value);
 969            }
 36970        }
 971
 972        private bool HasChanged(BaseItem item, IHasItemChangeMonitor changeMonitor, IDirectoryService directoryService)
 973        {
 974            try
 975            {
 25976                var hasChanged = changeMonitor.HasChanged(item, directoryService);
 977
 24978                if (hasChanged)
 979                {
 0980                    Logger.LogDebug("{Monitor} reports change to {Item}", changeMonitor.GetType().Name, item.Path ?? ite
 981                }
 982
 24983                return hasChanged;
 984            }
 1985            catch (Exception ex)
 986            {
 1987                Logger.LogError(ex, "Error in {Monitor}.HasChanged", changeMonitor.GetType().Name);
 1988                return false;
 989            }
 25990        }
 991
 992        /// <summary>
 993        /// Merges metadata from source into target.
 994        /// </summary>
 995        /// <param name="source">The source for new metadata.</param>
 996        /// <param name="target">The target to insert new metadata into.</param>
 997        /// <param name="lockedFields">The fields that are locked and should not be updated.</param>
 998        /// <param name="replaceData"><c>true</c> if existing data should be replaced.</param>
 999        /// <param name="mergeMetadataSettings"><c>true</c> if the metadata settings in target should be updated to matc
 1000        /// <exception cref="ArgumentException">Thrown if source or target are null.</exception>
 1001        protected virtual void MergeData(
 1002            MetadataResult<TItemType> source,
 1003            MetadataResult<TItemType> target,
 1004            MetadataField[] lockedFields,
 1005            bool replaceData,
 1006            bool mergeMetadataSettings)
 1007        {
 31008            MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
 31009        }
 1010
 1011        internal static void MergeBaseItemData(
 1012            MetadataResult<TItemType> sourceResult,
 1013            MetadataResult<TItemType> targetResult,
 1014            MetadataField[] lockedFields,
 1015            bool replaceData,
 1016            bool mergeMetadataSettings)
 1017        {
 1321018            var source = sourceResult.Item;
 1321019            var target = targetResult.Item;
 1020
 1321021            ArgumentNullException.ThrowIfNull(sourceResult);
 1321022            ArgumentNullException.ThrowIfNull(targetResult);
 1023
 1321024            if (!lockedFields.Contains(MetadataField.Name))
 1025            {
 1291026                if (replaceData || string.IsNullOrEmpty(target.Name))
 1027                {
 1028                    // Safeguard against incoming data having an empty name
 1281029                    if (!string.IsNullOrWhiteSpace(source.Name))
 1030                    {
 31031                        target.Name = source.Name;
 1032                    }
 1033                }
 1034            }
 1035
 1321036            if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
 1037            {
 1311038                target.OriginalTitle = source.OriginalTitle;
 1039            }
 1040
 1321041            if (replaceData || string.IsNullOrEmpty(target.HomePageUrl))
 1042            {
 1321043                target.HomePageUrl = source.HomePageUrl;
 1044            }
 1045
 1321046            if (replaceData || string.IsNullOrEmpty(target.OriginalLanguage))
 1047            {
 1321048                target.OriginalLanguage = source.OriginalLanguage;
 1049            }
 1050
 1321051            if (replaceData || !target.CommunityRating.HasValue)
 1052            {
 1311053                target.CommunityRating = source.CommunityRating;
 1054            }
 1055
 1321056            if (replaceData || !target.EndDate.HasValue)
 1057            {
 1311058                target.EndDate = source.EndDate;
 1059            }
 1060
 1321061            if (!lockedFields.Contains(MetadataField.Genres))
 1062            {
 1301063                if (replaceData || target.Genres.Length == 0)
 1064                {
 1291065                    target.Genres = source.Genres;
 1066                }
 1067            }
 1068
 1321069            if (replaceData || !target.IndexNumber.HasValue)
 1070            {
 1311071                target.IndexNumber = source.IndexNumber;
 1072            }
 1073
 1321074            if (!lockedFields.Contains(MetadataField.OfficialRating))
 1075            {
 1291076                if (replaceData || string.IsNullOrEmpty(target.OfficialRating))
 1077                {
 1281078                    target.OfficialRating = source.OfficialRating;
 1079                }
 1080            }
 1081
 1321082            if (replaceData || string.IsNullOrEmpty(target.CustomRating))
 1083            {
 1311084                target.CustomRating = source.CustomRating;
 1085            }
 1086
 1321087            if (replaceData || string.IsNullOrEmpty(target.Tagline))
 1088            {
 1311089                target.Tagline = source.Tagline;
 1090            }
 1091
 1321092            if (!lockedFields.Contains(MetadataField.Overview))
 1093            {
 1291094                if (replaceData || string.IsNullOrEmpty(target.Overview))
 1095                {
 1281096                    target.Overview = source.Overview;
 1097                }
 1098            }
 1099
 1321100            if (replaceData || !target.ParentIndexNumber.HasValue)
 1101            {
 1281102                target.ParentIndexNumber = source.ParentIndexNumber;
 1103            }
 1104
 1321105            if (!lockedFields.Contains(MetadataField.Cast))
 1106            {
 1311107                if (replaceData || targetResult.People is null || targetResult.People.Count == 0)
 1108                {
 1271109                    targetResult.People = sourceResult.People;
 1110                }
 41111                else if (sourceResult.People is not null && sourceResult.People.Count > 0)
 1112                {
 41113                    MergePeople(sourceResult.People, targetResult.People);
 1114                }
 1115            }
 1116
 1321117            if (replaceData || !target.PremiereDate.HasValue)
 1118            {
 1311119                target.PremiereDate = source.PremiereDate;
 1120            }
 1121
 1321122            if (replaceData || target.ProductionYear is null)
 1123            {
 1311124                target.ProductionYear = source.ProductionYear;
 1125            }
 1126
 1321127            if (!lockedFields.Contains(MetadataField.Runtime))
 1128            {
 1321129                if (replaceData || !target.RunTimeTicks.HasValue)
 1130                {
 1321131                    if (target is not Audio && target is not Video && target is not Book)
 1132                    {
 491133                        target.RunTimeTicks = source.RunTimeTicks;
 1134                    }
 1135                }
 1136            }
 1137
 1321138            if (!lockedFields.Contains(MetadataField.Studios))
 1139            {
 1301140                if (replaceData || target.Studios.Length == 0)
 1141                {
 1291142                    target.Studios = source.Studios;
 1143                }
 1144                else
 1145                {
 11146                    target.Studios = target.Studios.Concat(source.Studios).Distinct(StringComparer.OrdinalIgnoreCase).To
 1147                }
 1148            }
 1149
 1321150            if (!lockedFields.Contains(MetadataField.Tags))
 1151            {
 1301152                if (replaceData || target.Tags.Length == 0)
 1153                {
 1291154                    target.Tags = source.Tags;
 1155                }
 1156                else
 1157                {
 11158                    target.Tags = target.Tags.Concat(source.Tags).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 1159                }
 1160            }
 1161
 1321162            if (!lockedFields.Contains(MetadataField.ProductionLocations))
 1163            {
 1301164                if (replaceData || target.ProductionLocations.Length == 0)
 1165                {
 1291166                    target.ProductionLocations = source.ProductionLocations;
 1167                }
 1168                else
 1169                {
 11170                    target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct(
 1171                }
 1172            }
 1173
 2721174            foreach (var id in source.ProviderIds)
 1175            {
 41176                var key = id.Key;
 1177
 1178                // Don't replace existing Id's.
 41179                if (replaceData)
 1180                {
 11181                    target.ProviderIds[key] = id.Value;
 1182                }
 1183                else
 1184                {
 31185                    target.ProviderIds.TryAdd(key, id.Value);
 1186                }
 1187            }
 1188
 1321189            if (replaceData || !target.CriticRating.HasValue)
 1190            {
 1311191                target.CriticRating = source.CriticRating;
 1192            }
 1193
 1321194            if (replaceData || target.RemoteTrailers.Count == 0)
 1195            {
 1311196                target.RemoteTrailers = source.RemoteTrailers;
 1197            }
 1198            else
 1199            {
 11200                target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).DistinctBy(t => t.Url).ToArr
 1201            }
 1202
 1321203            MergeAlbumArtist(source, target, replaceData);
 1321204            MergeVideoInfo(source, target, replaceData);
 1321205            MergeDisplayOrder(source, target, replaceData);
 1206
 1321207            if (replaceData || string.IsNullOrEmpty(target.ForcedSortName))
 1208            {
 1311209                var forcedSortName = source.ForcedSortName;
 1311210                if (!string.IsNullOrEmpty(forcedSortName))
 1211                {
 31212                    target.ForcedSortName = forcedSortName;
 1213                }
 1214            }
 1215
 1321216            if (mergeMetadataSettings)
 1217            {
 41218                if (replaceData || !target.IsLocked)
 1219                {
 41220                    target.IsLocked = target.IsLocked || source.IsLocked;
 1221                }
 1222
 41223                if (target.LockedFields.Length == 0)
 1224                {
 21225                    target.LockedFields = source.LockedFields;
 1226                }
 1227                else
 1228                {
 21229                    target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray();
 1230                }
 1231
 41232                if (source.DateCreated != DateTime.MinValue)
 1233                {
 11234                    target.DateCreated = source.DateCreated;
 1235                }
 1236
 41237                if (replaceData || source.DateModified != DateTime.MinValue)
 1238                {
 21239                    target.DateModified = source.DateModified;
 1240                }
 1241
 41242                if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode))
 1243                {
 41244                    target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
 1245                }
 1246
 41247                if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataLanguage))
 1248                {
 41249                    target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
 1250                }
 1251            }
 1321252        }
 1253
 1254        private static void MergePeople(IReadOnlyList<PersonInfo> source, IReadOnlyList<PersonInfo> target)
 1255        {
 41256            var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
 41257            var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase);
 1258
 161259            foreach (var name in targetByName.Select(g => g.Key))
 1260            {
 41261                var targetPeople = targetByName[name].ToArray();
 41262                var sourcePeople = sourceByName[name].ToArray();
 1263
 41264                if (sourcePeople.Length == 0)
 1265                {
 1266                    continue;
 1267                }
 1268
 121269                for (int i = 0; i < targetPeople.Length; i++)
 1270                {
 31271                    var person = targetPeople[i];
 31272                    var personInSource = i < sourcePeople.Length ? sourcePeople[i] : sourcePeople[0];
 1273
 101274                    foreach (var providerId in personInSource.ProviderIds)
 1275                    {
 21276                        person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
 1277                    }
 1278
 31279                    if (string.IsNullOrWhiteSpace(person.ImageUrl))
 1280                    {
 21281                        person.ImageUrl = personInSource.ImageUrl;
 1282                    }
 1283
 31284                    if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role))
 1285                    {
 01286                        person.Role = personInSource.Role;
 1287                    }
 1288
 31289                    if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue)
 1290                    {
 01291                        person.SortOrder = personInSource.SortOrder;
 1292                    }
 1293                }
 1294            }
 41295        }
 1296
 1297        private static void MergeDisplayOrder(BaseItem source, BaseItem target, bool replaceData)
 1298        {
 1321299            if (source is IHasDisplayOrder sourceHasDisplayOrder
 1321300                && target is IHasDisplayOrder targetHasDisplayOrder)
 1301            {
 491302                if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder))
 1303                {
 481304                    var displayOrder = sourceHasDisplayOrder.DisplayOrder;
 481305                    if (!string.IsNullOrWhiteSpace(displayOrder))
 1306                    {
 31307                        targetHasDisplayOrder.DisplayOrder = displayOrder;
 1308                    }
 1309                }
 1310            }
 1321311        }
 1312
 1313        private static void MergeAlbumArtist(BaseItem source, BaseItem target, bool replaceData)
 1314        {
 1321315            if (source is IHasAlbumArtist sourceHasAlbumArtist
 1321316                && target is IHasAlbumArtist targetHasAlbumArtist)
 1317            {
 281318                if (replaceData || targetHasAlbumArtist.AlbumArtists.Count == 0)
 1319                {
 271320                    targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
 1321                }
 11322                else if (sourceHasAlbumArtist.AlbumArtists.Count > 0)
 1323                {
 11324                    targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.Al
 1325                }
 1326            }
 1051327        }
 1328
 1329        private static void MergeVideoInfo(BaseItem source, BaseItem target, bool replaceData)
 1330        {
 1321331            if (source is Video sourceCast && target is Video targetCast)
 1332            {
 551333                if (sourceCast.Video3DFormat.HasValue && (replaceData || !targetCast.Video3DFormat.HasValue))
 1334                {
 21335                    targetCast.Video3DFormat = sourceCast.Video3DFormat;
 1336                }
 1337            }
 1321338        }
 1339    }
 1340}

Methods/Properties

.ctor(MediaBrowser.Controller.Configuration.IServerConfigurationManager,Microsoft.Extensions.Logging.ILogger`1<MediaBrowser.Providers.Manager.MetadataService`2<TItemType,TIdType>>,MediaBrowser.Controller.Providers.IProviderManager,MediaBrowser.Model.IO.IFileSystem,MediaBrowser.Controller.Library.ILibraryManager,MediaBrowser.Controller.IO.IExternalDataManager,MediaBrowser.Controller.Persistence.IItemRepository)
get_EnableUpdatingPremiereDateFromChildren()
get_EnableUpdatingGenresFromChildren()
get_EnableUpdatingStudiosFromChildren()
get_EnableUpdatingOfficialRatingFromChildren()
get_Order()
TryGetFileSystemMetadata(System.String,MediaBrowser.Controller.Providers.IDirectoryService)
RefreshMetadata()
ApplySearchResult(MediaBrowser.Controller.Providers.ItemLookupInfo,MediaBrowser.Model.Providers.RemoteSearchResult)
SaveItemAsync()
AfterMetadataRefresh(TItemType,MediaBrowser.Controller.Providers.MetadataRefreshOptions,System.Threading.CancellationToken)
BeforeSave()
BeforeSaveInternal(TItemType,System.Boolean,MediaBrowser.Controller.Library.ItemUpdateType)
EnableUpdateMetadataFromChildren(TItemType,System.Boolean,MediaBrowser.Controller.Library.ItemUpdateType)
GetChildrenForMetadataUpdates(TItemType)
UpdateMetadataFromChildren(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>,System.Boolean,MediaBrowser.Controller.Library.ItemUpdateType)
UpdateCumulativeRunTimeTicks(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
UpdateDateLastMediaAdded(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
UpdatePremiereDate(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
UpdateGenres(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
UpdateStudios(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
UpdateOfficialRating(TItemType,System.Collections.Generic.IReadOnlyList`1<MediaBrowser.Controller.Entities.BaseItem>)
GetProviders(MediaBrowser.Controller.Entities.BaseItem,MediaBrowser.Model.Configuration.LibraryOptions,MediaBrowser.Controller.Providers.MetadataRefreshOptions,System.Boolean,System.Boolean)
GetNonLocalImageProviders(MediaBrowser.Controller.Entities.BaseItem,System.Collections.Generic.IEnumerable`1<MediaBrowser.Controller.Providers.IImageProvider>,MediaBrowser.Controller.Providers.MetadataRefreshOptions)
CanRefresh(MediaBrowser.Controller.Entities.BaseItem)
CanRefreshPrimary(System.Type)
RefreshWithProviders()
RunCustomProvider()
CreateNew()
ExecuteRemoteProviders()
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)