aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Providers
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Providers')
-rw-r--r--MediaBrowser.Providers/Lyric/DefaultLyricProvider.cs69
-rw-r--r--MediaBrowser.Providers/Lyric/ILyricProvider.cs36
-rw-r--r--MediaBrowser.Providers/Lyric/LrcLyricParser.cs (renamed from MediaBrowser.Providers/Lyric/LrcLyricProvider.cs)53
-rw-r--r--MediaBrowser.Providers/Lyric/LyricManager.cs22
-rw-r--r--MediaBrowser.Providers/Lyric/TxtLyricParser.cs44
-rw-r--r--MediaBrowser.Providers/Lyric/TxtLyricProvider.cs60
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs42
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs22
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs79
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioFileProber.cs99
-rw-r--r--MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs4
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs128
-rw-r--r--MediaBrowser.Providers/MediaInfo/ProbeProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs8
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs9
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs5
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs73
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;
+ }
}
}