diff options
Diffstat (limited to 'MediaBrowser.Providers')
20 files changed, 778 insertions, 696 deletions
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index d82716831..9a676cb2e 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -14,6 +14,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; @@ -100,8 +101,8 @@ namespace MediaBrowser.Providers.Manager { saveLocally = false; - // If season is virtual under a physical series, save locally if using compatible convention - if (item is Season season && _config.Configuration.ImageSavingConvention == ImageSavingConvention.Compatible) + // If season is virtual under a physical series, save locally + if (item is Season season) { var series = season.Series; @@ -126,7 +127,11 @@ namespace MediaBrowser.Providers.Manager var paths = GetSavePaths(item, type, imageIndex, mimeType, saveLocally); - var retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false); + string[] retryPaths = []; + if (saveLocally) + { + retryPaths = GetSavePaths(item, type, imageIndex, mimeType, false); + } // If there are more than one output paths, the stream will need to be seekable if (paths.Length > 1 && !source.CanSeek) @@ -183,6 +188,29 @@ namespace MediaBrowser.Providers.Manager try { _fileSystem.DeleteFile(currentPath); + + // Remove local episode metadata directory if it exists and is empty + var directory = Path.GetDirectoryName(currentPath); + if (item is Episode && directory.Equals("metadata", StringComparison.Ordinal)) + { + var parentDirectoryPath = Directory.GetParent(currentPath).FullName; + if (_fileSystem.DirectoryExists(parentDirectoryPath) && !_fileSystem.GetFiles(parentDirectoryPath).Any()) + { + try + { + _logger.LogInformation("Deleting empty local metadata folder {Folder}", parentDirectoryPath); + Directory.Delete(parentDirectoryPath); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogError(ex, "Error deleting directory {Path}", parentDirectoryPath); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting directory {Path}", parentDirectoryPath); + } + } + } } catch (FileNotFoundException) { @@ -374,6 +402,47 @@ namespace MediaBrowser.Providers.Manager throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unable to determine image file extension from mime type {0}", mimeType)); } + if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase)) + { + extension = ".jpg"; + } + + extension = extension.ToLowerInvariant(); + + if (type == ImageType.Primary && saveLocally) + { + if (season is not null && season.IndexNumber.HasValue) + { + var seriesFolder = season.SeriesPath; + + var seasonMarker = season.IndexNumber.Value == 0 + ? "-specials" + : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); + + var imageFilename = "season" + seasonMarker + "-poster" + extension; + + return Path.Combine(seriesFolder, imageFilename); + } + } + + if (type == ImageType.Backdrop && saveLocally) + { + if (season is not null + && season.IndexNumber.HasValue + && (imageIndex is null || imageIndex == 0)) + { + var seriesFolder = season.SeriesPath; + + var seasonMarker = season.IndexNumber.Value == 0 + ? "-specials" + : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); + + var imageFilename = "season" + seasonMarker + "-fanart" + extension; + + return Path.Combine(seriesFolder, imageFilename); + } + } + if (type == ImageType.Thumb && saveLocally) { if (season is not null && season.IndexNumber.HasValue) @@ -447,20 +516,12 @@ namespace MediaBrowser.Providers.Manager break; } - if (string.Equals(extension, ".jpeg", StringComparison.OrdinalIgnoreCase)) - { - extension = ".jpg"; - } - - extension = extension.ToLowerInvariant(); - string path = null; - if (saveLocally) { if (type == ImageType.Primary && item is Episode) { - path = Path.Combine(Path.GetDirectoryName(item.Path), "metadata", filename + extension); + path = Path.Combine(Path.GetDirectoryName(item.Path), filename + "-thumb" + extension); } else if (item.IsInMixedFolder) { diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 1a5dbd7a5..1bb7ffcce 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Providers; @@ -96,7 +97,7 @@ namespace MediaBrowser.Providers.Manager public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions) { var hasChanges = false; - IDirectoryService directoryService = refreshOptions?.DirectoryService; + var directoryService = refreshOptions?.DirectoryService; if (item is not Photo) { @@ -158,7 +159,7 @@ namespace MediaBrowser.Providers.Manager } } - // only delete existing multi-images if new ones were added + // Only delete existing multi-images if new ones were added if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count()) { PruneImages(item, oldBackdropImages); @@ -359,10 +360,8 @@ namespace MediaBrowser.Providers.Manager private void PruneImages(BaseItem item, IReadOnlyList<ItemImageInfo> images) { - for (var i = 0; i < images.Count; i++) + foreach (var image in images) { - var image = images[i]; - if (image.IsLocalFile) { try @@ -371,7 +370,7 @@ namespace MediaBrowser.Providers.Manager } catch (FileNotFoundException) { - // nothing to do, already gone + // Nothing to do, already gone } catch (UnauthorizedAccessException ex) { @@ -381,6 +380,16 @@ namespace MediaBrowser.Providers.Manager } item.RemoveImages(images); + + // Cleanup old metadata directory for episodes if empty + if (item is Episode) + { + var oldLocalMetadataDirectory = Path.Combine(item.ContainingFolderPath, "metadata"); + if (_fileSystem.DirectoryExists(oldLocalMetadataDirectory) && !_fileSystem.GetFiles(oldLocalMetadataDirectory).Any()) + { + Directory.Delete(oldLocalMetadataDirectory); + } + } } /// <summary> @@ -413,12 +422,10 @@ namespace MediaBrowser.Providers.Manager { var changed = item.ValidateImages(); var foundImageTypes = new List<ImageType>(); - for (var i = 0; i < _singularImages.Length; i++) { var type = _singularImages[i]; var image = GetFirstLocalImageInfoByType(images, type); - if (image is not null) { var currentImage = item.GetImageInfo(type, 0); diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 6f473fc07..8af4ed2a8 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -72,7 +71,7 @@ namespace MediaBrowser.Providers.Manager } } - public async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) + public virtual async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) { var itemOfType = (TItemType)item; @@ -93,10 +92,6 @@ namespace MediaBrowser.Providers.Manager } } - var localImagesFailed = false; - - var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList(); - if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages) { if (ImageProvider.RemoveImages(item)) @@ -105,24 +100,35 @@ namespace MediaBrowser.Providers.Manager } } - // Start by validating images - try + var localImagesFailed = false; + var allImageProviders = ProviderManager.GetImageProviders(item, refreshOptions).ToList(); + + // Only validate already registered images if we are replacing and saving locally + if (item.IsSaveLocalMetadataEnabled() && refreshOptions.ReplaceAllImages) { - // Always validate images and check for new locally stored ones. - if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions)) - { - updateType |= ItemUpdateType.ImageUpdate; - } + item.ValidateImages(); } - catch (Exception ex) + else { - localImagesFailed = true; - Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name"); + // Run full image validation and register new local images + try + { + if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions)) + { + updateType |= ItemUpdateType.ImageUpdate; + } + } + catch (Exception ex) + { + localImagesFailed = true; + Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name"); + } } var metadataResult = new MetadataResult<TItemType> { - Item = itemOfType + Item = itemOfType, + People = LibraryManager.GetPeople(item) }; bool hasRefreshedMetadata = true; @@ -154,7 +160,8 @@ namespace MediaBrowser.Providers.Manager id.IsAutomated = refreshOptions.IsAutomated; - var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, cancellationToken).ConfigureAwait(false); + var hasMetadataSavers = ProviderManager.GetMetadataSavers(item, libraryOptions).Any(); + var result = await RefreshWithProviders(metadataResult, id, refreshOptions, providers, ImageProvider, hasMetadataSavers, cancellationToken).ConfigureAwait(false); updateType |= result.UpdateType; if (result.Failures > 0) @@ -165,7 +172,7 @@ namespace MediaBrowser.Providers.Manager } // Next run remote image providers, but only if local image providers didn't throw an exception - if (!localImagesFailed && refreshOptions.ImageRefreshMode != MetadataRefreshMode.ValidationOnly) + if (!localImagesFailed && refreshOptions.ImageRefreshMode > MetadataRefreshMode.ValidationOnly) { var providers = GetNonLocalImageProviders(item, allImageProviders, refreshOptions).ToList(); @@ -243,7 +250,7 @@ namespace MediaBrowser.Providers.Manager protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken) { - if (result.Item.SupportsPeople && result.People is not null) + if (result.Item.SupportsPeople) { var baseItem = result.Item; @@ -399,7 +406,8 @@ namespace MediaBrowser.Providers.Manager foreach (var child in children) { - if (!child.IsFolder) + // Exclude any folders and virtual items since they are only placeholders + if (!child.IsFolder && !child.IsVirtualItem) { var childDateCreated = child.DateCreated; if (childDateCreated > dateLastMediaAdded) @@ -638,6 +646,7 @@ namespace MediaBrowser.Providers.Manager MetadataRefreshOptions options, ICollection<IMetadataProvider> providers, ItemImageProvider imageService, + bool isSavingMetadata, CancellationToken cancellationToken) { var refreshResult = new RefreshResult @@ -655,102 +664,96 @@ namespace MediaBrowser.Providers.Manager await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false); } + if (item.IsLocked) + { + return refreshResult; + } + var temp = new MetadataResult<TItemType> { Item = CreateNew() }; temp.Item.Path = item.Path; + temp.Item.Id = item.Id; + temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode; + temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage; - // If replacing all metadata, run internet providers first - if (options.ReplaceAllMetadata) - { - var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken) - .ConfigureAwait(false); - - refreshResult.UpdateType |= remoteResult.UpdateType; - refreshResult.ErrorMessage = remoteResult.ErrorMessage; - refreshResult.Failures += remoteResult.Failures; - } - - var hasLocalMetadata = false; var foundImageTypes = new List<ImageType>(); - foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>()) + // Do not execute local providers if we are identifying or replacing with local metadata saving enabled + if (options.SearchResult is null && !(isSavingMetadata && options.ReplaceAllMetadata)) { - var providerName = provider.GetType().Name; - Logger.LogDebug("Running {Provider} for {Item}", providerName, logName); - - var itemInfo = new ItemInfo(item); - - try + foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>()) { - var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false); + var providerName = provider.GetType().Name; + Logger.LogDebug("Running {Provider} for {Item}", providerName, logName); + + var itemInfo = new ItemInfo(item); - if (localItem.HasMetadata) + try { - foreach (var remoteImage in localItem.RemoteImages) + var localItem = await provider.GetMetadata(itemInfo, options.DirectoryService, cancellationToken).ConfigureAwait(false); + + if (localItem.HasMetadata) { - try + foreach (var remoteImage in localItem.RemoteImages) { - if (item.ImageInfos.Any(x => x.Type == remoteImage.Type) - && !options.IsReplacingImage(remoteImage.Type)) + try { - continue; - } + if (item.ImageInfos.Any(x => x.Type == remoteImage.Type) + && !options.IsReplacingImage(remoteImage.Type)) + { + continue; + } - await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false); - refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; + await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false); + refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; - // remember imagetype that has just been downloaded - foundImageTypes.Add(remoteImage.Type); + // remember imagetype that has just been downloaded + foundImageTypes.Add(remoteImage.Type); + } + catch (HttpRequestException ex) + { + Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url); + } } - catch (HttpRequestException ex) + + if (foundImageTypes.Count > 0) { - Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.Type), remoteImage.Url); + imageService.UpdateReplaceImages(options, foundImageTypes); } - } - if (foundImageTypes.Count > 0) - { - imageService.UpdateReplaceImages(options, foundImageTypes); - } - - if (imageService.MergeImages(item, localItem.Images, options)) - { - refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; - } + if (imageService.MergeImages(item, localItem.Images, options)) + { + refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; + } - MergeData(localItem, temp, Array.Empty<MetadataField>(), options.ReplaceAllMetadata, true); - refreshResult.UpdateType |= ItemUpdateType.MetadataImport; + MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true); + refreshResult.UpdateType |= ItemUpdateType.MetadataImport; - // Only one local provider allowed per item - if (item.IsLocked || localItem.Item.IsLocked || IsFullLocalMetadata(localItem.Item)) - { - hasLocalMetadata = true; + break; } - break; + Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName); } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + Logger.LogError(ex, "Error in {Provider}", provider.Name); - Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - Logger.LogError(ex, "Error in {Provider}", provider.Name); - - // If a local provider fails, consider that a failure - refreshResult.ErrorMessage = ex.Message; + // If a local provider fails, consider that a failure + refreshResult.ErrorMessage = ex.Message; + } } } - // Local metadata is king - if any is found don't run remote providers - if (!options.ReplaceAllMetadata && (!hasLocalMetadata || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || !item.StopRefreshIfLocalMetadataFound)) + var isLocalLocked = temp.Item.IsLocked; + if (!isLocalLocked && (options.ReplaceAllMetadata || options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly)) { - var remoteResult = await ExecuteRemoteProviders(temp, logName, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken) + var remoteResult = await ExecuteRemoteProviders(temp, logName, false, id, providers.OfType<IRemoteMetadataProvider<TItemType, TIdType>>(), cancellationToken) .ConfigureAwait(false); refreshResult.UpdateType |= remoteResult.UpdateType; @@ -762,19 +765,20 @@ namespace MediaBrowser.Providers.Manager { if (refreshResult.UpdateType > ItemUpdateType.None) { - if (hasLocalMetadata) + if (!options.RemoveOldMetadata) + { + // Add existing metadata to provider result if it does not exist there + MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false); + } + + if (isLocalLocked) { MergeData(temp, metadata, item.LockedFields, true, true); } else { - if (!options.RemoveOldMetadata) - { - MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false); - } - - // Will always replace all metadata when Scan for new and updated files is used. Else, follow the options. - MergeData(temp, metadata, item.LockedFields, options.MetadataRefreshMode == MetadataRefreshMode.Default || options.ReplaceAllMetadata, false); + var shouldReplace = options.MetadataRefreshMode > MetadataRefreshMode.ValidationOnly || options.ReplaceAllMetadata; + MergeData(temp, metadata, item.LockedFields, shouldReplace, true); } } } @@ -787,16 +791,6 @@ namespace MediaBrowser.Providers.Manager return refreshResult; } - protected virtual bool IsFullLocalMetadata(TItemType item) - { - if (string.IsNullOrWhiteSpace(item.Name)) - { - return false; - } - - return true; - } - private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, string logName, MetadataRefreshOptions options, RefreshResult refreshResult, CancellationToken cancellationToken) { Logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, logName); @@ -821,23 +815,20 @@ namespace MediaBrowser.Providers.Manager return new TItemType(); } - private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, TIdType id, IEnumerable<IRemoteMetadataProvider<TItemType, TIdType>> providers, CancellationToken cancellationToken) + private async Task<RefreshResult> ExecuteRemoteProviders(MetadataResult<TItemType> temp, string logName, bool replaceData, TIdType id, IEnumerable<IRemoteMetadataProvider<TItemType, TIdType>> providers, CancellationToken cancellationToken) { var refreshResult = new RefreshResult(); - var tmpDataMerged = false; + if (id is not null) + { + MergeNewData(temp.Item, id); + } foreach (var provider in providers) { var providerName = provider.GetType().Name; Logger.LogDebug("Running {Provider} for {Item}", providerName, logName); - if (id is not null && !tmpDataMerged) - { - MergeNewData(temp.Item, id); - tmpDataMerged = true; - } - try { var result = await provider.GetMetadata(id, cancellationToken).ConfigureAwait(false); @@ -846,7 +837,7 @@ namespace MediaBrowser.Providers.Manager { result.Provider = provider.Name; - MergeData(result, temp, Array.Empty<MetadataField>(), false, false); + MergeData(result, temp, Array.Empty<MetadataField>(), replaceData, false); MergeNewData(temp.Item, id); refreshResult.UpdateType |= ItemUpdateType.MetadataDownload; @@ -949,11 +940,7 @@ namespace MediaBrowser.Providers.Manager if (replaceData || string.IsNullOrEmpty(target.OriginalTitle)) { - // Safeguard against incoming data having an empty name - if (!string.IsNullOrWhiteSpace(source.OriginalTitle)) - { - target.OriginalTitle = source.OriginalTitle; - } + target.OriginalTitle = source.OriginalTitle; } if (replaceData || !target.CommunityRating.HasValue) @@ -1016,7 +1003,7 @@ namespace MediaBrowser.Providers.Manager { targetResult.People = sourceResult.People; } - else if (targetResult.People is not null && sourceResult.People is not null) + else if (sourceResult.People is not null && sourceResult.People.Count > 0) { MergePeople(sourceResult.People, targetResult.People); } @@ -1049,6 +1036,10 @@ namespace MediaBrowser.Providers.Manager { target.Studios = source.Studios; } + else + { + target.Studios = target.Studios.Concat(source.Studios).Distinct().ToArray(); + } } if (!lockedFields.Contains(MetadataField.Tags)) @@ -1057,6 +1048,10 @@ namespace MediaBrowser.Providers.Manager { target.Tags = source.Tags; } + else + { + target.Tags = target.Tags.Concat(source.Tags).Distinct().ToArray(); + } } if (!lockedFields.Contains(MetadataField.ProductionLocations)) @@ -1065,6 +1060,10 @@ namespace MediaBrowser.Providers.Manager { target.ProductionLocations = source.ProductionLocations; } + else + { + target.ProductionLocations = target.ProductionLocations.Concat(source.ProductionLocations).Distinct().ToArray(); + } } foreach (var id in source.ProviderIds) @@ -1082,17 +1081,28 @@ namespace MediaBrowser.Providers.Manager } } + if (replaceData || !target.CriticRating.HasValue) + { + target.CriticRating = source.CriticRating; + } + + if (replaceData || target.RemoteTrailers.Count == 0) + { + target.RemoteTrailers = source.RemoteTrailers; + } + else + { + target.RemoteTrailers = target.RemoteTrailers.Concat(source.RemoteTrailers).DistinctBy(t => t.Url).ToArray(); + } + MergeAlbumArtist(source, target, replaceData); - MergeCriticRating(source, target, replaceData); - MergeTrailers(source, target, replaceData); MergeVideoInfo(source, target, replaceData); MergeDisplayOrder(source, target, replaceData); if (replaceData || string.IsNullOrEmpty(target.ForcedSortName)) { var forcedSortName = source.ForcedSortName; - - if (!string.IsNullOrWhiteSpace(forcedSortName)) + if (!string.IsNullOrEmpty(forcedSortName)) { target.ForcedSortName = forcedSortName; } @@ -1100,22 +1110,44 @@ namespace MediaBrowser.Providers.Manager if (mergeMetadataSettings) { - target.LockedFields = source.LockedFields; - target.IsLocked = source.IsLocked; + if (replaceData || !target.IsLocked) + { + target.IsLocked = target.IsLocked || source.IsLocked; + } + + if (target.LockedFields.Length == 0) + { + target.LockedFields = source.LockedFields; + } + else + { + target.LockedFields = target.LockedFields.Concat(source.LockedFields).Distinct().ToArray(); + } - // Grab the value if it's there, but if not then don't overwrite with the default if (source.DateCreated != default) { target.DateCreated = source.DateCreated; } - target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode; - target.PreferredMetadataLanguage = source.PreferredMetadataLanguage; + if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataCountryCode)) + { + target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode; + } + + if (replaceData || string.IsNullOrEmpty(target.PreferredMetadataLanguage)) + { + target.PreferredMetadataLanguage = source.PreferredMetadataLanguage; + } } } private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target) { + if (target is null) + { + target = new List<PersonInfo>(); + } + foreach (var person in target) { var normalizedName = person.Name.RemoveDiacritics(); @@ -1144,7 +1176,6 @@ namespace MediaBrowser.Providers.Manager if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder)) { var displayOrder = sourceHasDisplayOrder.DisplayOrder; - if (!string.IsNullOrWhiteSpace(displayOrder)) { targetHasDisplayOrder.DisplayOrder = displayOrder; @@ -1162,22 +1193,10 @@ namespace MediaBrowser.Providers.Manager { targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists; } - } - } - - private static void MergeCriticRating(BaseItem source, BaseItem target, bool replaceData) - { - if (replaceData || !target.CriticRating.HasValue) - { - target.CriticRating = source.CriticRating; - } - } - - private static void MergeTrailers(BaseItem source, BaseItem target, bool replaceData) - { - if (replaceData || target.RemoteTrailers.Count == 0) - { - target.RemoteTrailers = source.RemoteTrailers; + else if (sourceHasAlbumArtist.AlbumArtists.Count > 0) + { + targetHasAlbumArtist.AlbumArtists = targetHasAlbumArtist.AlbumArtists.Concat(sourceHasAlbumArtist.AlbumArtists).Distinct().ToArray(); + } } } @@ -1185,7 +1204,7 @@ namespace MediaBrowser.Providers.Manager { if (source is Video sourceCast && target is Video targetCast) { - if (replaceData || targetCast.Video3DFormat is null) + if (replaceData || !targetCast.Video3DFormat.HasValue) { targetCast.Video3DFormat = sourceCast.Video3DFormat; } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index f34034964..60d89a51b 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -69,11 +69,12 @@ namespace MediaBrowser.Providers.Manager o.PoolInitialFill = 1; }); - private IImageProvider[] _imageProviders = Array.Empty<IImageProvider>(); - private IMetadataService[] _metadataServices = Array.Empty<IMetadataService>(); - private IMetadataProvider[] _metadataProviders = Array.Empty<IMetadataProvider>(); - private IMetadataSaver[] _savers = Array.Empty<IMetadataSaver>(); - private IExternalId[] _externalIds = Array.Empty<IExternalId>(); + private IImageProvider[] _imageProviders = []; + private IMetadataService[] _metadataServices = []; + private IMetadataProvider[] _metadataProviders = []; + private IMetadataSaver[] _savers = []; + private IExternalId[] _externalIds = []; + private IExternalUrlProvider[] _externalUrlProviders = []; private bool _isProcessingRefreshQueue; private bool _disposed; @@ -132,12 +133,14 @@ namespace MediaBrowser.Providers.Manager IEnumerable<IMetadataService> metadataServices, IEnumerable<IMetadataProvider> metadataProviders, IEnumerable<IMetadataSaver> metadataSavers, - IEnumerable<IExternalId> externalIds) + IEnumerable<IExternalId> externalIds, + IEnumerable<IExternalUrlProvider> externalUrlProviders) { _imageProviders = imageProviders.ToArray(); _metadataServices = metadataServices.OrderBy(i => i.Order).ToArray(); _metadataProviders = metadataProviders.ToArray(); _externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray(); + _externalUrlProviders = externalUrlProviders.OrderBy(i => i.Name).ToArray(); _savers = metadataSavers.ToArray(); } @@ -286,7 +289,7 @@ namespace MediaBrowser.Providers.Manager var results = await Task.WhenAll(tasks).ConfigureAwait(false); - return results.SelectMany(i => i.ToList()); + return results.SelectMany(i => i); } /// <summary> @@ -418,6 +421,12 @@ namespace MediaBrowser.Providers.Manager return GetMetadataProvidersInternal<T>(item, libraryOptions, globalMetadataOptions, false, false); } + /// <inheritdoc /> + public IEnumerable<IMetadataSaver> GetMetadataSavers(BaseItem item, LibraryOptions libraryOptions) + { + return _savers.Where(i => IsSaverEnabledForItem(i, item, libraryOptions, ItemUpdateType.MetadataEdit, false)); + } + private IEnumerable<IMetadataProvider<T>> GetMetadataProvidersInternal<T>(BaseItem item, LibraryOptions libraryOptions, MetadataOptions globalMetadataOptions, bool includeDisabled, bool forceEnableInternetMetadata) where T : BaseItem { @@ -871,31 +880,35 @@ namespace MediaBrowser.Providers.Manager /// <inheritdoc/> public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item) { - return GetExternalIds(item) +#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11 + var legacyExternalIdUrls = GetExternalIds(item) .Select(i => - { - if (string.IsNullOrEmpty(i.UrlFormatString)) { - return null; - } + var urlFormatString = i.UrlFormatString; + if (string.IsNullOrEmpty(urlFormatString) + || !item.TryGetProviderId(i.Key, out var providerId)) + { + return null; + } - var value = item.GetProviderId(i.Key); + return new ExternalUrl + { + Name = i.ProviderName, + Url = string.Format( + CultureInfo.InvariantCulture, + urlFormatString, + providerId) + }; + }) + .OfType<ExternalUrl>(); +#pragma warning restore CS0618 // Type or member is obsolete - if (string.IsNullOrEmpty(value)) - { - return null; - } + var externalUrls = _externalUrlProviders + .SelectMany(p => p + .GetExternalUrls(item) + .Select(externalUrl => new ExternalUrl { Name = p.Name, Url = externalUrl })); - return new ExternalUrl - { - Name = i.ProviderName, - Url = string.Format( - CultureInfo.InvariantCulture, - i.UrlFormatString, - value) - }; - }).Where(i => i is not null) - .Concat(item.GetRelatedUrls())!; // We just filtered out all the nulls + return legacyExternalIdUrls.Concat(externalUrls).OrderBy(u => u.Name); } /// <inheritdoc/> @@ -906,7 +919,9 @@ namespace MediaBrowser.Providers.Manager name: i.ProviderName, key: i.Key, type: i.Type, +#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11 urlFormatString: i.UrlFormatString)); +#pragma warning restore CS0618 // Type or member is obsolete } /// <inheritdoc/> @@ -968,16 +983,13 @@ namespace MediaBrowser.Providers.Manager var id = item.Id; _logger.LogDebug("OnRefreshProgress {Id:N} {Progress}", id, progress); - // TODO: Need to hunt down the conditions for this happening - _activeRefreshes.AddOrUpdate( - id, - _ => throw new InvalidOperationException( - string.Format( - CultureInfo.InvariantCulture, - "Cannot update refresh progress of item '{0}' ({1}) because a refresh for this item is not running", - item.GetType().Name, - item.Id.ToString("N", CultureInfo.InvariantCulture))), - (_, _) => progress); + if (!_activeRefreshes.TryGetValue(id, out var current) + || progress <= current + || !_activeRefreshes.TryUpdate(id, progress, current)) + { + // Item isn't currently refreshing, or update was received out-of-order, so don't trigger event. + return; + } try { @@ -1106,9 +1118,10 @@ namespace MediaBrowser.Providers.Manager var musicArtists = albums .Select(i => i.MusicArtist) - .Where(i => i is not null); + .Where(i => i is not null) + .Distinct(); - var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new Progress<double>(), options, true, cancellationToken)); + var musicArtistRefreshTasks = musicArtists.Select(i => i.ValidateChildren(new Progress<double>(), options, true, false, cancellationToken)); await Task.WhenAll(musicArtistRefreshTasks).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index c9fe4c9b6..0083d4f75 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.Linq; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -26,15 +25,12 @@ namespace MediaBrowser.Providers.MediaInfo /// <summary> /// Probes audio files for metadata. /// </summary> - public partial class AudioFileProber + public class AudioFileProber { - // Default LUFS value for use with the web interface, at -18db gain will be 1(no db gain). - private const float DefaultLUFSValue = -18; - - private readonly ILogger<AudioFileProber> _logger; private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; private readonly ILibraryManager _libraryManager; + private readonly ILogger<AudioFileProber> _logger; private readonly IMediaSourceManager _mediaSourceManager; private readonly LyricResolver _lyricResolver; private readonly ILyricManager _lyricManager; @@ -58,21 +54,15 @@ namespace MediaBrowser.Providers.MediaInfo LyricResolver lyricResolver, ILyricManager lyricManager) { - _logger = logger; _mediaEncoder = mediaEncoder; _itemRepo = itemRepo; _libraryManager = libraryManager; + _logger = logger; _mediaSourceManager = mediaSourceManager; _lyricResolver = lyricResolver; _lyricManager = lyricManager; } - [GeneratedRegex(@"I:\s+(.*?)\s+LUFS")] - private static partial Regex LUFSRegex(); - - [GeneratedRegex(@"REPLAYGAIN_TRACK_GAIN:\s+-?([0-9.]+)\s+dB")] - private static partial Regex ReplayGainTagRegex(); - /// <summary> /// Probes the specified item for metadata. /// </summary> @@ -115,97 +105,6 @@ namespace MediaBrowser.Providers.MediaInfo await FetchAsync(item, result, options, cancellationToken).ConfigureAwait(false); } - var libraryOptions = _libraryManager.GetLibraryOptions(item); - bool foundLUFSValue = false; - - if (libraryOptions.UseReplayGainTags) - { - using (var process = new Process() - { - StartInfo = new ProcessStartInfo - { - FileName = _mediaEncoder.ProbePath, - Arguments = $"-hide_banner -i \"{path}\"", - RedirectStandardOutput = false, - RedirectStandardError = true - }, - }) - { - try - { - process.Start(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting ffmpeg"); - - throw; - } - - using var reader = process.StandardError; - var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - Match split = ReplayGainTagRegex().Match(output); - - if (split.Success) - { - item.LUFS = DefaultLUFSValue - float.Parse(split.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat); - foundLUFSValue = true; - } - else - { - item.LUFS = DefaultLUFSValue; - } - } - } - - if (libraryOptions.EnableLUFSScan && !foundLUFSValue) - { - using (var process = new Process() - { - StartInfo = new ProcessStartInfo - { - FileName = _mediaEncoder.EncoderPath, - Arguments = $"-hide_banner -i \"{path}\" -af ebur128=framelog=verbose -f null -", - RedirectStandardOutput = false, - RedirectStandardError = true - }, - }) - { - try - { - process.Start(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting ffmpeg"); - - throw; - } - - using var reader = process.StandardError; - var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - MatchCollection split = LUFSRegex().Matches(output); - - if (split.Count != 0) - { - item.LUFS = float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat); - } - else - { - item.LUFS = DefaultLUFSValue; - } - } - } - - if (!libraryOptions.EnableLUFSScan && !libraryOptions.UseReplayGainTags) - { - item.LUFS = DefaultLUFSValue; - } - - _logger.LogDebug("LUFS for {ItemName} is {LUFS}.", item.Name, item.LUFS); - return ItemUpdateType.MetadataImport; } @@ -230,14 +129,20 @@ namespace MediaBrowser.Providers.MediaInfo audio.Size = mediaInfo.Size; audio.PremiereDate = mediaInfo.PremiereDate; + // Add external lyrics first to prevent the lrc file get overwritten on first scan + var mediaStreams = new List<MediaStream>(mediaInfo.MediaStreams); + AddExternalLyrics(audio, mediaStreams, options); + var tryExtractEmbeddedLyrics = mediaStreams.All(s => s.Type != MediaStreamType.Lyric); + if (!audio.IsLocked) { - await FetchDataFromTags(audio, options).ConfigureAwait(false); + await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false); + if (tryExtractEmbeddedLyrics) + { + AddExternalLyrics(audio, mediaStreams, options); + } } - var mediaStreams = new List<MediaStream>(mediaInfo.MediaStreams); - AddExternalLyrics(audio, mediaStreams, options); - audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric); _itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken); @@ -247,177 +152,221 @@ namespace MediaBrowser.Providers.MediaInfo /// Fetches data from the tags. /// </summary> /// <param name="audio">The <see cref="Audio"/>.</param> + /// <param name="mediaInfo">The <see cref="Model.MediaInfo.MediaInfo"/>.</param> /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param> - private async Task FetchDataFromTags(Audio audio, MetadataRefreshOptions options) + /// <param name="tryExtractEmbeddedLyrics">Whether to extract embedded lyrics to lrc file. </param> + private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, bool tryExtractEmbeddedLyrics) { - using var file = TagLib.File.Create(audio.Path); - var tagTypes = file.TagTypesOnDisk; Tag? tags = null; - - if (tagTypes.HasFlag(TagTypes.Id3v2)) - { - tags = file.GetTag(TagTypes.Id3v2); - } - else if (tagTypes.HasFlag(TagTypes.Ape)) - { - tags = file.GetTag(TagTypes.Ape); - } - else if (tagTypes.HasFlag(TagTypes.FlacMetadata)) - { - tags = file.GetTag(TagTypes.FlacMetadata); - } - else if (tagTypes.HasFlag(TagTypes.Apple)) + try { - tags = file.GetTag(TagTypes.Apple); - } - else if (tagTypes.HasFlag(TagTypes.Xiph)) - { - tags = file.GetTag(TagTypes.Xiph); - } - else if (tagTypes.HasFlag(TagTypes.AudibleMetadata)) - { - tags = file.GetTag(TagTypes.AudibleMetadata); + using var file = TagLib.File.Create(audio.Path); + var tagTypes = file.TagTypesOnDisk; + + if (tagTypes.HasFlag(TagTypes.Id3v2)) + { + tags = file.GetTag(TagTypes.Id3v2); + } + else if (tagTypes.HasFlag(TagTypes.Ape)) + { + tags = file.GetTag(TagTypes.Ape); + } + else if (tagTypes.HasFlag(TagTypes.FlacMetadata)) + { + tags = file.GetTag(TagTypes.FlacMetadata); + } + else if (tagTypes.HasFlag(TagTypes.Apple)) + { + tags = file.GetTag(TagTypes.Apple); + } + else if (tagTypes.HasFlag(TagTypes.Xiph)) + { + tags = file.GetTag(TagTypes.Xiph); + } + else if (tagTypes.HasFlag(TagTypes.AudibleMetadata)) + { + tags = file.GetTag(TagTypes.AudibleMetadata); + } + else if (tagTypes.HasFlag(TagTypes.Id3v1)) + { + tags = file.GetTag(TagTypes.Id3v1); + } } - else if (tagTypes.HasFlag(TagTypes.Id3v1)) + catch (Exception e) { - tags = file.GetTag(TagTypes.Id3v1); + _logger.LogWarning(e, "TagLib-Sharp does not support this audio"); } - if (tags is not null) + tags ??= new TagLib.Id3v2.Tag(); + tags.AlbumArtists ??= mediaInfo.AlbumArtists; + tags.Album ??= mediaInfo.Album; + tags.Title ??= mediaInfo.Name; + tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year; + tags.Performers ??= mediaInfo.Artists; + tags.Genres ??= mediaInfo.Genres; + tags.Track = tags.Track == 0U ? Convert.ToUInt32(mediaInfo.IndexNumber, CultureInfo.InvariantCulture) : tags.Track; + tags.Disc = tags.Disc == 0U ? Convert.ToUInt32(mediaInfo.ParentIndexNumber, CultureInfo.InvariantCulture) : tags.Disc; + + if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast)) { - if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast)) + var people = new List<PersonInfo>(); + var albumArtists = tags.AlbumArtists; + foreach (var albumArtist in albumArtists) { - var people = new List<PersonInfo>(); - var albumArtists = tags.AlbumArtists; - foreach (var albumArtist in albumArtists) + if (!string.IsNullOrEmpty(albumArtist)) { - if (!string.IsNullOrEmpty(albumArtist)) + PeopleHelper.AddPerson(people, new PersonInfo { - PeopleHelper.AddPerson(people, new PersonInfo - { - Name = albumArtist, - Type = PersonKind.AlbumArtist - }); - } + Name = albumArtist, + Type = PersonKind.AlbumArtist + }); } + } - var performers = tags.Performers; - foreach (var performer in performers) + var performers = tags.Performers; + foreach (var performer in performers) + { + if (!string.IsNullOrEmpty(performer)) { - if (!string.IsNullOrEmpty(performer)) + PeopleHelper.AddPerson(people, new PersonInfo { - PeopleHelper.AddPerson(people, new PersonInfo - { - Name = performer, - Type = PersonKind.Artist - }); - } + Name = performer, + Type = PersonKind.Artist + }); } + } - foreach (var composer in tags.Composers) + foreach (var composer in tags.Composers) + { + if (!string.IsNullOrEmpty(composer)) { - if (!string.IsNullOrEmpty(composer)) + PeopleHelper.AddPerson(people, new PersonInfo { - PeopleHelper.AddPerson(people, new PersonInfo - { - Name = composer, - Type = PersonKind.Composer - }); - } + Name = composer, + Type = PersonKind.Composer + }); } + } - _libraryManager.UpdatePeople(audio, people); - - if (options.ReplaceAllMetadata && performers.Length != 0) - { - audio.Artists = performers; - } - else if (!options.ReplaceAllMetadata - && (audio.Artists is null || audio.Artists.Count == 0)) - { - audio.Artists = performers; - } + _libraryManager.UpdatePeople(audio, people); - if (options.ReplaceAllMetadata && albumArtists.Length != 0) - { - audio.AlbumArtists = albumArtists; - } - else if (!options.ReplaceAllMetadata - && (audio.AlbumArtists is null || audio.AlbumArtists.Count == 0)) - { - audio.AlbumArtists = albumArtists; - } + if (options.ReplaceAllMetadata && performers.Length != 0) + { + audio.Artists = performers; + } + else if (!options.ReplaceAllMetadata + && (audio.Artists is null || audio.Artists.Count == 0)) + { + audio.Artists = performers; } - if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(tags.Title)) + if (albumArtists.Length == 0) { - audio.Name = tags.Title; + // Album artists not provided, fall back to performers (artists). + albumArtists = performers; } - if (options.ReplaceAllMetadata) + if (options.ReplaceAllMetadata && albumArtists.Length != 0) { - audio.Album = tags.Album; - audio.IndexNumber = Convert.ToInt32(tags.Track); - audio.ParentIndexNumber = Convert.ToInt32(tags.Disc); + audio.AlbumArtists = albumArtists; } - else + else if (!options.ReplaceAllMetadata + && (audio.AlbumArtists is null || audio.AlbumArtists.Count == 0)) { - audio.Album ??= tags.Album; - audio.IndexNumber ??= Convert.ToInt32(tags.Track); - audio.ParentIndexNumber ??= Convert.ToInt32(tags.Disc); + audio.AlbumArtists = albumArtists; } + } - if (tags.Year != 0) - { - var year = Convert.ToInt32(tags.Year); - audio.ProductionYear = year; + if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(tags.Title)) + { + audio.Name = tags.Title; + } + + if (options.ReplaceAllMetadata) + { + audio.Album = tags.Album; + audio.IndexNumber = Convert.ToInt32(tags.Track); + audio.ParentIndexNumber = Convert.ToInt32(tags.Disc); + } + else + { + audio.Album ??= tags.Album; + audio.IndexNumber ??= Convert.ToInt32(tags.Track); + audio.ParentIndexNumber ??= Convert.ToInt32(tags.Disc); + } + + if (tags.Year != 0) + { + var year = Convert.ToInt32(tags.Year); + audio.ProductionYear = year; - if (!audio.PremiereDate.HasValue) + if (!audio.PremiereDate.HasValue) + { + try { audio.PremiereDate = new DateTime(year, 01, 01); } + catch (ArgumentOutOfRangeException ex) + { + _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, tags.Year); + } } + } - if (!audio.LockedFields.Contains(MetadataField.Genres)) - { - audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0 - ? tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray() - : audio.Genres; - } + if (!audio.LockedFields.Contains(MetadataField.Genres)) + { + audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0 + ? tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray() + : audio.Genres; + } - if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _)) - { - audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId); - } + if (!double.IsNaN(tags.ReplayGainTrackGain)) + { + audio.NormalizationGain = (float)tags.ReplayGainTrackGain; + } - if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _)) - { - audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId); - } + if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _)) + && !string.IsNullOrEmpty(tags.MusicBrainzArtistId)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId); + } - if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _)) - { - audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId); - } + if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _)) + && !string.IsNullOrEmpty(tags.MusicBrainzReleaseArtistId)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId); + } - if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _)) - { - audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId); - } + if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _)) + && !string.IsNullOrEmpty(tags.MusicBrainzReleaseId)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId); + } - if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _)) - { - audio.SetProviderId(MetadataProvider.MusicBrainzTrack, tags.MusicBrainzTrackId); - } + if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _)) + && !string.IsNullOrEmpty(tags.MusicBrainzReleaseGroupId)) + { + audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId); + } - // Save extracted lyrics if they exist, - // and if we are replacing all metadata or the audio doesn't yet have lyrics. - if (!string.IsNullOrWhiteSpace(tags.Lyrics) - && (options.ReplaceAllMetadata || audio.GetMediaStreams().All(s => s.Type != MediaStreamType.Lyric))) + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _)) + { + // Fallback to ffprobe as TagLib incorrectly provides recording MBID in `tags.MusicBrainzTrackId`. + // See https://github.com/mono/taglib-sharp/issues/304 + var trackMbId = mediaInfo.GetProviderId(MetadataProvider.MusicBrainzTrack); + if (trackMbId is not null) { - await _lyricManager.SaveLyricAsync(audio, "lrc", tags.Lyrics).ConfigureAwait(false); + audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId); } } + + // Save extracted lyrics if they exist, + // and if the audio doesn't yet have lyrics. + if (!string.IsNullOrWhiteSpace(tags.Lyrics) + && tryExtractEmbeddedLyrics) + { + await _lyricManager.SaveLyricAsync(audio, "lrc", tags.Lyrics).ConfigureAwait(false); + } } private void AddExternalLyrics( @@ -429,7 +378,10 @@ namespace MediaBrowser.Providers.MediaInfo var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, false); audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray(); - currentStreams.AddRange(externalLyricFiles); + if (externalLyricFiles.Count > 0) + { + currentStreams.Add(externalLyricFiles[0]); + } } } } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 5d0fccbe1..246ba2733 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -124,11 +124,8 @@ namespace MediaBrowser.Providers.MediaInfo // Get BD disc information blurayDiscInfo = GetBDInfo(item.Path); - // Get playable .m2ts files - var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path); - // Return if no playable .m2ts files are found - if (blurayDiscInfo is null || blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0) + if (blurayDiscInfo is null || blurayDiscInfo.Files.Length == 0) { _logger.LogError("No playable .m2ts files found in Blu-ray structure, skipping FFprobe."); return ItemUpdateType.MetadataImport; @@ -138,7 +135,7 @@ namespace MediaBrowser.Providers.MediaInfo mediaInfoResult = await GetMediaInfo( new Video { - Path = m2ts[0] + Path = blurayDiscInfo.Files[0] }, cancellationToken).ConfigureAwait(false); } @@ -358,6 +355,10 @@ namespace MediaBrowser.Providers.MediaInfo blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate; blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width; blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height; + blurayVideoStream.ColorRange = ffmpegVideoStream.ColorRange; + blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace; + blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer; + blurayVideoStream.ColorPrimaries = ffmpegVideoStream.ColorPrimaries; } } diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 8bb8d5bb4..04da8fb88 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -1,6 +1,7 @@ #nullable disable using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; @@ -141,19 +142,15 @@ namespace MediaBrowser.Providers.MediaInfo && item.SupportsLocalMetadata && !video.IsPlaceHolder) { - if (!video.SubtitleFiles.SequenceEqual( - _subtitleResolver.GetExternalFiles(video, directoryService, false) - .Select(info => info.Path).ToList(), - StringComparer.Ordinal)) + var externalFiles = new HashSet<string>(_subtitleResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase); + if (!new HashSet<string>(video.SubtitleFiles, StringComparer.Ordinal).SetEquals(externalFiles)) { _logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path); return true; } - if (!video.AudioFiles.SequenceEqual( - _audioResolver.GetExternalFiles(video, directoryService, false) - .Select(info => info.Path).ToList(), - StringComparer.Ordinal)) + externalFiles = new HashSet<string>(_audioResolver.GetExternalFiles(video, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase); + if (!new HashSet<string>(video.AudioFiles, StringComparer.Ordinal).SetEquals(externalFiles)) { _logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path); return true; @@ -161,14 +158,14 @@ namespace MediaBrowser.Providers.MediaInfo } if (item is Audio audio - && item.SupportsLocalMetadata - && !audio.LyricFiles.SequenceEqual( - _lyricResolver.GetExternalFiles(audio, directoryService, false) - .Select(info => info.Path).ToList(), - StringComparer.Ordinal)) + && item.SupportsLocalMetadata) { - _logger.LogDebug("Refreshing {ItemPath} due to external lyrics change.", item.Path); - return true; + var externalFiles = new HashSet<string>(_lyricResolver.GetExternalFiles(audio, directoryService, false).Select(info => info.Path), StringComparer.OrdinalIgnoreCase); + if (!new HashSet<string>(audio.LyricFiles, StringComparer.Ordinal).SetEquals(externalFiles)) + { + _logger.LogDebug("Refreshing {ItemPath} due to external lyrics change.", item.Path); + return true; + } } return false; diff --git a/MediaBrowser.Providers/Movies/MovieMetadataService.cs b/MediaBrowser.Providers/Movies/MovieMetadataService.cs index 984a3c122..8997ddc64 100644 --- a/MediaBrowser.Providers/Movies/MovieMetadataService.cs +++ b/MediaBrowser.Providers/Movies/MovieMetadataService.cs @@ -24,22 +24,6 @@ namespace MediaBrowser.Providers.Movies } /// <inheritdoc /> - protected override bool IsFullLocalMetadata(Movie item) - { - if (string.IsNullOrWhiteSpace(item.Overview)) - { - return false; - } - - if (!item.ProductionYear.HasValue) - { - return false; - } - - return base.IsFullLocalMetadata(item); - } - - /// <inheritdoc /> protected override void MergeData(MetadataResult<Movie> source, MetadataResult<Movie> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) { base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); diff --git a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs index ad0c5aaa7..e77d2fa8a 100644 --- a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs +++ b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System.Linq; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -24,22 +25,6 @@ namespace MediaBrowser.Providers.Movies } /// <inheritdoc /> - protected override bool IsFullLocalMetadata(Trailer item) - { - if (string.IsNullOrWhiteSpace(item.Overview)) - { - return false; - } - - if (!item.ProductionYear.HasValue) - { - return false; - } - - return base.IsFullLocalMetadata(item); - } - - /// <inheritdoc /> protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings) { base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings); @@ -48,6 +33,10 @@ namespace MediaBrowser.Providers.Movies { target.Item.TrailerTypes = source.Item.TrailerTypes; } + else + { + target.Item.TrailerTypes = target.Item.TrailerTypes.Concat(source.Item.TrailerTypes).Distinct().ToArray(); + } } } } diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index e4f34776b..a39bd16ce 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -225,6 +225,10 @@ namespace MediaBrowser.Providers.Music { targetItem.Artists = sourceItem.Artists; } + else + { + targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray(); + } if (replaceData || string.IsNullOrEmpty(targetItem.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist))) { diff --git a/MediaBrowser.Providers/Music/AudioMetadataService.cs b/MediaBrowser.Providers/Music/AudioMetadataService.cs index a5b7cb895..7b25bc0e4 100644 --- a/MediaBrowser.Providers/Music/AudioMetadataService.cs +++ b/MediaBrowser.Providers/Music/AudioMetadataService.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -60,6 +61,10 @@ namespace MediaBrowser.Providers.Music { targetItem.Artists = sourceItem.Artists; } + else + { + targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray(); + } if (replaceData || string.IsNullOrEmpty(targetItem.Album)) { diff --git a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs index b97b76630..24c4b5501 100644 --- a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs +++ b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System.Linq; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -45,6 +46,10 @@ namespace MediaBrowser.Providers.Music { targetItem.Artists = sourceItem.Artists; } + else + { + targetItem.Artists = targetItem.Artists.Concat(sourceItem.Artists).Distinct().ToArray(); + } } } } diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs index 9bd36f25c..51a3ba0c7 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs @@ -1,170 +1,227 @@ #nullable disable -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; using PlaylistsNET.Content; -namespace MediaBrowser.Providers.Playlists +namespace MediaBrowser.Providers.Playlists; + +/// <summary> +/// Local playlist provider. +/// </summary> +public class PlaylistItemsProvider : ILocalMetadataProvider<Playlist>, + IHasOrder, + IForcedProvider, + IHasItemChangeMonitor { - public class PlaylistItemsProvider : ICustomMetadataProvider<Playlist>, - IHasOrder, - IForcedProvider, - IPreRefreshProvider, - IHasItemChangeMonitor + private readonly IFileSystem _fileSystem; + private readonly ILibraryManager _libraryManager; + private readonly ILogger<PlaylistItemsProvider> _logger; + private readonly CollectionType[] _ignoredCollections = [CollectionType.livetv, CollectionType.boxsets, CollectionType.playlists]; + + /// <summary> + /// Initializes a new instance of the <see cref="PlaylistItemsProvider"/> class. + /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger{PlaylistItemsProvider}"/> interface.</param> + /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger, ILibraryManager libraryManager, IFileSystem fileSystem) { - private readonly ILogger<PlaylistItemsProvider> _logger; - - public PlaylistItemsProvider(ILogger<PlaylistItemsProvider> logger) - { - _logger = logger; - } + _logger = logger; + _libraryManager = libraryManager; + _fileSystem = fileSystem; + } - public string Name => "Playlist Reader"; + /// <inheritdoc /> + public string Name => "Playlist Item Provider"; - // Run last - public int Order => 100; + /// <inheritdoc /> + public int Order => 100; - public Task<ItemUpdateType> FetchAsync(Playlist item, MetadataRefreshOptions options, CancellationToken cancellationToken) + /// <inheritdoc /> + public Task<MetadataResult<Playlist>> GetMetadata( + ItemInfo info, + IDirectoryService directoryService, + CancellationToken cancellationToken) + { + var result = new MetadataResult<Playlist>() { - var path = item.Path; - if (!Playlist.IsPlaylistFile(path)) + Item = new Playlist { - return Task.FromResult(ItemUpdateType.None); + Path = info.Path } + }; + Fetch(result); - var extension = Path.GetExtension(path); - if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase)) - { - return Task.FromResult(ItemUpdateType.None); - } + return Task.FromResult(result); + } - using (var stream = File.OpenRead(path)) - { - var items = GetItems(stream, extension).ToArray(); + private void Fetch(MetadataResult<Playlist> result) + { + var item = result.Item; + var path = item.Path; + if (!Playlist.IsPlaylistFile(path)) + { + return; + } - item.LinkedChildren = items; - } + var extension = Path.GetExtension(path); + if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase)) + { + return; + } - return Task.FromResult(ItemUpdateType.None); + var items = GetItems(path, extension).ToArray(); + if (items.Length > 0) + { + result.HasMetadata = true; + item.LinkedChildren = items; } - private IEnumerable<LinkedChild> GetItems(Stream stream, string extension) + return; + } + + private IEnumerable<LinkedChild> GetItems(string path, string extension) + { + var libraryRoots = _libraryManager.GetUserRootFolder().Children + .OfType<CollectionFolder>() + .Where(f => f.CollectionType.HasValue && !_ignoredCollections.Contains(f.CollectionType.Value)) + .SelectMany(f => f.PhysicalLocations) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + using (var stream = File.OpenRead(path)) { if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase)) { - return GetWplItems(stream); + return GetWplItems(stream, path, libraryRoots); } if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase)) { - return GetZplItems(stream); + return GetZplItems(stream, path, libraryRoots); } if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase)) { - return GetM3uItems(stream); + return GetM3uItems(stream, path, libraryRoots); } if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase)) { - return GetM3u8Items(stream); + return GetM3uItems(stream, path, libraryRoots); } if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase)) { - return GetPlsItems(stream); + return GetPlsItems(stream, path, libraryRoots); } - - return Enumerable.Empty<LinkedChild>(); } - private IEnumerable<LinkedChild> GetPlsItems(Stream stream) - { - var content = new PlsContent(); - var playlist = content.GetFromStream(stream); + return Enumerable.Empty<LinkedChild>(); + } - return playlist.PlaylistEntries.Select(i => new LinkedChild - { - Path = i.Path, - Type = LinkedChildType.Manual - }); - } + private IEnumerable<LinkedChild> GetPlsItems(Stream stream, string playlistPath, List<string> libraryRoots) + { + var content = new PlsContent(); + var playlist = content.GetFromStream(stream); - private IEnumerable<LinkedChild> GetM3u8Items(Stream stream) - { - var content = new M3uContent(); - var playlist = content.GetFromStream(stream); + return playlist.PlaylistEntries + .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots)) + .Where(i => i is not null); + } - return playlist.PlaylistEntries.Select(i => new LinkedChild - { - Path = i.Path, - Type = LinkedChildType.Manual - }); - } + private IEnumerable<LinkedChild> GetM3uItems(Stream stream, string playlistPath, List<string> libraryRoots) + { + var content = new M3uContent(); + var playlist = content.GetFromStream(stream); - private IEnumerable<LinkedChild> GetM3uItems(Stream stream) - { - var content = new M3uContent(); - var playlist = content.GetFromStream(stream); + return playlist.PlaylistEntries + .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots)) + .Where(i => i is not null); + } - return playlist.PlaylistEntries.Select(i => new LinkedChild - { - Path = i.Path, - Type = LinkedChildType.Manual - }); - } + private IEnumerable<LinkedChild> GetZplItems(Stream stream, string playlistPath, List<string> libraryRoots) + { + var content = new ZplContent(); + var playlist = content.GetFromStream(stream); - private IEnumerable<LinkedChild> GetZplItems(Stream stream) - { - var content = new ZplContent(); - var playlist = content.GetFromStream(stream); + return playlist.PlaylistEntries + .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots)) + .Where(i => i is not null); + } + + private IEnumerable<LinkedChild> GetWplItems(Stream stream, string playlistPath, List<string> libraryRoots) + { + var content = new WplContent(); + var playlist = content.GetFromStream(stream); - return playlist.PlaylistEntries.Select(i => new LinkedChild + return playlist.PlaylistEntries + .Select(i => GetLinkedChild(i.Path, playlistPath, libraryRoots)) + .Where(i => i is not null); + } + + private LinkedChild GetLinkedChild(string itemPath, string playlistPath, List<string> libraryRoots) + { + if (TryGetPlaylistItemPath(itemPath, playlistPath, libraryRoots, out var parsedPath)) + { + return new LinkedChild { - Path = i.Path, + Path = parsedPath, Type = LinkedChildType.Manual - }); + }; } - private IEnumerable<LinkedChild> GetWplItems(Stream stream) + return null; + } + + private bool TryGetPlaylistItemPath(string itemPath, string playlistPath, List<string> libraryPaths, out string path) + { + path = null; + string pathToCheck = _fileSystem.MakeAbsolutePath(Path.GetDirectoryName(playlistPath), itemPath); + if (!File.Exists(pathToCheck)) { - var content = new WplContent(); - var playlist = content.GetFromStream(stream); + return false; + } - return playlist.PlaylistEntries.Select(i => new LinkedChild + foreach (var libraryPath in libraryPaths) + { + if (pathToCheck.StartsWith(libraryPath, StringComparison.OrdinalIgnoreCase)) { - Path = i.Path, - Type = LinkedChildType.Manual - }); + path = pathToCheck; + return true; + } } - public bool HasChanged(BaseItem item, IDirectoryService directoryService) - { - var path = item.Path; + return false; + } - if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol) + /// <inheritdoc /> + public bool HasChanged(BaseItem item, IDirectoryService directoryService) + { + var path = item.Path; + if (!string.IsNullOrWhiteSpace(path) && item.IsFileProtocol) + { + var file = directoryService.GetFile(path); + if (file is not null && file.LastWriteTimeUtc != item.DateModified) { - var file = directoryService.GetFile(path); - if (file is not null && file.LastWriteTimeUtc != item.DateModified) - { - _logger.LogDebug("Refreshing {0} due to date modified timestamp change.", path); - return true; - } + _logger.LogDebug("Refreshing {Path} due to date modified timestamp change.", path); + return true; } - - return false; } + + return false; } } diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs index 1bd000a48..43889bfbf 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System.Collections.Generic; +using System.Linq; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -49,8 +50,24 @@ namespace MediaBrowser.Providers.Playlists if (mergeMetadataSettings) { targetItem.PlaylistMediaType = sourceItem.PlaylistMediaType; - targetItem.LinkedChildren = sourceItem.LinkedChildren; - targetItem.Shares = sourceItem.Shares; + + if (replaceData || targetItem.LinkedChildren.Length == 0) + { + targetItem.LinkedChildren = sourceItem.LinkedChildren; + } + else + { + targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray(); + } + + if (replaceData || targetItem.Shares.Count == 0) + { + targetItem.Shares = sourceItem.Shares; + } + else + { + targetItem.Shares = sourceItem.Shares.Concat(targetItem.Shares).DistinctBy(s => s.UserId).ToArray(); + } } } } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs index d0bd7d609..c35324746 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs @@ -250,7 +250,7 @@ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, Albu // If we have a release ID but not a release group ID, lookup the release group if (!string.IsNullOrWhiteSpace(releaseId) && string.IsNullOrWhiteSpace(releaseGroupId)) { - var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.Releases, cancellationToken).ConfigureAwait(false); + var release = await _musicBrainzQuery.LookupReleaseAsync(new Guid(releaseId), Include.ReleaseGroups, cancellationToken).ConfigureAwait(false); releaseGroupId = release.ReleaseGroup?.Id.ToString(); result.HasMetadata = true; } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index 3fd4ae1fc..c750caa1c 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -447,11 +447,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb var actorList = result.Actors.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); foreach (var actor in actorList) { - if (string.IsNullOrWhiteSpace(actor)) - { - continue; - } - var person = new PersonInfo { Name = actor, diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index 8dc2d6938..d8476bd47 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -278,17 +278,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); - if (string.Equals(seriesResult.Status, "Ended", StringComparison.OrdinalIgnoreCase) - || string.Equals(seriesResult.Status, "Canceled", StringComparison.OrdinalIgnoreCase)) + if (Emby.Naming.TV.TvParserHelpers.TryParseSeriesStatus(seriesResult.Status, out var seriesStatus)) { - series.Status = SeriesStatus.Ended; - series.EndDate = seriesResult.LastAirDate; - } - else - { - series.Status = SeriesStatus.Continuing; + series.Status = seriesStatus; } + series.EndDate = seriesResult.LastAirDate; series.PremiereDate = seriesResult.FirstAirDate; var ids = seriesResult.ExternalIds; diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index f68b3cee6..ae5e1090a 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -117,7 +117,7 @@ namespace MediaBrowser.Providers.Subtitles } catch (Exception ex) { - _logger.LogError(ex, "Error downloading subtitles from {0}", i.Name); + _logger.LogError(ex, "Error downloading subtitles from {Name}", i.Name); return Array.Empty<RemoteSubtitleInfo>(); } }); @@ -205,72 +205,71 @@ namespace MediaBrowser.Providers.Subtitles saveFileName += ".sdh"; } - saveFileName += "." + response.Format.ToLowerInvariant(); - if (saveInMediaFolder) { var mediaFolderPath = Path.GetFullPath(Path.Combine(video.ContainingFolderPath, saveFileName)); - // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path."); - if (mediaFolderPath.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)) - { - savePaths.Add(mediaFolderPath); - } + savePaths.Add(mediaFolderPath); } var internalPath = Path.GetFullPath(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); - // TODO: Add some error to the user: return BadRequest("Could not save subtitle, bad path."); - if (internalPath.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal)) - { - savePaths.Add(internalPath); - } + savePaths.Add(internalPath); - if (savePaths.Count > 0) - { - await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); - } - else - { - _logger.LogError("An uploaded subtitle could not be saved because the resulting paths were invalid."); - } + await TrySaveToFiles(memoryStream, savePaths, video, response.Format.ToLowerInvariant()).ConfigureAwait(false); } } - private async Task TrySaveToFiles(Stream stream, List<string> savePaths) + private async Task TrySaveToFiles(Stream stream, List<string> savePaths, Video video, string extension) { List<Exception>? exs = null; foreach (var savePath in savePaths) { - _logger.LogInformation("Saving subtitles to {SavePath}", savePath); - - _monitor.ReportFileSystemChangeBeginning(savePath); - + var path = savePath + "." + extension; try { - Directory.CreateDirectory(Path.GetDirectoryName(savePath) ?? throw new InvalidOperationException("Path can't be a root directory.")); + if (path.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal) + || path.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal)) + { + var fileExists = File.Exists(path); + var counter = 0; + + while (fileExists) + { + path = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", savePath, counter, extension); + fileExists = File.Exists(path); + counter++; + } + + _logger.LogInformation("Saving subtitles to {SavePath}", path); + _monitor.ReportFileSystemChangeBeginning(path); + + Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory.")); - var fileOptions = AsyncFile.WriteOptions; - fileOptions.Mode = FileMode.CreateNew; - fileOptions.PreallocationSize = stream.Length; - var fs = new FileStream(savePath, fileOptions); - await using (fs.ConfigureAwait(false)) + var fileOptions = AsyncFile.WriteOptions; + fileOptions.Mode = FileMode.CreateNew; + fileOptions.PreallocationSize = stream.Length; + var fs = new FileStream(path, fileOptions); + await using (fs.ConfigureAwait(false)) + { + await stream.CopyToAsync(fs).ConfigureAwait(false); + } + + return; + } + else { - await stream.CopyToAsync(fs).ConfigureAwait(false); + // TODO: Add some error handling to the API user: return BadRequest("Could not save subtitle, bad path."); + _logger.LogError("An uploaded subtitle could not be saved because the resulting path was invalid."); } - - return; } catch (Exception ex) { -// Bug in analyzer -- https://github.com/dotnet/roslyn-analyzers/issues/5160 -#pragma warning disable CA1508 - (exs ??= new List<Exception>()).Add(ex); -#pragma warning restore CA1508 + (exs ??= []).Add(ex); } finally { - _monitor.ReportFileSystemChangeComplete(savePath, false); + _monitor.ReportFileSystemChangeComplete(path, false); } stream.Position = 0; diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 01c07d633..b03d6ffb5 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -35,30 +36,33 @@ namespace MediaBrowser.Providers.TV _localizationManager = localizationManager; } - /// <inheritdoc /> - protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) + public override async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) { - await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false); + if (item is Series series) + { + var seasons = series.GetRecursiveChildren(i => i is Season).ToList(); - RemoveObsoleteEpisodes(item); - RemoveObsoleteSeasons(item); - await UpdateAndCreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false); + foreach (var season in seasons) + { + var hasUpdate = refreshOptions != null && season.BeforeMetadataRefresh(refreshOptions.ReplaceAllMetadata); + if (hasUpdate) + { + await season.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); + } + } + } + + return await base.RefreshMetadata(item, refreshOptions, cancellationToken).ConfigureAwait(false); } /// <inheritdoc /> - protected override bool IsFullLocalMetadata(Series item) + protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(item.Overview)) - { - return false; - } - - if (!item.ProductionYear.HasValue) - { - return false; - } + await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false); - return base.IsFullLocalMetadata(item); + RemoveObsoleteEpisodes(item); + await CreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false); + RemoveObsoleteSeasons(item); } /// <inheritdoc /> @@ -68,20 +72,6 @@ namespace MediaBrowser.Providers.TV var sourceItem = source.Item; var targetItem = target.Item; - var sourceSeasonNames = sourceItem.SeasonNames; - var targetSeasonNames = targetItem.SeasonNames; - - if (replaceData || targetSeasonNames.Count == 0) - { - targetItem.SeasonNames = sourceSeasonNames; - } - else if (targetSeasonNames.Count != sourceSeasonNames.Count || !sourceSeasonNames.Keys.All(targetSeasonNames.ContainsKey)) - { - foreach (var (number, name) in sourceSeasonNames) - { - targetSeasonNames.TryAdd(number, name); - } - } if (replaceData || string.IsNullOrEmpty(targetItem.AirTime)) { @@ -101,7 +91,7 @@ namespace MediaBrowser.Providers.TV private void RemoveObsoleteSeasons(Series series) { - // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in UpdateAndCreateSeasonsAsync. + // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in CreateSeasonsAsync. var physicalSeasonNumbers = new HashSet<int>(); var virtualSeasons = new List<Season>(); foreach (var existingSeason in series.Children.OfType<Season>()) @@ -129,7 +119,8 @@ namespace MediaBrowser.Providers.TV virtualSeason, new DeleteOptions { - DeleteFileLocation = true + // Internal metadata paths are removed regardless of this. + DeleteFileLocation = false }, false); } @@ -138,7 +129,7 @@ namespace MediaBrowser.Providers.TV private void RemoveObsoleteEpisodes(Series series) { - var episodes = series.GetEpisodes(null, new DtoOptions()).OfType<Episode>().ToList(); + var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList(); var numberOfEpisodes = episodes.Count; // TODO: O(n^2), but can it be done faster without overcomplicating it? for (var i = 0; i < numberOfEpisodes; i++) @@ -186,7 +177,8 @@ namespace MediaBrowser.Providers.TV episode, new DeleteOptions { - DeleteFileLocation = true + // Internal metadata paths are removed regardless of this. + DeleteFileLocation = false }, false); } @@ -194,14 +186,12 @@ namespace MediaBrowser.Providers.TV /// <summary> /// Creates seasons for all episodes if they don't exist. /// If no season number can be determined, a dummy season will be created. - /// Updates seasons names. /// </summary> /// <param name="series">The series.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The async task.</returns> - private async Task UpdateAndCreateSeasonsAsync(Series series, CancellationToken cancellationToken) + private async Task CreateSeasonsAsync(Series series, CancellationToken cancellationToken) { - var seasonNames = series.SeasonNames; var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season); var seasons = seriesChildren.OfType<Season>().ToList(); var uniqueSeasonNumbers = seriesChildren @@ -214,20 +204,14 @@ namespace MediaBrowser.Providers.TV { // Null season numbers will have a 'dummy' season created because seasons are always required. var existingSeason = seasons.FirstOrDefault(i => i.IndexNumber == seasonNumber); - - if (!seasonNumber.HasValue || !seasonNames.TryGetValue(seasonNumber.Value, out var seasonName)) - { - seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber); - } - if (existingSeason is null) { - var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false); - series.AddChild(season); + var seasonName = GetValidSeasonNameForSeries(series, null, seasonNumber); + await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false); } - else if (!string.Equals(existingSeason.Name, seasonName, StringComparison.Ordinal)) + else if (existingSeason.IsVirtualItem) { - existingSeason.Name = seasonName; + existingSeason.IsVirtualItem = false; await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } } @@ -241,7 +225,7 @@ namespace MediaBrowser.Providers.TV /// <param name="seasonNumber">The season number.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The newly created season.</returns> - private async Task<Season> CreateSeasonAsync( + private async Task CreateSeasonAsync( Series series, string? seasonName, int? seasonNumber, @@ -258,14 +242,12 @@ namespace MediaBrowser.Providers.TV typeof(Season)), IsVirtualItem = false, SeriesId = series.Id, - SeriesName = series.Name + SeriesName = series.Name, + SeriesPresentationUniqueKey = series.GetPresentationUniqueKey() }; series.AddChild(season); - await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false); - - return season; } private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber) diff --git a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs index f6dcde4f6..9dc4446fc 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayProvider.cs @@ -101,7 +101,7 @@ public class TrickplayProvider : ICustomMetadataProvider<Episode>, bool? enableDuringScan = libraryOptions?.ExtractTrickplayImagesDuringLibraryScan; bool replace = options.ReplaceAllImages; - if (options.IsAutomated && !enableDuringScan.GetValueOrDefault(false)) + if (!enableDuringScan.GetValueOrDefault(false)) { return ItemUpdateType.None; } |
