aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Providers
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Providers')
-rw-r--r--MediaBrowser.Providers/Books/Isbn/ISBNExternalId.cs23
-rw-r--r--MediaBrowser.Providers/Books/Isbn/ISBNExternalUrlProvider.cs25
-rw-r--r--MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs120
-rw-r--r--MediaBrowser.Providers/Books/OpenPackagingFormat/EpubProvider.cs100
-rw-r--r--MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs35
-rw-r--r--MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs94
-rw-r--r--MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs329
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs4
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs17
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs7
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj3
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs43
-rw-r--r--MediaBrowser.Providers/MediaInfo/ProbeProvider.cs21
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs46
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs25
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs39
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs4
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs6
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs43
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs70
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs35
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs35
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs33
35 files changed, 1153 insertions, 186 deletions
diff --git a/MediaBrowser.Providers/Books/Isbn/ISBNExternalId.cs b/MediaBrowser.Providers/Books/Isbn/ISBNExternalId.cs
new file mode 100644
index 0000000000..a86275d5ae
--- /dev/null
+++ b/MediaBrowser.Providers/Books/Isbn/ISBNExternalId.cs
@@ -0,0 +1,23 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Books.Isbn
+{
+ /// <inheritdoc />
+ public class IsbnExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "ISBN";
+
+ /// <inheritdoc />
+ public string Key => "ISBN";
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => null;
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Book;
+ }
+}
diff --git a/MediaBrowser.Providers/Books/Isbn/ISBNExternalUrlProvider.cs b/MediaBrowser.Providers/Books/Isbn/ISBNExternalUrlProvider.cs
new file mode 100644
index 0000000000..9d7b1ff208
--- /dev/null
+++ b/MediaBrowser.Providers/Books/Isbn/ISBNExternalUrlProvider.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Books.Isbn;
+
+/// <inheritdoc/>
+public class IsbnExternalUrlProvider : IExternalUrlProvider
+{
+ /// <inheritdoc/>
+ public string Name => "ISBN";
+
+ /// <inheritdoc />
+ public IEnumerable<string> GetExternalUrls(BaseItem item)
+ {
+ if (item.TryGetProviderId("ISBN", out var externalId))
+ {
+ if (item is Book)
+ {
+ yield return $"https://search.worldcat.org/search?q=bn:{externalId}";
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs
new file mode 100644
index 0000000000..69cae77628
--- /dev/null
+++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubImageProvider.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Books.OpenPackagingFormat
+{
+ /// <summary>
+ /// Provides the primary image for EPUB items that have embedded covers.
+ /// </summary>
+ public class EpubImageProvider : IDynamicImageProvider
+ {
+ private readonly ILogger<EpubImageProvider> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EpubImageProvider"/> class.
+ /// </summary>
+ /// <param name="logger">Instance of the <see cref="ILogger{EpubImageProvider}"/> interface.</param>
+ public EpubImageProvider(ILogger<EpubImageProvider> logger)
+ {
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public string Name => "EPUB Metadata";
+
+ /// <inheritdoc />
+ public bool Supports(BaseItem item)
+ {
+ return item is Book;
+ }
+
+ /// <inheritdoc />
+ public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
+ {
+ yield return ImageType.Primary;
+ }
+
+ /// <inheritdoc />
+ public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
+ {
+ if (string.Equals(Path.GetExtension(item.Path), ".epub", StringComparison.OrdinalIgnoreCase))
+ {
+ return GetFromZip(item, cancellationToken);
+ }
+
+ return Task.FromResult(new DynamicImageResponse { HasImage = false });
+ }
+
+ private async Task<DynamicImageResponse> LoadCover(ZipArchive epub, XmlDocument opf, string opfRootDirectory, CancellationToken cancellationToken)
+ {
+ var utilities = new OpfReader<EpubImageProvider>(opf, _logger);
+ var coverReference = utilities.ReadCoverPath(opfRootDirectory);
+ if (coverReference == null)
+ {
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ var cover = coverReference.Value;
+ var coverFile = epub.GetEntry(cover.Path);
+
+ if (coverFile == null)
+ {
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ var memoryStream = new MemoryStream();
+
+ var coverStream = await coverFile.OpenAsync(cancellationToken).ConfigureAwait(false);
+ await using (coverStream.ConfigureAwait(false))
+ {
+ await coverStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
+ }
+
+ memoryStream.Position = 0;
+
+ var response = new DynamicImageResponse { HasImage = true, Stream = memoryStream };
+ response.SetFormatFromMimeType(cover.MimeType);
+
+ return response;
+ }
+
+ private async Task<DynamicImageResponse> GetFromZip(BaseItem item, CancellationToken cancellationToken)
+ {
+ using var epub = await ZipFile.OpenReadAsync(item.Path, cancellationToken).ConfigureAwait(false);
+
+ var opfFilePath = EpubUtils.ReadContentFilePath(epub);
+ if (opfFilePath == null)
+ {
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ var opfRootDirectory = Path.GetDirectoryName(opfFilePath);
+ if (opfRootDirectory == null)
+ {
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ var opfFile = epub.GetEntry(opfFilePath);
+ if (opfFile == null)
+ {
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ using var opfStream = await opfFile.OpenAsync(cancellationToken).ConfigureAwait(false);
+
+ var opfDocument = new XmlDocument();
+ opfDocument.Load(opfStream);
+
+ return await LoadCover(epub, opfDocument, opfRootDirectory, cancellationToken).ConfigureAwait(false);
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubProvider.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubProvider.cs
new file mode 100644
index 0000000000..bc77e5928d
--- /dev/null
+++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubProvider.cs
@@ -0,0 +1,100 @@
+using System;
+using System.IO;
+using System.IO.Compression;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Books.OpenPackagingFormat
+{
+ /// <summary>
+ /// Provides book metadata from OPF content in an EPUB item.
+ /// </summary>
+ public class EpubProvider : ILocalMetadataProvider<Book>
+ {
+ private readonly IFileSystem _fileSystem;
+ private readonly ILogger<EpubProvider> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EpubProvider"/> class.
+ /// </summary>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{EpubProvider}"/> interface.</param>
+ public EpubProvider(IFileSystem fileSystem, ILogger<EpubProvider> logger)
+ {
+ _fileSystem = fileSystem;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public string Name => "EPUB Metadata";
+
+ /// <inheritdoc />
+ public Task<MetadataResult<Book>> GetMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
+ {
+ var path = GetEpubFile(info.Path)?.FullName;
+
+ if (path is null)
+ {
+ return Task.FromResult(new MetadataResult<Book> { HasMetadata = false });
+ }
+
+ var result = ReadEpubAsZip(path, cancellationToken);
+
+ if (result is null)
+ {
+ return Task.FromResult(new MetadataResult<Book> { HasMetadata = false });
+ }
+ else
+ {
+ return Task.FromResult(result);
+ }
+ }
+
+ private FileSystemMetadata? GetEpubFile(string path)
+ {
+ var fileInfo = _fileSystem.GetFileSystemInfo(path);
+
+ if (fileInfo.IsDirectory)
+ {
+ return null;
+ }
+
+ if (!string.Equals(Path.GetExtension(fileInfo.FullName), ".epub", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ return fileInfo;
+ }
+
+ private MetadataResult<Book>? ReadEpubAsZip(string path, CancellationToken cancellationToken)
+ {
+ using var epub = ZipFile.OpenRead(path);
+
+ var opfFilePath = EpubUtils.ReadContentFilePath(epub);
+ if (opfFilePath == null)
+ {
+ return null;
+ }
+
+ var opf = epub.GetEntry(opfFilePath);
+ if (opf == null)
+ {
+ return null;
+ }
+
+ using var opfStream = opf.Open();
+
+ var opfDocument = new XmlDocument();
+ opfDocument.Load(opfStream);
+
+ var utilities = new OpfReader<EpubProvider>(opfDocument, _logger);
+ return utilities.ReadOpfData(cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs
new file mode 100644
index 0000000000..e5d2987312
--- /dev/null
+++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/EpubUtils.cs
@@ -0,0 +1,35 @@
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Xml.Linq;
+
+namespace MediaBrowser.Providers.Books.OpenPackagingFormat
+{
+ /// <summary>
+ /// Utilities for EPUB files.
+ /// </summary>
+ public static class EpubUtils
+ {
+ /// <summary>
+ /// Attempt to read content from ZIP archive.
+ /// </summary>
+ /// <param name="epub">The ZIP archive.</param>
+ /// <returns>The content file path.</returns>
+ public static string? ReadContentFilePath(ZipArchive epub)
+ {
+ var container = epub.GetEntry(Path.Combine("META-INF", "container.xml"));
+ if (container == null)
+ {
+ return null;
+ }
+
+ using var containerStream = container.Open();
+
+ XNamespace containerNamespace = "urn:oasis:names:tc:opendocument:xmlns:container";
+ var containerDocument = XDocument.Load(containerStream);
+ var element = containerDocument.Descendants(containerNamespace + "rootfile").FirstOrDefault();
+
+ return element?.Attribute("full-path")?.Value;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs
new file mode 100644
index 0000000000..6e678802c1
--- /dev/null
+++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfProvider.cs
@@ -0,0 +1,94 @@
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Xml;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Books.OpenPackagingFormat
+{
+ /// <summary>
+ /// Provides metadata for book items that have an OPF file in the same directory. Supports the standard
+ /// content.opf filename, bespoke metadata.opf name from Calibre libraries, and OPF files that have the
+ /// same name as their respective books for directories with several books.
+ /// </summary>
+ public class OpfProvider : ILocalMetadataProvider<Book>, IHasItemChangeMonitor
+ {
+ private const string StandardOpfFile = "content.opf";
+ private const string CalibreOpfFile = "metadata.opf";
+
+ private readonly IFileSystem _fileSystem;
+
+ private readonly ILogger<OpfProvider> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpfProvider"/> class.
+ /// </summary>
+ /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{OpfProvider}"/> interface.</param>
+ public OpfProvider(IFileSystem fileSystem, ILogger<OpfProvider> logger)
+ {
+ _fileSystem = fileSystem;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public string Name => "Open Packaging Format";
+
+ /// <inheritdoc />
+ public bool HasChanged(BaseItem item, IDirectoryService directoryService)
+ {
+ var file = GetXmlFile(item.Path);
+
+ return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved;
+ }
+
+ /// <inheritdoc />
+ public Task<MetadataResult<Book>> GetMetadata(ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken)
+ {
+ var path = GetXmlFile(info.Path).FullName;
+
+ try
+ {
+ return Task.FromResult(ReadOpfData(path, cancellationToken));
+ }
+ catch (FileNotFoundException)
+ {
+ return Task.FromResult(new MetadataResult<Book> { HasMetadata = false });
+ }
+ }
+
+ private FileSystemMetadata GetXmlFile(string path)
+ {
+ var fileInfo = _fileSystem.GetFileSystemInfo(path);
+ var directoryInfo = fileInfo.IsDirectory ? fileInfo : _fileSystem.GetDirectoryInfo(Path.GetDirectoryName(path)!);
+
+ // check for OPF with matching name first since it's the most specific filename
+ var specificFile = Path.Combine(directoryInfo.FullName, Path.GetFileNameWithoutExtension(path) + ".opf");
+ var file = _fileSystem.GetFileInfo(specificFile);
+
+ if (file.Exists)
+ {
+ return file;
+ }
+
+ file = _fileSystem.GetFileInfo(Path.Combine(directoryInfo.FullName, StandardOpfFile));
+
+ // check metadata.opf last since it's really only used by Calibre
+ return file.Exists ? file : _fileSystem.GetFileInfo(Path.Combine(directoryInfo.FullName, CalibreOpfFile));
+ }
+
+ private MetadataResult<Book> ReadOpfData(string file, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var doc = new XmlDocument();
+ doc.Load(file);
+
+ var utilities = new OpfReader<OpfProvider>(doc, _logger);
+ return utilities.ReadOpfData(cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs
new file mode 100644
index 0000000000..5d202c59e1
--- /dev/null
+++ b/MediaBrowser.Providers/Books/OpenPackagingFormat/OpfReader.cs
@@ -0,0 +1,329 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Xml;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Net;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.Books.OpenPackagingFormat
+{
+ /// <summary>
+ /// Methods used to pull metadata and other information from Open Packaging Format in XML objects.
+ /// </summary>
+ /// <typeparam name="TCategoryName">The type of category.</typeparam>
+ public class OpfReader<TCategoryName>
+ {
+ private const string DcNamespace = @"http://purl.org/dc/elements/1.1/";
+ private const string OpfNamespace = @"http://www.idpf.org/2007/opf";
+
+ private readonly XmlNamespaceManager _namespaceManager;
+ private readonly XmlDocument _document;
+
+ private readonly ILogger<TCategoryName> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="OpfReader{TCategoryName}"/> class.
+ /// </summary>
+ /// <param name="document">The XML document to parse.</param>
+ /// <param name="logger">Instance of the <see cref="ILogger{TCategoryName}"/> interface.</param>
+ public OpfReader(XmlDocument document, ILogger<TCategoryName> logger)
+ {
+ _document = document;
+ _logger = logger;
+ _namespaceManager = new XmlNamespaceManager(_document.NameTable);
+
+ _namespaceManager.AddNamespace("dc", DcNamespace);
+ _namespaceManager.AddNamespace("opf", OpfNamespace);
+ }
+
+ /// <summary>
+ /// Checks for the existence of a cover image.
+ /// </summary>
+ /// <param name="opfRootDirectory">The root directory in which the OPF file is located.</param>
+ /// <returns>Returns the found cover and its type or null.</returns>
+ public (string MimeType, string Path)? ReadCoverPath(string opfRootDirectory)
+ {
+ var coverImage = ReadEpubCoverInto(opfRootDirectory, "//opf:item[@properties='cover-image']");
+ if (coverImage is not null)
+ {
+ return coverImage;
+ }
+
+ var coverId = ReadEpubCoverInto(opfRootDirectory, "//opf:item[@id='cover' and @media-type='image/*']");
+ if (coverId is not null)
+ {
+ return coverId;
+ }
+
+ var coverImageId = ReadEpubCoverInto(opfRootDirectory, "//opf:item[@id='*cover-image']");
+ if (coverImageId is not null)
+ {
+ return coverImageId;
+ }
+
+ var metaCoverImage = _document.SelectSingleNode("//opf:meta[@name='cover']", _namespaceManager);
+ var content = metaCoverImage?.Attributes?["content"]?.Value;
+ if (string.IsNullOrEmpty(content) || metaCoverImage is null)
+ {
+ return null;
+ }
+
+ var coverPath = Path.Combine("Images", content);
+ var coverFileManifest = _document.SelectSingleNode($"//opf:item[@href='{coverPath}']", _namespaceManager);
+ var mediaType = coverFileManifest?.Attributes?["media-type"]?.Value;
+ if (coverFileManifest?.Attributes is not null && !string.IsNullOrEmpty(mediaType) && IsValidImage(mediaType))
+ {
+ return (mediaType, Path.Combine(opfRootDirectory, coverPath));
+ }
+
+ var coverFileIdManifest = _document.SelectSingleNode($"//opf:item[@id='{content}']", _namespaceManager);
+ if (coverFileIdManifest is not null)
+ {
+ return ReadManifestItem(coverFileIdManifest, opfRootDirectory);
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Read all supported OPF data from the file.
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The metadata result to update.</returns>
+ public MetadataResult<Book> ReadOpfData(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var book = CreateBookFromOpf();
+ var result = new MetadataResult<Book> { Item = book, HasMetadata = true };
+
+ FindAuthors(result);
+ ReadStringInto("//dc:language", language => result.ResultLanguage = language);
+
+ return result;
+ }
+
+ private Book CreateBookFromOpf()
+ {
+ var book = new Book
+ {
+ Name = FindMainTitle(),
+ ForcedSortName = FindSortTitle(),
+ };
+
+ ReadStringInto("//dc:description", summary => book.Overview = summary);
+ ReadStringInto("//dc:publisher", publisher => book.AddStudio(publisher));
+ ReadStringInto("//dc:identifier[@opf:scheme='AMAZON']", amazon => book.SetProviderId("Amazon", amazon));
+ ReadStringInto("//dc:identifier[@opf:scheme='GOOGLE']", google => book.SetProviderId("GoogleBooks", google));
+ ReadStringInto("//dc:identifier[@opf:scheme='ISBN']", isbn => book.SetProviderId("ISBN", isbn));
+
+ ReadStringInto("//dc:date", date =>
+ {
+ if (DateTime.TryParse(date, out var dateValue))
+ {
+ book.PremiereDate = dateValue.Date;
+ book.ProductionYear = dateValue.Date.Year;
+ }
+ });
+
+ var genreNodes = _document.SelectNodes("//dc:subject", _namespaceManager);
+
+ if (genreNodes?.Count > 0)
+ {
+ foreach (var node in genreNodes.Cast<XmlNode>().Where(node => !string.IsNullOrEmpty(node.InnerText) && !book.Genres.Contains(node.InnerText)))
+ {
+ // specification has no rules about content and some books combine every genre into a single element
+ foreach (var item in node.InnerText.Split(["/", "&", ",", ";", " - "], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
+ {
+ book.AddGenre(item);
+ }
+ }
+ }
+
+ ReadInt32AttributeInto("//opf:meta[@name='calibre:series_index']", index => book.IndexNumber = index);
+ ReadInt32AttributeInto("//opf:meta[@name='calibre:rating']", rating => book.CommunityRating = rating);
+
+ var seriesNameNode = _document.SelectSingleNode("//opf:meta[@name='calibre:series']", _namespaceManager);
+
+ if (!string.IsNullOrEmpty(seriesNameNode?.Attributes?["content"]?.Value))
+ {
+ try
+ {
+ book.SeriesName = seriesNameNode.Attributes["content"]?.Value;
+ }
+ catch (Exception)
+ {
+ _logger.LogError("error parsing Calibre series name");
+ }
+ }
+
+ return book;
+ }
+
+ private string FindMainTitle()
+ {
+ var title = string.Empty;
+ var titleTypes = _document.SelectNodes("//opf:meta[@property='title-type']", _namespaceManager);
+
+ if (titleTypes is not null && titleTypes.Count > 0)
+ {
+ foreach (XmlElement titleNode in titleTypes)
+ {
+ string refines = titleNode.GetAttribute("refines").TrimStart('#');
+ string titleType = titleNode.InnerText;
+
+ var titleElement = _document.SelectSingleNode($"//dc:title[@id='{refines}']", _namespaceManager);
+ if (titleElement is not null && string.Equals(titleType, "main", StringComparison.OrdinalIgnoreCase))
+ {
+ title = titleElement.InnerText;
+ }
+ }
+ }
+
+ // fallback in case there is no main title definition
+ if (string.IsNullOrEmpty(title))
+ {
+ ReadStringInto("//dc:title", titleString => title = titleString);
+ }
+
+ return title;
+ }
+
+ private string? FindSortTitle()
+ {
+ var titleTypes = _document.SelectNodes("//opf:meta[@property='file-as']", _namespaceManager);
+
+ if (titleTypes is not null && titleTypes.Count > 0)
+ {
+ foreach (XmlElement titleNode in titleTypes)
+ {
+ string refines = titleNode.GetAttribute("refines").TrimStart('#');
+ string sortTitle = titleNode.InnerText;
+
+ var titleElement = _document.SelectSingleNode($"//dc:title[@id='{refines}']", _namespaceManager);
+ if (titleElement is not null)
+ {
+ return sortTitle;
+ }
+ }
+ }
+
+ // search for OPF 2.0 style title_sort node
+ var resultElement = _document.SelectSingleNode("//opf:meta[@name='calibre:title_sort']", _namespaceManager);
+ var titleSort = resultElement?.Attributes?["content"]?.Value;
+
+ return titleSort;
+ }
+
+ private void FindAuthors(MetadataResult<Book> book)
+ {
+ var resultElement = _document.SelectNodes("//dc:creator", _namespaceManager);
+
+ if (resultElement != null && resultElement.Count > 0)
+ {
+ foreach (XmlElement creator in resultElement)
+ {
+ var creatorName = creator.InnerText;
+ var role = creator.GetAttribute("opf:role");
+ var person = new PersonInfo { Name = creatorName, Type = GetRole(role) };
+
+ book.AddPerson(person);
+ }
+ }
+ }
+
+ private PersonKind GetRole(string? role)
+ {
+ switch (role)
+ {
+ case "arr":
+ return PersonKind.Arranger;
+ case "art":
+ return PersonKind.Artist;
+ case "aut":
+ case "aqt":
+ case "aft":
+ case "aui":
+ default:
+ return PersonKind.Author;
+ case "edt":
+ return PersonKind.Editor;
+ case "ill":
+ return PersonKind.Illustrator;
+ case "lyr":
+ return PersonKind.Lyricist;
+ case "mus":
+ return PersonKind.AlbumArtist;
+ case "oth":
+ return PersonKind.Unknown;
+ case "trl":
+ return PersonKind.Translator;
+ }
+ }
+
+ private void ReadStringInto(string xmlPath, Action<string> commitResult)
+ {
+ var resultElement = _document.SelectSingleNode(xmlPath, _namespaceManager);
+ if (resultElement is not null && !string.IsNullOrWhiteSpace(resultElement.InnerText))
+ {
+ commitResult(resultElement.InnerText);
+ }
+ }
+
+ private void ReadInt32AttributeInto(string xmlPath, Action<int> commitResult)
+ {
+ var resultElement = _document.SelectSingleNode(xmlPath, _namespaceManager);
+ var resultValue = resultElement?.Attributes?["content"]?.Value;
+
+ if (!string.IsNullOrEmpty(resultValue))
+ {
+ try
+ {
+ commitResult(Convert.ToInt32(Convert.ToDouble(resultValue, CultureInfo.InvariantCulture)));
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "error converting to Int32");
+ }
+ }
+ }
+
+ private (string MimeType, string Path)? ReadEpubCoverInto(string opfRootDirectory, string xmlPath)
+ {
+ var resultElement = _document.SelectSingleNode(xmlPath, _namespaceManager);
+
+ if (resultElement is not null)
+ {
+ return ReadManifestItem(resultElement, opfRootDirectory);
+ }
+
+ return null;
+ }
+
+ private (string MimeType, string Path)? ReadManifestItem(XmlNode manifestNode, string opfRootDirectory)
+ {
+ var href = manifestNode.Attributes?["href"]?.Value;
+ var mediaType = manifestNode.Attributes?["media-type"]?.Value;
+
+ if (string.IsNullOrEmpty(href) || string.IsNullOrEmpty(mediaType) || !IsValidImage(mediaType))
+ {
+ return null;
+ }
+
+ var coverPath = Path.Combine(opfRootDirectory, href);
+
+ return (MimeType: mediaType, Path: coverPath);
+ }
+
+ private static bool IsValidImage(string? mimeType)
+ {
+ return !string.IsNullOrEmpty(mimeType) && !string.IsNullOrWhiteSpace(MimeTypes.ToExtension(mimeType));
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index e0354dbdfa..727f481b65 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -255,7 +255,7 @@ namespace MediaBrowser.Providers.Manager
catch (Exception ex)
{
result.ErrorMessage = ex.Message;
- _logger.LogError(ex, "Error in {Provider}", provider.Name);
+ _logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, item.Path ?? item.Name);
}
}
@@ -339,7 +339,7 @@ namespace MediaBrowser.Providers.Manager
catch (Exception ex)
{
result.ErrorMessage = ex.Message;
- _logger.LogError(ex, "Error in {Provider}", provider.Name);
+ _logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, item.Path ?? item.Name);
}
}
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index a2102ca9cd..abdfb1e3b7 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -153,7 +153,7 @@ namespace MediaBrowser.Providers.Manager
if (isFirstRefresh)
{
- await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+ await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, false, cancellationToken).ConfigureAwait(false);
}
// Next run metadata providers
@@ -247,7 +247,7 @@ namespace MediaBrowser.Providers.Manager
}
// Save to database
- await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
+ await SaveItemAsync(metadataResult, updateType, isFirstRefresh, cancellationToken).ConfigureAwait(false);
}
return updateType;
@@ -275,9 +275,14 @@ namespace MediaBrowser.Providers.Manager
}
}
- protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, CancellationToken cancellationToken)
+ protected async Task SaveItemAsync(MetadataResult<TItemType> result, ItemUpdateType reason, bool reattachUserData, CancellationToken cancellationToken)
{
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
+ if (reattachUserData)
+ {
+ await result.Item.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
+ }
+
if (result.Item.SupportsPeople && result.People is not null)
{
var baseItem = result.Item;
@@ -815,7 +820,7 @@ namespace MediaBrowser.Providers.Manager
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error in {Provider}", provider.Name);
+ Logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, logName);
// If a local provider fails, consider that a failure
refreshResult.ErrorMessage = ex.Message;
@@ -881,7 +886,7 @@ namespace MediaBrowser.Providers.Manager
catch (Exception ex)
{
refreshResult.ErrorMessage = ex.Message;
- Logger.LogError(ex, "Error in {Provider}", provider.Name);
+ Logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, logName);
}
}
@@ -930,7 +935,7 @@ namespace MediaBrowser.Providers.Manager
{
refreshResult.Failures++;
refreshResult.ErrorMessage = ex.Message;
- Logger.LogError(ex, "Error in {Provider}", provider.Name);
+ Logger.LogError(ex, "Error in {Provider} for {Item}", provider.Name, logName);
}
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index f8e2aece1f..0bab73180f 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -487,6 +487,13 @@ namespace MediaBrowser.Providers.Manager
return true;
}
+ // Artists without a folder structure that are derived from metadata have no real path in the library,
+ // so GetLibraryOptions returns null. Allow all providers through rather than blocking them.
+ if (item is MusicArtist && libraryTypeOptions is null)
+ {
+ return true;
+ }
+
return _baseItemManager.IsMetadataFetcherEnabled(item, libraryTypeOptions, provider.Name);
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 34b3104b0b..ed0c63b97f 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -18,7 +18,6 @@
<PackageReference Include="AsyncKeyedLock" />
<PackageReference Include="LrcParser" />
<PackageReference Include="MetaBrainz.MusicBrainz" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
<PackageReference Include="Newtonsoft.Json" />
@@ -28,7 +27,7 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net9.0</TargetFramework>
+ <TargetFramework>net10.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index bde23e842f..fdc2f36469 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -8,7 +8,6 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -25,7 +24,6 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.MediaInfo;
-using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.MediaInfo
@@ -74,7 +72,6 @@ namespace MediaBrowser.Providers.MediaInfo
_subtitleResolver = subtitleResolver;
_mediaAttachmentRepository = mediaAttachmentRepository;
_mediaStreamRepository = mediaStreamRepository;
- _mediaStreamRepository = mediaStreamRepository;
}
public async Task<ItemUpdateType> ProbeVideo<T>(
@@ -366,6 +363,8 @@ namespace MediaBrowser.Providers.MediaInfo
blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace;
blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer;
blurayVideoStream.ColorPrimaries = ffmpegVideoStream.ColorPrimaries;
+ blurayVideoStream.BitDepth = ffmpegVideoStream.BitDepth;
+ blurayVideoStream.PixelFormat = ffmpegVideoStream.PixelFormat;
}
}
@@ -549,47 +548,19 @@ namespace MediaBrowser.Providers.MediaInfo
var enableSubtitleDownloading = options.MetadataRefreshMode == MetadataRefreshMode.Default ||
options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh;
- var subtitleOptions = _config.GetConfiguration<SubtitleOptions>("subtitles");
-
var libraryOptions = _libraryManager.GetLibraryOptions(video);
- string[] subtitleDownloadLanguages;
- bool skipIfEmbeddedSubtitlesPresent;
- bool skipIfAudioTrackMatches;
- bool requirePerfectMatch;
- bool enabled;
-
- if (libraryOptions.SubtitleDownloadLanguages is null)
- {
- subtitleDownloadLanguages = subtitleOptions.DownloadLanguages;
- skipIfEmbeddedSubtitlesPresent = subtitleOptions.SkipIfEmbeddedSubtitlesPresent;
- skipIfAudioTrackMatches = subtitleOptions.SkipIfAudioTrackMatches;
- requirePerfectMatch = subtitleOptions.RequirePerfectMatch;
- enabled = (subtitleOptions.DownloadEpisodeSubtitles &&
- video is Episode) ||
- (subtitleOptions.DownloadMovieSubtitles &&
- video is Movie);
- }
- else
- {
- subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
- skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
- skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
- requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
- enabled = true;
- }
-
- if (enableSubtitleDownloading && enabled)
+ if (enableSubtitleDownloading && libraryOptions.SubtitleDownloadLanguages is not null)
{
var downloadedLanguages = await new SubtitleDownloader(
_logger,
_subtitleManager).DownloadSubtitles(
video,
currentStreams.Concat(externalSubtitleStreams).ToList(),
- skipIfEmbeddedSubtitlesPresent,
- skipIfAudioTrackMatches,
- requirePerfectMatch,
- subtitleDownloadLanguages,
+ libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent,
+ libraryOptions.SkipSubtitlesIfAudioTrackMatches,
+ libraryOptions.RequirePerfectSubtitleMatch,
+ libraryOptions.SubtitleDownloadLanguages,
libraryOptions.DisabledSubtitleFetchers,
libraryOptions.SubtitleFetcherOrder,
true,
diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
index 9f5463b82c..c3ff26202f 100644
--- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
@@ -262,9 +262,28 @@ namespace MediaBrowser.Providers.MediaInfo
private void FetchShortcutInfo(BaseItem item)
{
- item.ShortcutPath = File.ReadAllLines(item.Path)
+ var shortcutPath = File.ReadAllLines(item.Path)
.Select(NormalizeStrmLine)
.FirstOrDefault(i => !string.IsNullOrWhiteSpace(i) && !i.StartsWith('#'));
+
+ if (string.IsNullOrWhiteSpace(shortcutPath))
+ {
+ return;
+ }
+
+ // Only allow remote URLs in .strm files to prevent local file access
+ if (Uri.TryCreate(shortcutPath, UriKind.Absolute, out var uri)
+ && (string.Equals(uri.Scheme, "http", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(uri.Scheme, "https", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(uri.Scheme, "rtsp", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(uri.Scheme, "rtp", StringComparison.OrdinalIgnoreCase)))
+ {
+ item.ShortcutPath = shortcutPath;
+ }
+ else
+ {
+ _logger.LogWarning("Ignoring invalid or non-remote .strm path in {File}: {Path}", item.Path, shortcutPath);
+ }
}
/// <summary>
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
index 1134baf92d..7188e9804e 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -8,14 +8,12 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
@@ -57,16 +55,9 @@ namespace MediaBrowser.Providers.MediaInfo
public bool IsLogged => true;
- private SubtitleOptions GetOptions()
- {
- return _config.GetConfiguration<SubtitleOptions>("subtitles");
- }
-
/// <inheritdoc />
public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
{
- var options = GetOptions();
-
var types = new[] { BaseItemKind.Episode, BaseItemKind.Movie };
var dict = new Dictionary<Guid, BaseItem>();
@@ -81,17 +72,14 @@ namespace MediaBrowser.Providers.MediaInfo
if (libraryOptions.SubtitleDownloadLanguages is null)
{
- subtitleDownloadLanguages = options.DownloadLanguages;
- skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent;
- skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches;
- }
- else
- {
- subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
- skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
- skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
+ // Skip this library if subtitle download languages are not configured
+ continue;
}
+ subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
+ skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
+ skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
+
foreach (var lang in subtitleDownloadLanguages)
{
var query = new InternalItemsQuery
@@ -144,7 +132,7 @@ namespace MediaBrowser.Providers.MediaInfo
try
{
- await DownloadSubtitles(video as Video, options, cancellationToken).ConfigureAwait(false);
+ await DownloadSubtitles(video as Video, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -160,7 +148,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- private async Task<bool> DownloadSubtitles(Video video, SubtitleOptions options, CancellationToken cancellationToken)
+ private async Task<bool> DownloadSubtitles(Video video, CancellationToken cancellationToken)
{
var mediaStreams = video.GetMediaStreams();
@@ -173,19 +161,15 @@ namespace MediaBrowser.Providers.MediaInfo
if (libraryOptions.SubtitleDownloadLanguages is null)
{
- subtitleDownloadLanguages = options.DownloadLanguages;
- skipIfEmbeddedSubtitlesPresent = options.SkipIfEmbeddedSubtitlesPresent;
- skipIfAudioTrackMatches = options.SkipIfAudioTrackMatches;
- requirePerfectMatch = options.RequirePerfectMatch;
- }
- else
- {
- subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
- skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
- skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
- requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
+ // Subtitle downloading is not configured for this library
+ return true;
}
+ subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages;
+ skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent;
+ skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches;
+ requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch;
+
var downloadedLanguages = await new SubtitleDownloader(
_logger,
_subtitleManager).DownloadSubtitles(
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
index 00bd96282c..d8cb6b4b24 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
@@ -125,7 +125,9 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
if (string.IsNullOrWhiteSpace(overview))
{
- overview = result.strBiographyEN;
+ overview = string.IsNullOrWhiteSpace(result.strBiographyEN)
+ ? result.strBiography
+ : result.strBiographyEN;
}
item.Overview = (overview ?? string.Empty).StripHtml();
@@ -224,6 +226,8 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public string strTwitter { get; set; }
+ public string strBiography { get; set; }
+
public string strBiographyEN { get; set; }
public string strBiographyDE { get; set; }
diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs b/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs
new file mode 100644
index 0000000000..8cbd1f89a7
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalId.cs
@@ -0,0 +1,23 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Plugins.ComicVine
+{
+ /// <inheritdoc />
+ public class ComicVineExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "Comic Vine";
+
+ /// <inheritdoc />
+ public string Key => "ComicVine";
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => null;
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Book;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs
new file mode 100644
index 0000000000..9122399179
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ComicVine/ComicVineExternalUrlProvider.cs
@@ -0,0 +1,28 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Plugins.ComicVine;
+
+/// <inheritdoc/>
+public class ComicVineExternalUrlProvider : IExternalUrlProvider
+{
+ /// <inheritdoc/>
+ public string Name => "Comic Vine";
+
+ /// <inheritdoc />
+ public IEnumerable<string> GetExternalUrls(BaseItem item)
+ {
+ if (item.TryGetProviderId("ComicVine", out var externalId))
+ {
+ switch (item)
+ {
+ case Person:
+ case Book:
+ yield return $"https://comicvine.gamespot.com/{externalId}";
+ break;
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs b/MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs
new file mode 100644
index 0000000000..26b8e11380
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/ComicVine/ComicVinePersonExternalId.cs
@@ -0,0 +1,23 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Plugins.ComicVine
+{
+ /// <inheritdoc />
+ public class ComicVinePersonExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "Comic Vine";
+
+ /// <inheritdoc />
+ public string Key => "ComicVine";
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Person;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs b/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs
new file mode 100644
index 0000000000..02d3b36974
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalId.cs
@@ -0,0 +1,23 @@
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Providers.Plugins.GoogleBooks
+{
+ /// <inheritdoc />
+ public class GoogleBooksExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "Google Books";
+
+ /// <inheritdoc />
+ public string Key => "GoogleBooks";
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => null;
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Book;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs
new file mode 100644
index 0000000000..95047ee83e
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/GoogleBooks/GoogleBooksExternalUrlProvider.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Providers.Plugins.GoogleBooks;
+
+/// <inheritdoc/>
+public class GoogleBooksExternalUrlProvider : IExternalUrlProvider
+{
+ /// <inheritdoc />
+ public string Name => "Google Books";
+
+ /// <inheritdoc />
+ public IEnumerable<string> GetExternalUrls(BaseItem item)
+ {
+ if (item.TryGetProviderId("GoogleBooks", out var externalId))
+ {
+ if (item is Book)
+ {
+ yield return $"https://books.google.com/books?id={externalId}";
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
index 1323d2604a..9df21596c5 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
@@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz;
/// <summary>
/// MusicBrainz artist provider.
/// </summary>
-public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable
+public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>, IDisposable, IHasOrder
{
private readonly ILogger<MusicBrainzArtistProvider> _logger;
private Query _musicBrainzQuery;
@@ -42,6 +42,10 @@ public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, Ar
/// <inheritdoc />
public string Name => "MusicBrainz";
+ /// <inheritdoc />
+ /// Runs first to populate the MusicBrainz artist ID used by downstream providers.
+ public int Order => 0;
+
private void ReloadConfig(object? sender, BasePluginConfiguration e)
{
var configuration = (PluginConfiguration)e;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
index 450ee2a337..3eacc4f0f0 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
@@ -33,7 +33,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Api
/// <returns>The image portion of the TMDb client configuration.</returns>
[HttpGet("ClientConfiguration")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public async Task<ConfigImageTypes> TmdbClientConfiguration()
+ public async Task<ConfigImageTypes?> TmdbClientConfiguration()
{
return (await _tmdbClientManager.GetClientConfiguration().ConfigureAwait(false)).Images;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
index 02818a0e24..78be5804e3 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
@@ -75,10 +75,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
var posters = collection.Images.Posters;
var backdrops = collection.Images.Backdrops;
- var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
+ var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0);
- remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
- remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
+ if (posters is not null)
+ {
+ remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
+ }
+
+ if (backdrops is not null)
+ {
+ remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
+ }
return remoteImages;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
index 34c9abae12..a7bba2d539 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
@@ -67,10 +67,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture));
- return new[] { result };
+ return [result];
}
var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, searchInfo.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+ if (collectionSearchResults is null)
+ {
+ return [];
+ }
var collections = new RemoteSearchResult[collectionSearchResults.Count];
for (var i = 0; i < collectionSearchResults.Count; i++)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
index fcc3574107..714c57d361 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
@@ -79,7 +79,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movieTmdbId <= 0)
{
- return Enumerable.Empty<RemoteImageInfo>();
+ return [];
}
// TODO use image languages if All Languages isn't toggled, but there's currently no way to get that value in here
@@ -89,17 +89,28 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movie?.Images is null)
{
- return Enumerable.Empty<RemoteImageInfo>();
+ return [];
}
var posters = movie.Images.Posters;
var backdrops = movie.Images.Backdrops;
var logos = movie.Images.Logos;
- var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count + logos.Count);
+ var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0 + logos?.Count ?? 0);
- remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
- remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
- remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
+ if (posters is not null)
+ {
+ remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
+ }
+
+ if (backdrops is not null)
+ {
+ remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
+ }
+
+ if (logos is not null)
+ {
+ remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
+ }
return remoteImages;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index 414a0a3c9b..ff584ba1de 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -15,6 +15,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using TMDbLib.Objects.Find;
+using TMDbLib.Objects.General;
using TMDbLib.Objects.Search;
namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
@@ -84,7 +85,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
remoteResult.TrySetProviderId(MetadataProvider.Imdb, movie.ImdbId);
- return new[] { remoteResult };
+ return [remoteResult];
}
}
@@ -118,6 +119,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
.ConfigureAwait(false);
}
+ if (movieResults is null)
+ {
+ return [];
+ }
+
var len = movieResults.Count;
var remoteSearchResults = new RemoteSearchResult[len];
for (var i = 0; i < len; i++)
@@ -158,7 +164,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var searchResults = await _tmdbClientManager.SearchMovieAsync(cleanedName, info.Year ?? parsedName.Year ?? 0, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
- if (searchResults.Count > 0)
+ if (searchResults?.Count > 0)
{
tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
@@ -167,7 +173,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (string.IsNullOrEmpty(tmdbId) && !string.IsNullOrEmpty(imdbId))
{
var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
- if (movieResultFromImdbId?.MovieResults.Count > 0)
+ if (movieResultFromImdbId?.MovieResults?.Count > 0)
{
tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
@@ -193,7 +199,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
OriginalTitle = movieResult.OriginalTitle,
Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture),
Tagline = movieResult.Tagline,
- ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray()
+ ProductionLocations = movieResult.ProductionCountries?.Select(pc => pc.Name).ToArray() ?? Array.Empty<string>()
};
var metadataResult = new MetadataResult<Movie>
{
@@ -218,14 +224,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, StringComparison.OrdinalIgnoreCase));
- if (ourRelease is not null)
+ if (ourRelease?.Certification is not null)
{
- movie.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Certification);
+ movie.OfficialRating = TmdbUtils.BuildParentalRating(info.MetadataCountryCode, ourRelease.Certification);
}
else
{
var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
- if (usRelease is not null)
+ if (usRelease?.Certification is not null)
{
movie.OfficialRating = usRelease.Certification;
}
@@ -242,16 +248,23 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var genres = movieResult.Genres;
- foreach (var genre in genres.Select(g => g.Name).Trimmed())
+ if (genres is not null)
{
- movie.AddGenre(genre);
+ foreach (var genre in genres.Select(g => g.Name).Trimmed())
+ {
+ movie.AddGenre(genre);
+ }
}
if (movieResult.Keywords?.Keywords is not null)
{
- for (var i = 0; i < movieResult.Keywords.Keywords.Count; i++)
+ foreach (var keyword in movieResult.Keywords.Keywords)
{
- movie.AddTag(movieResult.Keywords.Keywords[i].Name);
+ var name = keyword.Name;
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ movie.AddTag(name);
+ }
}
}
@@ -303,9 +316,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
- .Where(entry =>
- TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
- TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
+ .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
if (config.HideMissingCrewMembers)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index 4b32d0f6bf..64ab98b262 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -56,13 +56,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
}
result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture));
- result.TrySetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId);
+ result.TrySetProviderId(MetadataProvider.Imdb, personResult.ExternalIds?.ImdbId);
- return new[] { result };
+ return [result];
}
}
var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false);
+ if (personSearchResult is null)
+ {
+ return [];
+ }
var remoteSearchResults = new RemoteSearchResult[personSearchResult.Count];
for (var i = 0; i < personSearchResult.Count; i++)
@@ -91,7 +95,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
if (personTmdbId <= 0)
{
var personSearchResults = await _tmdbClientManager.SearchPersonAsync(info.Name, cancellationToken).ConfigureAwait(false);
- if (personSearchResults.Count > 0)
+ if (personSearchResults?.Count > 0)
{
personTmdbId = personSearchResults[0].Id;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index e30c555cb4..f0e159f098 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -275,9 +275,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
- .Where(entry =>
- TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
- TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
+ .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
if (config.HideMissingCrewMembers)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 1b429039e7..1eb522137d 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -76,7 +76,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
result.Item.Name = seasonResult.Name;
}
- result.Item.TrySetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId);
+ result.Item.TrySetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds?.TvdbId);
// TODO why was this disabled?
var credits = seasonResult.Credits;
@@ -120,9 +120,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
- .Where(entry =>
- TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
- TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
+ .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
if (config.HideMissingCrewMembers)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 5cba84dcb3..f2e7d0c6e4 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -79,11 +79,22 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var posters = series.Images.Posters;
var backdrops = series.Images.Backdrops;
var logos = series.Images.Logos;
- var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count + logos.Count);
+ var remoteImages = new List<RemoteImageInfo>(posters?.Count ?? 0 + backdrops?.Count ?? 0 + logos?.Count ?? 0);
- remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
- remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
- remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
+ if (posters is not null)
+ {
+ remoteImages.AddRange(_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language));
+ }
+
+ if (backdrops is not null)
+ {
+ remoteImages.AddRange(_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language));
+ }
+
+ if (logos is not null)
+ {
+ remoteImages.AddRange(_tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language));
+ }
return remoteImages;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index f0828e8263..7e36c1e204 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -112,6 +112,10 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, searchInfo.MetadataCountryCode, cancellationToken: cancellationToken)
.ConfigureAwait(false);
+ if (tvSearchResults is null)
+ {
+ return [];
+ }
var remoteResults = new RemoteSearchResult[tvSearchResults.Count];
for (var i = 0; i < tvSearchResults.Count; i++)
@@ -141,6 +145,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
+ remoteResult.ProductionYear = series.FirstAirDate?.Year;
return remoteResult;
}
@@ -157,6 +162,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
+ remoteResult.ProductionYear = series.FirstAirDate?.Year;
return remoteResult;
}
@@ -174,7 +180,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Imdb, out var imdbId))
{
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
- if (searchResult?.TvResults.Count > 0)
+ if (searchResult?.TvResults?.Count > 0)
{
tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
@@ -183,7 +189,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (string.IsNullOrEmpty(tmdbId) && info.TryGetProviderId(MetadataProvider.Tvdb, out var tvdbId))
{
var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
- if (searchResult?.TvResults.Count > 0)
+ if (searchResult?.TvResults?.Count > 0)
{
tmdbId = searchResult.TvResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
@@ -198,7 +204,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
var searchResults = await _tmdbClientManager.SearchSeriesAsync(cleanedName, info.MetadataLanguage, info.MetadataCountryCode, info.Year ?? parsedName.Year ?? 0, cancellationToken).ConfigureAwait(false);
- if (searchResults.Count > 0)
+ if (searchResults?.Count > 0)
{
tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
@@ -262,15 +268,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (seriesResult.Keywords?.Results is not null)
{
- for (var i = 0; i < seriesResult.Keywords.Results.Count; i++)
+ foreach (var result in seriesResult.Keywords.Results)
{
- series.AddTag(seriesResult.Keywords.Results[i].Name);
+ var name = result.Name;
+ if (!string.IsNullOrWhiteSpace(name))
+ {
+ series.AddTag(name);
+ }
}
}
series.HomePageUrl = seriesResult.Homepage;
- series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
+ series.RunTimeTicks = seriesResult.EpisodeRunTime?.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
if (Emby.Naming.TV.TvParserHelpers.TryParseSeriesStatus(seriesResult.Status, out var seriesStatus))
{
@@ -279,6 +289,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
series.EndDate = seriesResult.LastAirDate;
series.PremiereDate = seriesResult.FirstAirDate;
+ series.ProductionYear = seriesResult.FirstAirDate?.Year;
var ids = seriesResult.ExternalIds;
if (ids is not null)
@@ -288,21 +299,21 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
series.TrySetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
}
- var contentRatings = seriesResult.ContentRatings.Results ?? new List<ContentRating>();
+ var contentRatings = seriesResult.ContentRatings?.Results ?? new List<ContentRating>();
var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase));
var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
var minimumRelease = contentRatings.FirstOrDefault();
- if (ourRelease is not null)
+ if (ourRelease?.Rating is not null)
{
- series.OfficialRating = TmdbUtils.BuildParentalRating(ourRelease.Iso_3166_1, ourRelease.Rating);
+ series.OfficialRating = TmdbUtils.BuildParentalRating(preferredCountryCode, ourRelease.Rating);
}
- else if (usRelease is not null)
+ else if (usRelease?.Rating is not null)
{
series.OfficialRating = usRelease.Rating;
}
- else if (minimumRelease is not null)
+ else if (minimumRelease?.Rating is not null)
{
series.OfficialRating = minimumRelease.Rating;
}
@@ -347,7 +358,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
Role = actor.Character?.Trim() ?? string.Empty,
Type = PersonKind.Actor,
SortOrder = actor.Order,
- ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath)
+ // NOTE: Null values are filtered out above
+ ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath!)
};
if (actor.Id > 0)
@@ -367,9 +379,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
- .Where(entry =>
- TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
- TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
+ .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
if (config.HideMissingCrewMembers)
{
@@ -390,7 +400,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
Name = crewMember.Name.Trim(),
Role = crewMember.Job?.Trim() ?? string.Empty,
Type = entry.PersonType,
- ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath)
+ // NOTE: Null values are filtered out above
+ ImageUrl = _tmdbClientManager.GetProfileUrl(crewMember.ProfilePath!)
};
if (crewMember.Id > 0)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index fedf345988..274db347ba 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
@@ -195,7 +194,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var series = await GetSeriesAsync(tvShowId, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false);
- var episodeGroupId = series?.EpisodeGroups.Results.Find(g => g.Type == groupType)?.Id;
+ var episodeGroupId = series?.EpisodeGroups?.Results?.Find(g => g.Type == groupType)?.Id;
if (episodeGroupId is null)
{
@@ -263,7 +262,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv episode information or null if not found.</returns>
- public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
+ public async Task<TvEpisode?> GetEpisodeAsync(int tvShowId, int seasonNumber, long episodeNumber, string displayOrder, string? language, string? imageLanguages, string? countryCode, CancellationToken cancellationToken)
{
var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{displayOrder}-{language}";
if (_memoryCache.TryGetValue(key, out TvEpisode? episode))
@@ -276,9 +275,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, countryCode, cancellationToken).ConfigureAwait(false);
if (group is not null)
{
- var season = group.Groups.Find(s => s.Order == seasonNumber);
+ var season = group.Groups?.Find(s => s.Order == seasonNumber);
// Episode order starts at 0
- var ep = season?.Episodes.Find(e => e.Order == episodeNumber - 1);
+ var ep = season?.Episodes?.Find(e => e.Order == episodeNumber - 1);
if (ep is not null)
{
seasonNumber = ep.SeasonNumber;
@@ -382,7 +381,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="year">The year the tv show first aired.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb tv show information.</returns>
- public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, string? countryCode, int year = 0, CancellationToken cancellationToken = default)
+ public async Task<IReadOnlyList<SearchTv>?> SearchSeriesAsync(string name, string language, string? countryCode, int year = 0, CancellationToken cancellationToken = default)
{
var key = $"searchseries-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv>? series) && series is not null)
@@ -396,12 +395,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
.SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken)
.ConfigureAwait(false);
- if (searchResults.Results.Count > 0)
+ if (searchResults?.Results?.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
- return searchResults.Results;
+ return searchResults?.Results;
}
/// <summary>
@@ -410,7 +409,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="name">The name of the person.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb person information.</returns>
- public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToken)
+ public async Task<IReadOnlyList<SearchPerson>?> SearchPersonAsync(string name, CancellationToken cancellationToken)
{
var key = $"searchperson-{name}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson>? person) && person is not null)
@@ -424,12 +423,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
.SearchPersonAsync(name, includeAdult: Plugin.Instance.Configuration.IncludeAdult, cancellationToken: cancellationToken)
.ConfigureAwait(false);
- if (searchResults.Results.Count > 0)
+ if (searchResults?.Results?.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
- return searchResults.Results;
+ return searchResults?.Results;
}
/// <summary>
@@ -439,7 +438,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="language">The movie's language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb movie information.</returns>
- public Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, string language, CancellationToken cancellationToken)
+ public Task<IReadOnlyList<SearchMovie>?> SearchMovieAsync(string name, string language, CancellationToken cancellationToken)
{
return SearchMovieAsync(name, 0, language, null, cancellationToken);
}
@@ -453,7 +452,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb movie information.</returns>
- public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, string? countryCode, CancellationToken cancellationToken)
+ public async Task<IReadOnlyList<SearchMovie>?> SearchMovieAsync(string name, int year, string language, string? countryCode, CancellationToken cancellationToken)
{
var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie>? movies) && movies is not null)
@@ -467,12 +466,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
.SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken)
.ConfigureAwait(false);
- if (searchResults.Results.Count > 0)
+ if (searchResults?.Results?.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
- return searchResults.Results;
+ return searchResults?.Results;
}
/// <summary>
@@ -483,7 +482,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="countryCode">The country code, ISO 3166-1.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The TMDb collection information.</returns>
- public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, string? countryCode, CancellationToken cancellationToken)
+ public async Task<IReadOnlyList<SearchCollection>?> SearchCollectionAsync(string name, string language, string? countryCode, CancellationToken cancellationToken)
{
var key = $"collectionsearch-{name}-{language}";
if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection>? collections) && collections is not null)
@@ -497,12 +496,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
.SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language, countryCode), cancellationToken: cancellationToken)
.ConfigureAwait(false);
- if (searchResults.Results.Count > 0)
+ if (searchResults?.Results?.Count > 0)
{
_memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
}
- return searchResults.Results;
+ return searchResults?.Results;
}
/// <summary>
@@ -511,14 +510,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="size">The image size to fetch.</param>
/// <param name="path">The relative URL of the image.</param>
/// <returns>The absolute URL.</returns>
- private string? GetUrl(string? size, string path)
+ private string? GetUrl(string? size, string? path)
{
if (string.IsNullOrEmpty(path))
{
return null;
}
- return _tmDbClient.GetImageUrl(size, path, true).ToString();
+ // Use "original" as default size if size is null or empty to prevent malformed URLs
+ var imageSize = string.IsNullOrEmpty(size) ? "original" : size;
+
+ return _tmDbClient.GetImageUrl(imageSize, path, true).ToString();
}
/// <summary>
@@ -526,7 +528,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
/// <param name="posterPath">The relative URL of the poster.</param>
/// <returns>The absolute URL.</returns>
- public string? GetPosterUrl(string posterPath)
+ public string? GetPosterUrl(string? posterPath)
{
return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath);
}
@@ -536,7 +538,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// </summary>
/// <param name="actorProfilePath">The relative URL of the profile image.</param>
/// <returns>The absolute URL.</returns>
- public string? GetProfileUrl(string actorProfilePath)
+ public string? GetProfileUrl(string? actorProfilePath)
{
return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath);
}
@@ -639,30 +641,44 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
private static void ValidatePreferences(TMDbConfig config)
{
var imageConfig = config.Images;
+ if (imageConfig is null)
+ {
+ return;
+ }
var pluginConfig = Plugin.Instance.Configuration;
- if (!imageConfig.PosterSizes.Contains(pluginConfig.PosterSize))
+ if (imageConfig.PosterSizes is not null
+ && pluginConfig.PosterSize is not null
+ && !imageConfig.PosterSizes.Contains(pluginConfig.PosterSize))
{
pluginConfig.PosterSize = imageConfig.PosterSizes[^1];
}
- if (!imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize))
+ if (imageConfig.BackdropSizes is not null
+ && pluginConfig.BackdropSize is not null
+ && !imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize))
{
pluginConfig.BackdropSize = imageConfig.BackdropSizes[^1];
}
- if (!imageConfig.LogoSizes.Contains(pluginConfig.LogoSize))
+ if (imageConfig.LogoSizes is not null
+ && pluginConfig.LogoSize is not null
+ && !imageConfig.LogoSizes.Contains(pluginConfig.LogoSize))
{
pluginConfig.LogoSize = imageConfig.LogoSizes[^1];
}
- if (!imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize))
+ if (imageConfig.ProfileSizes is not null
+ && pluginConfig.ProfileSize is not null
+ && !imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize))
{
pluginConfig.ProfileSize = imageConfig.ProfileSizes[^1];
}
- if (!imageConfig.StillSizes.Contains(pluginConfig.StillSize))
+ if (imageConfig.StillSizes is not null
+ && pluginConfig.StillSize is not null
+ && !imageConfig.StillSizes.Contains(pluginConfig.StillSize))
{
pluginConfig.StillSize = imageConfig.StillSizes[^1];
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index f5e59a2789..39c0497bed 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -69,19 +69,20 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>The Jellyfin person type.</returns>
public static PersonKind MapCrewToPersonType(Crew crew)
{
- if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
- && crew.Job.Contains("director", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(crew.Department, "directing", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(crew.Job, "director", StringComparison.OrdinalIgnoreCase))
{
return PersonKind.Director;
}
- if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
- && crew.Job.Contains("producer", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(crew.Department, "production", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(crew.Job, "producer", StringComparison.OrdinalIgnoreCase))
{
return PersonKind.Producer;
}
- if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(crew.Department, "writing", StringComparison.OrdinalIgnoreCase)
+ && (string.Equals(crew.Job, "writer", StringComparison.OrdinalIgnoreCase) || string.Equals(crew.Job, "screenplay", StringComparison.OrdinalIgnoreCase)))
{
return PersonKind.Writer;
}
@@ -96,9 +97,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>A boolean indicating whether the video is a trailer.</returns>
public static bool IsTrailerType(Video video)
{
- return video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase)
- && (video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase)
- || video.Type.Equals("teaser", StringComparison.OrdinalIgnoreCase));
+ return string.Equals(video.Site, "youtube", StringComparison.OrdinalIgnoreCase)
+ && (string.Equals(video.Type, "trailer", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(video.Type, "teaser", StringComparison.OrdinalIgnoreCase));
}
/// <summary>
@@ -116,14 +117,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
preferredLanguage = NormalizeLanguage(preferredLanguage, countryCode);
languages.Add(preferredLanguage);
-
- if (preferredLanguage.Length == 5) // Like en-US
- {
- // Currently, TMDb supports 2-letter language codes only.
- // They are planning to change this in the future, thus we're
- // supplying both codes if we're having a 5-letter code.
- languages.Add(preferredLanguage.Substring(0, 2));
- }
}
languages.Add("null");
@@ -184,10 +177,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <param name="imageLanguage">The image's actual language code.</param>
/// <param name="requestLanguage">The requested language code.</param>
/// <returns>The language code.</returns>
- public static string AdjustImageLanguage(string imageLanguage, string requestLanguage)
+ public static string AdjustImageLanguage(string? imageLanguage, string requestLanguage)
{
- if (!string.IsNullOrEmpty(imageLanguage)
- && !string.IsNullOrEmpty(requestLanguage)
+ if (string.IsNullOrEmpty(imageLanguage))
+ {
+ return string.Empty;
+ }
+
+ if (!string.IsNullOrEmpty(requestLanguage)
&& requestLanguage.Length > 2
&& imageLanguage.Length == 2
&& requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase))
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index ae5e1090ad..a78ec995cf 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Emby.Naming.Common;
using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
@@ -32,6 +33,7 @@ namespace MediaBrowser.Providers.Subtitles
private readonly ILibraryMonitor _monitor;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly ILocalizationManager _localization;
+ private readonly HashSet<string> _allowedSubtitleFormats;
private readonly ISubtitleProvider[] _subtitleProviders;
@@ -41,7 +43,8 @@ namespace MediaBrowser.Providers.Subtitles
ILibraryMonitor monitor,
IMediaSourceManager mediaSourceManager,
ILocalizationManager localizationManager,
- IEnumerable<ISubtitleProvider> subtitleProviders)
+ IEnumerable<ISubtitleProvider> subtitleProviders,
+ NamingOptions namingOptions)
{
_logger = logger;
_fileSystem = fileSystem;
@@ -51,6 +54,9 @@ namespace MediaBrowser.Providers.Subtitles
_subtitleProviders = subtitleProviders
.OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0)
.ToArray();
+ _allowedSubtitleFormats = new HashSet<string>(
+ namingOptions.SubtitleFileExtensions.Select(e => e.TrimStart('.')),
+ StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
@@ -171,6 +177,12 @@ namespace MediaBrowser.Providers.Subtitles
/// <inheritdoc />
public Task UploadSubtitle(Video video, SubtitleResponse response)
{
+ var format = response.Format;
+ if (string.IsNullOrEmpty(format) || !_allowedSubtitleFormats.Contains(format))
+ {
+ throw new ArgumentException($"Unsupported subtitle format: '{format}'");
+ }
+
var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video);
return TrySaveSubtitle(video, libraryOptions, response);
}
@@ -193,7 +205,13 @@ namespace MediaBrowser.Providers.Subtitles
}
var savePaths = new List<string>();
- var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
+ var language = response.Language.ToLowerInvariant();
+ if (language.AsSpan().IndexOfAny(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) >= 0)
+ {
+ throw new ArgumentException("Language contains invalid characters.");
+ }
+
+ var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + language;
if (response.IsForced)
{
@@ -221,15 +239,22 @@ namespace MediaBrowser.Providers.Subtitles
private async Task TrySaveToFiles(Stream stream, List<string> savePaths, Video video, string extension)
{
+ if (!_allowedSubtitleFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException($"Invalid subtitle format: {extension}");
+ }
+
List<Exception>? exs = null;
foreach (var savePath in savePaths)
{
- var path = savePath + "." + extension;
+ var path = Path.GetFullPath(savePath + "." + extension);
try
{
- if (path.StartsWith(video.ContainingFolderPath, StringComparison.Ordinal)
- || path.StartsWith(video.GetInternalMetadataPath(), StringComparison.Ordinal))
+ var containingFolder = video.ContainingFolderPath + Path.DirectorySeparatorChar;
+ var metadataFolder = video.GetInternalMetadataPath() + Path.DirectorySeparatorChar;
+ if (path.StartsWith(containingFolder, StringComparison.Ordinal)
+ || path.StartsWith(metadataFolder, StringComparison.Ordinal))
{
var fileExists = File.Exists(path);
var counter = 0;
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index c3a6ddd6ae..61a31fbfd6 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@@ -201,6 +202,26 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
false);
}
+ private static bool NeedsVirtualSeason(Episode episode, HashSet<Guid> physicalSeasonIds, HashSet<string> physicalSeasonPaths)
+ {
+ // Episode has a known season number, needs a season
+ if (episode.ParentIndexNumber.HasValue)
+ {
+ return true;
+ }
+
+ // Not yet processed
+ if (episode.SeasonId.IsEmpty())
+ {
+ return false;
+ }
+
+ // Episode has been processed, only needs a virtual season if it isn't
+ // already linked to a known physical season by ID or path
+ return !physicalSeasonIds.Contains(episode.SeasonId)
+ && !physicalSeasonPaths.Contains(System.IO.Path.GetDirectoryName(episode.Path) ?? string.Empty);
+ }
+
/// <summary>
/// Creates seasons for all episodes if they don't exist.
/// If no season number can be determined, a dummy season will be created.
@@ -212,8 +233,20 @@ public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
{
var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
var seasons = seriesChildren.OfType<Season>().ToList();
+
+ var physicalSeasonIds = seasons
+ .Where(e => e.LocationType != LocationType.Virtual)
+ .Select(e => e.Id)
+ .ToHashSet();
+
+ var physicalSeasonPathSet = seasons
+ .Where(e => e.LocationType != LocationType.Virtual && !string.IsNullOrEmpty(e.Path))
+ .Select(e => e.Path)
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
var uniqueSeasonNumbers = seriesChildren
.OfType<Episode>()
+ .Where(e => NeedsVirtualSeason(e, physicalSeasonIds, physicalSeasonPathSet))
.Select(e => e.ParentIndexNumber >= 0 ? e.ParentIndexNumber : null)
.Distinct();