diff options
Diffstat (limited to 'MediaBrowser.Providers')
17 files changed, 447 insertions, 308 deletions
diff --git a/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs b/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs new file mode 100644 index 000000000..ab09f278a --- /dev/null +++ b/MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs @@ -0,0 +1,69 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Resolvers; + +namespace MediaBrowser.Providers.Lyric; + +/// <inheritdoc /> +public class DefaultLyricProvider : ILyricProvider +{ + private static readonly string[] _lyricExtensions = { ".lrc", ".elrc", ".txt" }; + + /// <inheritdoc /> + public string Name => "DefaultLyricProvider"; + + /// <inheritdoc /> + public ResolverPriority Priority => ResolverPriority.First; + + /// <inheritdoc /> + public bool HasLyrics(BaseItem item) + { + var path = GetLyricsPath(item); + return path is not null; + } + + /// <inheritdoc /> + public async Task<LyricFile?> GetLyrics(BaseItem item) + { + var path = GetLyricsPath(item); + if (path is not null) + { + var content = await File.ReadAllTextAsync(path).ConfigureAwait(false); + if (!string.IsNullOrEmpty(content)) + { + return new LyricFile(path, content); + } + } + + return null; + } + + private string? GetLyricsPath(BaseItem item) + { + // Ensure the path to the item is not null + string? itemDirectoryPath = Path.GetDirectoryName(item.Path); + if (itemDirectoryPath is null) + { + return null; + } + + // Ensure the directory path exists + if (!Directory.Exists(itemDirectoryPath)) + { + return null; + } + + foreach (var lyricFilePath in Directory.GetFiles(itemDirectoryPath, $"{Path.GetFileNameWithoutExtension(item.Path)}.*")) + { + if (_lyricExtensions.Contains(Path.GetExtension(lyricFilePath.AsSpan()), StringComparison.OrdinalIgnoreCase)) + { + return lyricFilePath; + } + } + + return null; + } +} diff --git a/MediaBrowser.Providers/Lyric/ILyricProvider.cs b/MediaBrowser.Providers/Lyric/ILyricProvider.cs new file mode 100644 index 000000000..27ceba72b --- /dev/null +++ b/MediaBrowser.Providers/Lyric/ILyricProvider.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Resolvers; + +namespace MediaBrowser.Providers.Lyric; + +/// <summary> +/// Interface ILyricsProvider. +/// </summary> +public interface ILyricProvider +{ + /// <summary> + /// Gets a value indicating the provider name. + /// </summary> + string Name { get; } + + /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + ResolverPriority Priority { get; } + + /// <summary> + /// Checks if an item has lyrics available. + /// </summary> + /// <param name="item">The media item.</param> + /// <returns>Whether lyrics where found or not.</returns> + bool HasLyrics(BaseItem item); + + /// <summary> + /// Gets the lyrics. + /// </summary> + /// <param name="item">The media item.</param> + /// <returns>A task representing found lyrics.</returns> + Task<LyricFile?> GetLyrics(BaseItem item); +} diff --git a/MediaBrowser.Providers/Lyric/LrcLyricProvider.cs b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs index 7b108921b..7f1ecd743 100644 --- a/MediaBrowser.Providers/Lyric/LrcLyricProvider.cs +++ b/MediaBrowser.Providers/Lyric/LrcLyricParser.cs @@ -3,34 +3,29 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using System.Threading.Tasks; +using Jellyfin.Extensions; using LrcParser.Model; using LrcParser.Parser; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Resolvers; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Lyric; /// <summary> -/// LRC Lyric Provider. +/// LRC Lyric Parser. /// </summary> -public class LrcLyricProvider : ILyricProvider +public class LrcLyricParser : ILyricParser { - private readonly ILogger<LrcLyricProvider> _logger; - private readonly LyricParser _lrcLyricParser; + private static readonly string[] _supportedMediaTypes = { ".lrc", ".elrc" }; private static readonly string[] _acceptedTimeFormats = { "HH:mm:ss", "H:mm:ss", "mm:ss", "m:ss" }; /// <summary> - /// Initializes a new instance of the <see cref="LrcLyricProvider"/> class. + /// Initializes a new instance of the <see cref="LrcLyricParser"/> class. /// </summary> - /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> - public LrcLyricProvider(ILogger<LrcLyricProvider> logger) + public LrcLyricParser() { - _logger = logger; _lrcLyricParser = new LrcParser.Parser.Lrc.LrcParser(); } @@ -41,37 +36,25 @@ public class LrcLyricProvider : ILyricProvider /// Gets the priority. /// </summary> /// <value>The priority.</value> - public ResolverPriority Priority => ResolverPriority.First; + public ResolverPriority Priority => ResolverPriority.Fourth; /// <inheritdoc /> - public IReadOnlyCollection<string> SupportedMediaTypes { get; } = new[] { "lrc", "elrc" }; - - /// <summary> - /// Opens lyric file for the requested item, and processes it for API return. - /// </summary> - /// <param name="item">The item to to process.</param> - /// <returns>If provider can determine lyrics, returns a <see cref="LyricResponse"/> with or without metadata; otherwise, null.</returns> - public async Task<LyricResponse?> GetLyrics(BaseItem item) + public LyricResponse? ParseLyrics(LyricFile lyrics) { - string? lyricFilePath = this.GetLyricFilePath(item.Path); - - if (string.IsNullOrEmpty(lyricFilePath)) + if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan()), StringComparison.OrdinalIgnoreCase)) { return null; } - var fileMetaData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - string lrcFileContent = await File.ReadAllTextAsync(lyricFilePath).ConfigureAwait(false); - Song lyricData; try { - lyricData = _lrcLyricParser.Decode(lrcFileContent); + lyricData = _lrcLyricParser.Decode(lyrics.Content); } - catch (Exception ex) + catch (Exception) { - _logger.LogError(ex, "Error parsing lyric file {LyricFilePath} from {Provider}", lyricFilePath, Name); + // Failed to parse, return null so the next parser will be tried return null; } @@ -84,6 +67,7 @@ public class LrcLyricProvider : ILyricProvider .Select(x => x.Text) .ToList(); + var fileMetaData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); foreach (string metaDataRow in metaDataRows) { var index = metaDataRow.IndexOf(':', StringComparison.OrdinalIgnoreCase); @@ -130,17 +114,10 @@ public class LrcLyricProvider : ILyricProvider // Map metaData values from LRC file to LyricMetadata properties LyricMetadata lyricMetadata = MapMetadataValues(fileMetaData); - return new LyricResponse - { - Metadata = lyricMetadata, - Lyrics = lyricList - }; + return new LyricResponse { Metadata = lyricMetadata, Lyrics = lyricList }; } - return new LyricResponse - { - Lyrics = lyricList - }; + return new LyricResponse { Lyrics = lyricList }; } /// <summary> diff --git a/MediaBrowser.Providers/Lyric/LyricManager.cs b/MediaBrowser.Providers/Lyric/LyricManager.cs index f9547e0f0..6da811927 100644 --- a/MediaBrowser.Providers/Lyric/LyricManager.cs +++ b/MediaBrowser.Providers/Lyric/LyricManager.cs @@ -12,14 +12,17 @@ namespace MediaBrowser.Providers.Lyric; public class LyricManager : ILyricManager { private readonly ILyricProvider[] _lyricProviders; + private readonly ILyricParser[] _lyricParsers; /// <summary> /// Initializes a new instance of the <see cref="LyricManager"/> class. /// </summary> /// <param name="lyricProviders">All found lyricProviders.</param> - public LyricManager(IEnumerable<ILyricProvider> lyricProviders) + /// <param name="lyricParsers">All found lyricParsers.</param> + public LyricManager(IEnumerable<ILyricProvider> lyricProviders, IEnumerable<ILyricParser> lyricParsers) { _lyricProviders = lyricProviders.OrderBy(i => i.Priority).ToArray(); + _lyricParsers = lyricParsers.OrderBy(i => i.Priority).ToArray(); } /// <inheritdoc /> @@ -27,10 +30,19 @@ public class LyricManager : ILyricManager { foreach (ILyricProvider provider in _lyricProviders) { - var results = await provider.GetLyrics(item).ConfigureAwait(false); - if (results is not null) + var lyrics = await provider.GetLyrics(item).ConfigureAwait(false); + if (lyrics is null) { - return results; + continue; + } + + foreach (ILyricParser parser in _lyricParsers) + { + var result = parser.ParseLyrics(lyrics); + if (result is not null) + { + return result; + } } } @@ -47,7 +59,7 @@ public class LyricManager : ILyricManager continue; } - if (provider.GetLyricFilePath(item.Path) is not null) + if (provider.HasLyrics(item)) { return true; } diff --git a/MediaBrowser.Providers/Lyric/TxtLyricParser.cs b/MediaBrowser.Providers/Lyric/TxtLyricParser.cs new file mode 100644 index 000000000..706f13dbc --- /dev/null +++ b/MediaBrowser.Providers/Lyric/TxtLyricParser.cs @@ -0,0 +1,44 @@ +using System; +using System.IO; +using Jellyfin.Extensions; +using MediaBrowser.Controller.Lyrics; +using MediaBrowser.Controller.Resolvers; + +namespace MediaBrowser.Providers.Lyric; + +/// <summary> +/// TXT Lyric Parser. +/// </summary> +public class TxtLyricParser : ILyricParser +{ + private static readonly string[] _supportedMediaTypes = { ".lrc", ".elrc", ".txt" }; + private static readonly string[] _lineBreakCharacters = { "\r\n", "\r", "\n" }; + + /// <inheritdoc /> + public string Name => "TxtLyricProvider"; + + /// <summary> + /// Gets the priority. + /// </summary> + /// <value>The priority.</value> + public ResolverPriority Priority => ResolverPriority.Fifth; + + /// <inheritdoc /> + public LyricResponse? ParseLyrics(LyricFile lyrics) + { + if (!_supportedMediaTypes.Contains(Path.GetExtension(lyrics.Name.AsSpan()), StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + string[] lyricTextLines = lyrics.Content.Split(_lineBreakCharacters, StringSplitOptions.None); + LyricLine[] lyricList = new LyricLine[lyricTextLines.Length]; + + for (int lyricLineIndex = 0; lyricLineIndex < lyricTextLines.Length; lyricLineIndex++) + { + lyricList[lyricLineIndex] = new LyricLine(lyricTextLines[lyricLineIndex]); + } + + return new LyricResponse { Lyrics = lyricList }; + } +} diff --git a/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs b/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs deleted file mode 100644 index a9099d192..000000000 --- a/MediaBrowser.Providers/Lyric/TxtLyricProvider.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Lyrics; -using MediaBrowser.Controller.Resolvers; - -namespace MediaBrowser.Providers.Lyric; - -/// <summary> -/// TXT Lyric Provider. -/// </summary> -public class TxtLyricProvider : ILyricProvider -{ - /// <inheritdoc /> - public string Name => "TxtLyricProvider"; - - /// <summary> - /// Gets the priority. - /// </summary> - /// <value>The priority.</value> - public ResolverPriority Priority => ResolverPriority.Second; - - /// <inheritdoc /> - public IReadOnlyCollection<string> SupportedMediaTypes { get; } = new[] { "lrc", "elrc", "txt" }; - - /// <summary> - /// Opens lyric file for the requested item, and processes it for API return. - /// </summary> - /// <param name="item">The item to to process.</param> - /// <returns>If provider can determine lyrics, returns a <see cref="LyricResponse"/>; otherwise, null.</returns> - public async Task<LyricResponse?> GetLyrics(BaseItem item) - { - string? lyricFilePath = this.GetLyricFilePath(item.Path); - - if (string.IsNullOrEmpty(lyricFilePath)) - { - return null; - } - - string[] lyricTextLines = await File.ReadAllLinesAsync(lyricFilePath).ConfigureAwait(false); - - if (lyricTextLines.Length == 0) - { - return null; - } - - LyricLine[] lyricList = new LyricLine[lyricTextLines.Length]; - - for (int lyricLineIndex = 0; lyricLineIndex < lyricTextLines.Length; lyricLineIndex++) - { - lyricList[lyricLineIndex] = new LyricLine(lyricTextLines[lyricLineIndex]); - } - - return new LyricResponse - { - Lyrics = lyricList - }; - } -} diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index ba2d2db2f..dab36625e 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -32,6 +32,7 @@ namespace MediaBrowser.Providers.Manager private readonly ILogger _logger; private readonly IProviderManager _providerManager; private readonly IFileSystem _fileSystem; + private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>(); /// <summary> /// Image types that are only one per item. @@ -90,11 +91,12 @@ namespace MediaBrowser.Providers.Manager /// </summary> /// <param name="item">The <see cref="BaseItem"/> to validate images for.</param> /// <param name="providers">The providers to use, must include <see cref="ILocalImageProvider"/>(s) for local scanning.</param> - /// <param name="directoryService">The directory service for <see cref="ILocalImageProvider"/>s to use.</param> + /// <param name="refreshOptions">The refresh options.</param> /// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns> - public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, IDirectoryService directoryService) + public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions) { var hasChanges = false; + IDirectoryService directoryService = refreshOptions?.DirectoryService; if (item is not Photo) { @@ -102,7 +104,7 @@ namespace MediaBrowser.Providers.Manager .SelectMany(i => i.GetImages(item, directoryService)) .ToList(); - if (MergeImages(item, images)) + if (MergeImages(item, images, refreshOptions)) { hasChanges = true; } @@ -384,12 +386,33 @@ namespace MediaBrowser.Providers.Manager /// <summary> /// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary. /// </summary> + /// <param name="refreshOptions">The refresh options.</param> + /// <param name="dontReplaceImages">List of imageTypes to remove from ReplaceImages.</param> + public void UpdateReplaceImages(ImageRefreshOptions refreshOptions, ICollection<ImageType> dontReplaceImages) + { + if (refreshOptions is not null) + { + if (refreshOptions.ReplaceAllImages) + { + refreshOptions.ReplaceAllImages = false; + refreshOptions.ReplaceImages = AllImageTypes.ToList(); + } + + refreshOptions.ReplaceImages = refreshOptions.ReplaceImages.Except(dontReplaceImages).ToList(); + } + } + + /// <summary> + /// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary. + /// </summary> /// <param name="item">The <see cref="BaseItem"/> to modify.</param> /// <param name="images">The new images to place in <c>item</c>.</param> + /// <param name="refreshOptions">The refresh options.</param> /// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns> - public bool MergeImages(BaseItem item, IReadOnlyList<LocalImageInfo> images) + public bool MergeImages(BaseItem item, IReadOnlyList<LocalImageInfo> images, ImageRefreshOptions refreshOptions) { var changed = item.ValidateImages(); + var foundImageTypes = new List<ImageType>(); for (var i = 0; i < _singularImages.Length; i++) { @@ -399,6 +422,11 @@ namespace MediaBrowser.Providers.Manager if (image is not null) { var currentImage = item.GetImageInfo(type, 0); + // if image file is stored with media, don't replace that later + if (item.ContainingFolderPath is not null && item.ContainingFolderPath.Contains(Path.GetDirectoryName(image.FileInfo.FullName), StringComparison.OrdinalIgnoreCase)) + { + foundImageTypes.Add(type); + } if (currentImage is null || !string.Equals(currentImage.Path, image.FileInfo.FullName, StringComparison.OrdinalIgnoreCase)) { @@ -425,6 +453,12 @@ namespace MediaBrowser.Providers.Manager if (UpdateMultiImages(item, images, ImageType.Backdrop)) { changed = true; + foundImageTypes.Add(ImageType.Backdrop); + } + + if (foundImageTypes.Count > 0) + { + UpdateReplaceImages(refreshOptions, foundImageTypes); } return changed; diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index bcc9b809c..75291b317 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -12,6 +12,7 @@ using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; @@ -26,8 +27,6 @@ namespace MediaBrowser.Providers.Manager where TItemType : BaseItem, IHasLookupInfo<TIdType>, new() where TIdType : ItemLookupInfo, new() { - private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>(); - protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger<MetadataService<TItemType, TIdType>> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) { ServerConfigurationManager = serverConfigurationManager; @@ -110,7 +109,7 @@ namespace MediaBrowser.Providers.Manager try { // Always validate images and check for new locally stored ones. - if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService)) + if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions)) { updateType |= ItemUpdateType.ImageUpdate; } @@ -674,8 +673,7 @@ namespace MediaBrowser.Providers.Manager } var hasLocalMetadata = false; - var replaceImages = AllImageTypes.ToList(); - var localImagesFound = false; + var foundImageTypes = new List<ImageType>(); foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>()) { @@ -703,9 +701,8 @@ namespace MediaBrowser.Providers.Manager await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false); refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; - // remove imagetype that has just been downloaded - replaceImages.Remove(remoteImage.Type); - localImagesFound = true; + // remember imagetype that has just been downloaded + foundImageTypes.Add(remoteImage.Type); } catch (HttpRequestException ex) { @@ -713,18 +710,17 @@ namespace MediaBrowser.Providers.Manager } } - if (localImagesFound) + if (foundImageTypes.Count > 0) { - options.ReplaceAllImages = false; - options.ReplaceImages = replaceImages; + imageService.UpdateReplaceImages(options, foundImageTypes); } - if (imageService.MergeImages(item, localItem.Images)) + if (imageService.MergeImages(item, localItem.Images, options)) { refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; } - MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true); + MergeData(localItem, temp, Array.Empty<MetadataField>(), options.ReplaceAllMetadata, true); refreshResult.UpdateType |= ItemUpdateType.MetadataImport; // Only one local provider allowed per item diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 1028da32b..f3211ba45 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -131,12 +131,12 @@ namespace MediaBrowser.Providers.Manager { var type = item.GetType(); - var service = _metadataServices.FirstOrDefault(current => current.CanRefreshPrimary(type)); - service ??= _metadataServices.FirstOrDefault(current => current.CanRefresh(item)); + var service = _metadataServices.FirstOrDefault(current => current.CanRefreshPrimary(type)) + ?? _metadataServices.FirstOrDefault(current => current.CanRefresh(item)); if (service is null) { - _logger.LogError("Unable to find a metadata service for item of type {TypeName}", item.GetType().Name); + _logger.LogError("Unable to find a metadata service for item of type {TypeName}", type.Name); return Task.FromResult(ItemUpdateType.None); } @@ -160,7 +160,7 @@ namespace MediaBrowser.Providers.Manager // TODO: Isolate this hack into the tvh plugin if (string.IsNullOrEmpty(contentType)) { - if (url.IndexOf("/imagecache/", StringComparison.OrdinalIgnoreCase) != -1) + if (url.Contains("/imagecache/", StringComparison.OrdinalIgnoreCase)) { contentType = "image/png"; } @@ -232,6 +232,11 @@ namespace MediaBrowser.Providers.Manager providers = providers.Where(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase)); } + if (query.ImageType is not null) + { + providers = providers.Where(i => i.GetSupportedImages(item).Contains(query.ImageType.Value)); + } + var preferredLanguage = item.GetPreferredMetadataLanguage(); var tasks = providers.Select(i => GetImages(item, i, preferredLanguage, query.IncludeAllLanguages, cancellationToken, query.ImageType)); @@ -568,13 +573,7 @@ namespace MediaBrowser.Providers.Manager /// <inheritdoc/> public MetadataOptions GetMetadataOptions(BaseItem item) - { - var type = item.GetType().Name; - - return _configurationManager.Configuration.MetadataOptions - .FirstOrDefault(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) ?? - new MetadataOptions(); - } + => _configurationManager.GetMetadataOptionsForType(item.GetType().Name) ?? new MetadataOptions(); /// <inheritdoc/> public Task SaveMetadataAsync(BaseItem item, ItemUpdateType updateType) @@ -766,10 +765,12 @@ namespace MediaBrowser.Providers.Manager { try { - var results = await GetSearchResults(provider, searchInfo.SearchInfo, cancellationToken).ConfigureAwait(false); + var results = await provider.GetSearchResults(searchInfo.SearchInfo, cancellationToken).ConfigureAwait(false); foreach (var result in results) { + result.SearchProviderName = provider.Name; + var existingMatch = resultList.FirstOrDefault(i => i.ProviderIds.Any(p => string.Equals(result.GetProviderId(p.Key), p.Value, StringComparison.OrdinalIgnoreCase))); if (existingMatch is null) @@ -801,37 +802,6 @@ namespace MediaBrowser.Providers.Manager return resultList; } - private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>( - IRemoteSearchProvider<TLookupType> provider, - TLookupType searchInfo, - CancellationToken cancellationToken) - where TLookupType : ItemLookupInfo - { - var results = await provider.GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); - - var list = results.ToList(); - - foreach (var item in list) - { - item.SearchProviderName = provider.Name; - } - - return list; - } - - /// <inheritdoc/> - public Task<HttpResponseMessage> GetSearchImage(string providerName, string url, CancellationToken cancellationToken) - { - var provider = _metadataProviders.OfType<IRemoteSearchProvider>().FirstOrDefault(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase)); - - if (provider is null) - { - throw new ArgumentException("Search provider not found."); - } - - return provider.GetImageResponse(url, cancellationToken); - } - private IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item) { return _externalIds.Where(i => @@ -1102,29 +1072,6 @@ namespace MediaBrowser.Providers.Manager return RefreshItem(item, options, cancellationToken); } - /// <summary> - /// Runs multiple metadata refreshes concurrently. - /// </summary> - /// <param name="action">The action to run.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>A <see cref="Task"/> representing the result of the asynchronous operation.</returns> - public async Task RunMetadataRefresh(Func<Task> action, CancellationToken cancellationToken) - { - // create a variable for this since it is possible MetadataRefreshThrottler could change due to a config update during a scan - var metadataRefreshThrottler = _baseItemManager.MetadataRefreshThrottler; - - await metadataRefreshThrottler.WaitAsync(cancellationToken).ConfigureAwait(false); - - try - { - await action().ConfigureAwait(false); - } - finally - { - metadataRefreshThrottler.Release(); - } - } - /// <inheritdoc/> public void Dispose() { diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index b8578c46f..44f998742 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -1,6 +1,9 @@ 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; @@ -14,6 +17,7 @@ using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; +using Microsoft.Extensions.Logging; using TagLib; namespace MediaBrowser.Providers.MediaInfo @@ -21,8 +25,12 @@ namespace MediaBrowser.Providers.MediaInfo /// <summary> /// Probes audio files for metadata. /// </summary> - public class AudioFileProber + public partial 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; @@ -31,22 +39,28 @@ namespace MediaBrowser.Providers.MediaInfo /// <summary> /// Initializes a new instance of the <see cref="AudioFileProber"/> class. /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> public AudioFileProber( + ILogger<AudioFileProber> logger, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, ILibraryManager libraryManager) { + _logger = logger; _mediaEncoder = mediaEncoder; _itemRepo = itemRepo; _libraryManager = libraryManager; _mediaSourceManager = mediaSourceManager; } + [GeneratedRegex("I:\\s+(.*?)\\s+LUFS")] + private static partial Regex LUFSRegex(); + /// <summary> /// Probes the specified item for metadata. /// </summary> @@ -89,6 +103,55 @@ namespace MediaBrowser.Providers.MediaInfo Fetch(item, result, cancellationToken); } + var libraryOptions = _libraryManager.GetLibraryOptions(item); + + if (libraryOptions.EnableLUFSScan) + { + string output; + 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; + 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; + } + } + } + else + { + item.LUFS = DefaultLUFSValue; + } + + _logger.LogDebug("LUFS for {ItemName} is {LUFS}.", item.Name, item.LUFS); + return ItemUpdateType.MetadataImport; } @@ -161,30 +224,39 @@ namespace MediaBrowser.Providers.MediaInfo var albumArtists = tags.AlbumArtists; foreach (var albumArtist in albumArtists) { - PeopleHelper.AddPerson(people, new PersonInfo + if (!string.IsNullOrEmpty(albumArtist)) { - Name = albumArtist, - Type = PersonKind.AlbumArtist - }); + PeopleHelper.AddPerson(people, new PersonInfo + { + Name = albumArtist, + Type = PersonKind.AlbumArtist + }); + } } var performers = tags.Performers; foreach (var performer in performers) { - PeopleHelper.AddPerson(people, new PersonInfo + if (!string.IsNullOrEmpty(performer)) { - Name = performer, - Type = PersonKind.Artist - }); + PeopleHelper.AddPerson(people, new PersonInfo + { + Name = performer, + Type = PersonKind.Artist + }); + } } foreach (var composer in tags.Composers) { - PeopleHelper.AddPerson(people, new PersonInfo + if (!string.IsNullOrEmpty(composer)) { - Name = composer, - Type = PersonKind.Composer - }); + PeopleHelper.AddPerson(people, new PersonInfo + { + Name = composer, + Type = PersonKind.Composer + }); + } } _libraryManager.UpdatePeople(audio, people); @@ -196,6 +268,7 @@ namespace MediaBrowser.Providers.MediaInfo 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); diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index f58f5f7a3..c24f4e2fc 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -177,9 +177,11 @@ namespace MediaBrowser.Providers.MediaInfo var format = imageStream.Codec switch { + "bmp" => ImageFormat.Bmp, + "gif" => ImageFormat.Gif, "mjpeg" => ImageFormat.Jpg, "png" => ImageFormat.Png, - "gif" => ImageFormat.Gif, + "webp" => ImageFormat.Webp, _ => ImageFormat.Jpg }; diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 213639371..35ea04d21 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -1,11 +1,8 @@ -#nullable disable - #pragma warning disable CA1068, CS1591 using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -83,9 +80,9 @@ namespace MediaBrowser.Providers.MediaInfo CancellationToken cancellationToken) where T : Video { - BlurayDiscInfo blurayDiscInfo = null; + BlurayDiscInfo? blurayDiscInfo = null; - Model.MediaInfo.MediaInfo mediaInfoResult = null; + Model.MediaInfo.MediaInfo? mediaInfoResult = null; if (!item.IsShortcut || options.EnableRemoteContentProbe) { @@ -131,7 +128,7 @@ namespace MediaBrowser.Providers.MediaInfo var m2ts = _mediaEncoder.GetPrimaryPlaylistM2tsFiles(item.Path); // Return if no playable .m2ts files are found - if (blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0) + if (blurayDiscInfo is null || blurayDiscInfo.Files.Length == 0 || m2ts.Count == 0) { _logger.LogError("No playable .m2ts files found in Blu-ray structure, skipping FFprobe."); return ItemUpdateType.MetadataImport; @@ -192,16 +189,14 @@ namespace MediaBrowser.Providers.MediaInfo protected async Task Fetch( Video video, CancellationToken cancellationToken, - Model.MediaInfo.MediaInfo mediaInfo, - BlurayDiscInfo blurayInfo, + Model.MediaInfo.MediaInfo? mediaInfo, + BlurayDiscInfo? blurayInfo, MetadataRefreshOptions options) { - List<MediaStream> mediaStreams; + List<MediaStream> mediaStreams = new List<MediaStream>(); IReadOnlyList<MediaAttachment> mediaAttachments; ChapterInfo[] chapters; - mediaStreams = new List<MediaStream>(); - // Add external streams before adding the streams from the file to preserve stream IDs on remote videos await AddExternalSubtitlesAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false); @@ -221,18 +216,6 @@ namespace MediaBrowser.Providers.MediaInfo video.TotalBitrate = mediaInfo.Bitrate; video.RunTimeTicks = mediaInfo.RunTimeTicks; video.Size = mediaInfo.Size; - - if (video.VideoType == VideoType.VideoFile) - { - var extension = (Path.GetExtension(video.Path) ?? string.Empty).TrimStart('.'); - - video.Container = extension; - } - else - { - video.Container = null; - } - video.Container = mediaInfo.Container; chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>(); @@ -243,8 +226,7 @@ namespace MediaBrowser.Providers.MediaInfo } else { - var currentMediaStreams = video.GetMediaStreams(); - foreach (var mediaStream in currentMediaStreams) + foreach (var mediaStream in video.GetMediaStreams()) { if (!mediaStream.IsExternal) { @@ -295,8 +277,8 @@ namespace MediaBrowser.Providers.MediaInfo _itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken); } - if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || - options.MetadataRefreshMode == MetadataRefreshMode.Default) + if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh + || options.MetadataRefreshMode == MetadataRefreshMode.Default) { if (_config.Configuration.DummyChapterDuration > 0 && chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video)) { @@ -321,11 +303,11 @@ namespace MediaBrowser.Providers.MediaInfo { for (int i = 0; i < chapters.Length; i++) { - string name = chapters[i].Name; + string? name = chapters[i].Name; // Check if the name is empty and/or if the name is a time // Some ripping programs do that. - if (string.IsNullOrWhiteSpace(name) || - TimeSpan.TryParse(name, out _)) + if (string.IsNullOrWhiteSpace(name) + || TimeSpan.TryParse(name, out _)) { chapters[i].Name = string.Format( CultureInfo.InvariantCulture, @@ -384,23 +366,18 @@ namespace MediaBrowser.Providers.MediaInfo // Use the ffprobe values if these are empty if (videoStream is not null) { - videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate; - videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width; - videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height; + videoStream.BitRate = videoStream.BitRate.GetValueOrDefault() == 0 ? currentBitRate : videoStream.BitRate; + videoStream.Width = videoStream.Width.GetValueOrDefault() == 0 ? currentWidth : videoStream.Width; + videoStream.Height = videoStream.Height.GetValueOrDefault() == 0 ? currentHeight : videoStream.Height; } } - private bool IsEmpty(int? num) - { - return !num.HasValue || num.Value == 0; - } - /// <summary> /// Gets information about the longest playlist on a bdrom. /// </summary> /// <param name="path">The path.</param> /// <returns>VideoStream.</returns> - private BlurayDiscInfo GetBDInfo(string path) + private BlurayDiscInfo? GetBDInfo(string path) { ArgumentException.ThrowIfNullOrEmpty(path); @@ -527,32 +504,29 @@ namespace MediaBrowser.Providers.MediaInfo private void FetchPeople(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions options) { - var replaceData = options.ReplaceAllMetadata; + if (video.IsLocked + || video.LockedFields.Contains(MetadataField.Cast) + || data.People.Length == 0) + { + return; + } - if (!video.IsLocked && !video.LockedFields.Contains(MetadataField.Cast)) + if (options.ReplaceAllMetadata || _libraryManager.GetPeople(video).Count == 0) { - if (replaceData || _libraryManager.GetPeople(video).Count == 0) - { - var people = new List<PersonInfo>(); + var people = new List<PersonInfo>(); - foreach (var person in data.People) + foreach (var person in data.People) + { + PeopleHelper.AddPerson(people, new PersonInfo { - PeopleHelper.AddPerson(people, new PersonInfo - { - Name = person.Name, - Type = person.Type, - Role = person.Role - }); - } - - _libraryManager.UpdatePeople(video, people); + Name = person.Name, + Type = person.Type, + Role = person.Role + }); } - } - } - private SubtitleOptions GetOptions() - { - return _config.GetConfiguration<SubtitleOptions>("subtitles"); + _libraryManager.UpdatePeople(video, people); + } } /// <summary> @@ -575,7 +549,7 @@ namespace MediaBrowser.Providers.MediaInfo var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default || options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh; - var subtitleOptions = GetOptions(); + var subtitleOptions = _config.GetConfiguration<SubtitleOptions>("subtitles"); var libraryOptions = _libraryManager.GetLibraryOptions(video); @@ -659,9 +633,9 @@ namespace MediaBrowser.Providers.MediaInfo /// </summary> /// <param name="video">The video.</param> /// <returns>An array of dummy chapters.</returns> - private ChapterInfo[] CreateDummyChapters(Video video) + internal ChapterInfo[] CreateDummyChapters(Video video) { - var runtime = video.RunTimeTicks ?? 0; + var runtime = video.RunTimeTicks.GetValueOrDefault(); // Only process files with a runtime higher than 0 and lower than 12h. The latter are likely corrupted. if (runtime < 0 || runtime > TimeSpan.FromHours(12).Ticks) @@ -671,30 +645,30 @@ namespace MediaBrowser.Providers.MediaInfo CultureInfo.InvariantCulture, "{0} has an invalid runtime of {1} minutes", video.Name, - TimeSpan.FromTicks(runtime).Minutes)); + TimeSpan.FromTicks(runtime).TotalMinutes)); } long dummyChapterDuration = TimeSpan.FromSeconds(_config.Configuration.DummyChapterDuration).Ticks; - if (runtime > dummyChapterDuration) + if (runtime <= dummyChapterDuration) { - int chapterCount = (int)(runtime / dummyChapterDuration); - var chapters = new ChapterInfo[chapterCount]; + return Array.Empty<ChapterInfo>(); + } - long currentChapterTicks = 0; - for (int i = 0; i < chapterCount; i++) - { - chapters[i] = new ChapterInfo - { - StartPositionTicks = currentChapterTicks - }; + int chapterCount = (int)(runtime / dummyChapterDuration); + var chapters = new ChapterInfo[chapterCount]; - currentChapterTicks += dummyChapterDuration; - } + long currentChapterTicks = 0; + for (int i = 0; i < chapterCount; i++) + { + chapters[i] = new ChapterInfo + { + StartPositionTicks = currentChapterTicks + }; - return chapters; + currentChapterTicks += dummyChapterDuration; } - return Array.Empty<ChapterInfo>(); + return chapters; } } } diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 280021955..114a92975 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -79,7 +79,7 @@ namespace MediaBrowser.Providers.MediaInfo NamingOptions namingOptions) { _logger = loggerFactory.CreateLogger<ProbeProvider>(); - _audioProber = new AudioFileProber(mediaSourceManager, mediaEncoder, itemRepo, libraryManager); + _audioProber = new AudioFileProber(loggerFactory.CreateLogger<AudioFileProber>(), mediaSourceManager, mediaEncoder, itemRepo, libraryManager); _audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions); _subtitleResolver = new SubtitleResolver(loggerFactory.CreateLogger<SubtitleResolver>(), localization, mediaEncoder, fileSystem, namingOptions); _videoProber = new FFProbeVideoInfo( diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs index ae244da19..a8461e991 100644 --- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages { var thumbsPath = Path.Combine(_config.ApplicationPaths.CachePath, "imagesbyname", "remotestudiothumbs.txt"); - thumbsPath = await EnsureThumbsList(thumbsPath, cancellationToken).ConfigureAwait(false); + await EnsureThumbsList(thumbsPath, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); @@ -107,7 +107,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages return string.Format(CultureInfo.InvariantCulture, "{0}/images/{1}/{2}.jpg", GetRepositoryUrl(), image, filename); } - private Task<string> EnsureThumbsList(string file, CancellationToken cancellationToken) + private Task EnsureThumbsList(string file, CancellationToken cancellationToken) { string url = string.Format(CultureInfo.InvariantCulture, "{0}/thumbs.txt", GetRepositoryUrl()); @@ -129,7 +129,7 @@ namespace MediaBrowser.Providers.Plugins.StudioImages /// <param name="fileSystem">The file system.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>A Task to ensure existence of a file listing.</returns> - public async Task<string> EnsureList(string url, string file, IFileSystem fileSystem, CancellationToken cancellationToken) + public async Task EnsureList(string url, string file, IFileSystem fileSystem, CancellationToken cancellationToken) { var fileInfo = fileSystem.GetFileInfo(file); @@ -148,8 +148,6 @@ namespace MediaBrowser.Providers.Plugins.StudioImages } } } - - return file; } /// <summary> diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs index 516eee758..a7c93ac4c 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs @@ -11,10 +11,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb /// <summary> /// Utilities for the TMDb provider. /// </summary> - public static class TmdbUtils + public static partial class TmdbUtils { - private static readonly Regex _nonWords = new(@"[\W_]+", RegexOptions.Compiled); - /// <summary> /// URL of the TMDb instance to use. /// </summary> @@ -50,6 +48,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb PersonKind.Producer }; + [GeneratedRegex(@"[\W_]+")] + private static partial Regex NonWordRegex(); + /// <summary> /// Cleans the name according to TMDb requirements. /// </summary> @@ -58,7 +59,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb public static string CleanName(string name) { // TMDb expects a space separated list of words make sure that is the case - return _nonWords.Replace(name, " "); + return NonWordRegex().Replace(name, " "); } /// <summary> diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 0c01c5031..87fd2a3cd 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -200,6 +200,11 @@ namespace MediaBrowser.Providers.Subtitles saveFileName += ".forced"; } + if (response.IsHearingImpaired) + { + saveFileName += ".sdh"; + } + saveFileName += "." + response.Format.ToLowerInvariant(); if (saveInMediaFolder) diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 97f938397..e01c0f483 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -41,7 +42,7 @@ namespace MediaBrowser.Providers.TV RemoveObsoleteEpisodes(item); RemoveObsoleteSeasons(item); - await FillInMissingSeasonsAsync(item, cancellationToken).ConfigureAwait(false); + await UpdateAndCreateSeasonsAsync(item, cancellationToken).ConfigureAwait(false); } /// <inheritdoc /> @@ -67,6 +68,20 @@ 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)) { @@ -86,7 +101,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 FillInMissingSeasonsAsync. + // TODO Legacy. It's not really "physical" seasons as any virtual seasons are always converted to non-virtual in UpdateAndCreateSeasonsAsync. var physicalSeasonNumbers = new HashSet<int>(); var virtualSeasons = new List<Season>(); foreach (var existingSeason in series.Children.OfType<Season>()) @@ -177,36 +192,42 @@ namespace MediaBrowser.Providers.TV } /// <summary> - /// Creates seasons for all episodes that aren't in a season folder. + /// 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 FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken) + private async Task UpdateAndCreateSeasonsAsync(Series series, CancellationToken cancellationToken) { + var seasonNames = series.SeasonNames; var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season); - var episodesInSeriesFolder = seriesChildren + var seasons = seriesChildren.OfType<Season>().ToList(); + var uniqueSeasonNumbers = seriesChildren .OfType<Episode>() - .Where(i => !i.IsInSeasonFolder); - - List<Season> seasons = seriesChildren.OfType<Season>().ToList(); + .Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null) + .Distinct(); // Loop through the unique season numbers - foreach (var episode in episodesInSeriesFolder) + foreach (var seasonNumber in uniqueSeasonNumbers) { // Null season numbers will have a 'dummy' season created because seasons are always required. - var seasonNumber = episode.ParentIndexNumber >= 0 ? episode.ParentIndexNumber : null; 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, seasonNumber, cancellationToken).ConfigureAwait(false); - seasons.Add(season); + var season = await CreateSeasonAsync(series, seasonName, seasonNumber, cancellationToken).ConfigureAwait(false); + series.AddChild(season); } - else if (existingSeason.IsVirtualItem) + else if (!string.Equals(existingSeason.Name, seasonName, StringComparison.Ordinal)) { - existingSeason.IsVirtualItem = false; + existingSeason.Name = seasonName; await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false); } } @@ -216,21 +237,16 @@ namespace MediaBrowser.Providers.TV /// Creates a new season, adds it to the database by linking it to the [series] and refreshes the metadata. /// </summary> /// <param name="series">The series.</param> + /// <param name="seasonName">The season name.</param> /// <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( Series series, + string? seasonName, int? seasonNumber, CancellationToken cancellationToken) { - string seasonName = seasonNumber switch - { - null => _localizationManager.GetLocalizedString("NameSeasonUnknown"), - 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName, - _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value) - }; - Logger.LogInformation("Creating Season {SeasonName} entry for {SeriesName}", seasonName, series.Name); var season = new Season @@ -251,5 +267,20 @@ namespace MediaBrowser.Providers.TV return season; } + + private string GetValidSeasonNameForSeries(Series series, string? seasonName, int? seasonNumber) + { + if (string.IsNullOrEmpty(seasonName)) + { + seasonName = seasonNumber switch + { + null => _localizationManager.GetLocalizedString("NameSeasonUnknown"), + 0 => LibraryManager.GetLibraryOptions(series).SeasonZeroDisplayName, + _ => string.Format(CultureInfo.InvariantCulture, _localizationManager.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value) + }; + } + + return seasonName; + } } } |
