aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Providers
diff options
context:
space:
mode:
authorCody Robibero <cody@robibe.ro>2021-12-24 02:41:50 +0000
committerGitHub <noreply@github.com>2021-12-24 02:41:50 +0000
commit634ce40c2facfbfaf6454ad8d3a7f2aca4723b46 (patch)
treecf2f3c660f4dfbd8ea19614a10fc1cc0052d55de /MediaBrowser.Providers
parent6648b7d7dabeaa84835fc7a8a7a2a468a00cad5c (diff)
parentb5459f49d32d0fce3944f816915fb7380fd84681 (diff)
Merge branch 'master' into comparisons
Diffstat (limited to 'MediaBrowser.Providers')
-rw-r--r--MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs8
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs34
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs222
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs33
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs54
-rw-r--r--MediaBrowser.Providers/Manager/ProviderUtils.cs6
-rw-r--r--MediaBrowser.Providers/Manager/RefreshResult.cs2
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj14
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs40
-rw-r--r--MediaBrowser.Providers/MediaInfo/AudioResolver.cs176
-rw-r--r--MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs248
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs2
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs76
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs37
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs12
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs52
-rw-r--r--MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs6
-rw-r--r--MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs129
-rw-r--r--MediaBrowser.Providers/Movies/ImdbExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Music/AlbumInfoExtensions.cs12
-rw-r--r--MediaBrowser.Providers/Music/AlbumMetadataService.cs4
-rw-r--r--MediaBrowser.Providers/Music/ImvdbId.cs2
-rw-r--r--MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs (renamed from MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs)7
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs (renamed from MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs)30
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs (renamed from MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs)7
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs (renamed from MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs)27
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs1
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs119
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs426
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs (renamed from MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs)27
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs28
-rw-r--r--MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs21
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs44
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs41
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs37
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs56
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs226
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs191
-rw-r--r--MediaBrowser.Providers/Plugins/Omdb/Plugin.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs24
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html58
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs43
-rw-r--r--MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs (renamed from MediaBrowser.Providers/Studios/StudiosImageProvider.cs)24
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs41
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs44
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs50
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html137
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs44
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs20
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs60
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs26
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs24
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs41
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs203
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs7
-rw-r--r--MediaBrowser.Providers/Properties/AssemblyInfo.cs2
-rw-r--r--MediaBrowser.Providers/Studios/StudioMetadataService.cs3
-rw-r--r--MediaBrowser.Providers/Subtitles/SubtitleManager.cs33
-rw-r--r--MediaBrowser.Providers/TV/EpisodeMetadataService.cs16
-rw-r--r--MediaBrowser.Providers/TV/SeasonMetadataService.cs14
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs11
-rw-r--r--MediaBrowser.Providers/TV/Zap2ItExternalId.cs2
86 files changed, 2372 insertions, 1278 deletions
diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
index e5326da71..88ce8d087 100644
--- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
+++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
@@ -59,9 +59,9 @@ namespace MediaBrowser.Providers.BoxSets
}
/// <inheritdoc />
- protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType currentUpdateType)
+ protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType updateType)
{
- var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
+ var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
var libraryFolderIds = item.GetLibraryFolderIds();
@@ -69,10 +69,10 @@ namespace MediaBrowser.Providers.BoxSets
if (itemLibraryFolderIds == null || !libraryFolderIds.SequenceEqual(itemLibraryFolderIds))
{
item.LibraryFolderIds = libraryFolderIds;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
- return updateType;
+ return updatedType;
}
}
}
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index fb1d4f490..4632e1d51 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -29,8 +32,6 @@ namespace MediaBrowser.Providers.Manager
/// </summary>
public class ImageSaver
{
- private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
-
/// <summary>
/// The _config.
/// </summary>
@@ -91,7 +92,7 @@ namespace MediaBrowser.Providers.Manager
throw new ArgumentNullException(nameof(mimeType));
}
- var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.ExtraType.HasValue && !(item is Audio);
+ var saveLocally = item.SupportsLocalMetadata && item.IsSaveLocalMetadataEnabled() && !item.ExtraType.HasValue && item is not Audio;
if (type != ImageType.Primary && item is Episode)
{
@@ -173,7 +174,7 @@ namespace MediaBrowser.Providers.Manager
// Delete the current path
if (currentImageIsLocalFile
- && !savedPaths.Contains(currentImagePath, StringComparer.OrdinalIgnoreCase)
+ && !savedPaths.Contains(currentImagePath, StringComparison.OrdinalIgnoreCase)
&& (saveLocally || currentImagePath.Contains(_config.ApplicationPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase)))
{
var currentPath = currentImagePath;
@@ -263,8 +264,10 @@ namespace MediaBrowser.Providers.Manager
_fileSystem.SetAttributes(path, false, false);
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous))
+ var fileStreamOptions = AsyncFile.WriteOptions;
+ fileStreamOptions.Mode = FileMode.Create;
+ fileStreamOptions.PreallocationSize = source.Length;
+ await using (var fs = new FileStream(path, fileStreamOptions))
{
await source.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
@@ -377,7 +380,7 @@ namespace MediaBrowser.Providers.Manager
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
- : season.IndexNumber.Value.ToString("00", UsCulture);
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-landscape" + extension;
@@ -400,7 +403,7 @@ namespace MediaBrowser.Providers.Manager
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
- : season.IndexNumber.Value.ToString("00", UsCulture);
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-banner" + extension;
@@ -437,9 +440,6 @@ namespace MediaBrowser.Providers.Manager
case ImageType.Backdrop:
filename = GetBackdropSaveFilename(item.GetImages(type), "backdrop", "backdrop", imageIndex);
break;
- case ImageType.Screenshot:
- filename = GetBackdropSaveFilename(item.GetImages(type), "screenshot", "screenshot", imageIndex);
- break;
default:
filename = type.ToString().ToLowerInvariant();
break;
@@ -495,12 +495,12 @@ namespace MediaBrowser.Providers.Manager
var filenames = images.Select(i => Path.GetFileNameWithoutExtension(i.Path)).ToList();
var current = 1;
- while (filenames.Contains(numberedIndexPrefix + current.ToString(UsCulture), StringComparer.OrdinalIgnoreCase))
+ while (filenames.Contains(numberedIndexPrefix + current.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase))
{
current++;
}
- return numberedIndexPrefix + current.ToString(UsCulture);
+ return numberedIndexPrefix + current.ToString(CultureInfo.InvariantCulture);
}
/// <summary>
@@ -539,7 +539,7 @@ namespace MediaBrowser.Providers.Manager
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
- : season.IndexNumber.Value.ToString("00", UsCulture);
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-fanart" + extension;
@@ -556,7 +556,7 @@ namespace MediaBrowser.Providers.Manager
if (item.IsInMixedFolder)
{
- return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart" + outputIndex.ToString(UsCulture), extension) };
+ return new[] { GetSavePathForItemInMixedFolder(item, type, "fanart" + outputIndex.ToString(CultureInfo.InvariantCulture), extension) };
}
var extraFanartFilename = GetBackdropSaveFilename(item.GetImages(ImageType.Backdrop), "fanart", "fanart", outputIndex);
@@ -568,7 +568,7 @@ namespace MediaBrowser.Providers.Manager
if (EnableExtraThumbsDuplication)
{
- list.Add(Path.Combine(item.ContainingFolderPath, "extrathumbs", "thumb" + outputIndex.ToString(UsCulture) + extension));
+ list.Add(Path.Combine(item.ContainingFolderPath, "extrathumbs", "thumb" + outputIndex.ToString(CultureInfo.InvariantCulture) + extension));
}
return list.ToArray();
@@ -582,7 +582,7 @@ namespace MediaBrowser.Providers.Manager
var seasonMarker = season.IndexNumber.Value == 0
? "-specials"
- : season.IndexNumber.Value.ToString("00", UsCulture);
+ : season.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture);
var imageFilename = "season" + seasonMarker + "-poster" + extension;
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index 966a3d822..b1d73c4c4 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#nullable disable
using System;
using System.Collections.Generic;
@@ -23,6 +23,9 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Manager
{
+ /// <summary>
+ /// Utilities for managing images attached to items.
+ /// </summary>
public class ItemImageProvider
{
private readonly ILogger _logger;
@@ -45,6 +48,12 @@ namespace MediaBrowser.Providers.Manager
ImageType.Thumb
};
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ItemImageProvider"/> class.
+ /// </summary>
+ /// <param name="logger">The logger.</param>
+ /// <param name="providerManager">The provider manager for interacting with provider image references.</param>
+ /// <param name="fileSystem">The filesystem.</param>
public ItemImageProvider(ILogger logger, IProviderManager providerManager, IFileSystem fileSystem)
{
_logger = logger;
@@ -52,11 +61,18 @@ namespace MediaBrowser.Providers.Manager
_fileSystem = fileSystem;
}
+ /// <summary>
+ /// Verifies existing images have valid paths and adds any new local images provided.
+ /// </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>
+ /// <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)
{
var hasChanges = false;
- if (!(item is Photo))
+ if (item is not Photo)
{
var images = providers.OfType<ILocalImageProvider>()
.SelectMany(i => i.GetImages(item, directoryService))
@@ -71,6 +87,15 @@ namespace MediaBrowser.Providers.Manager
return hasChanges;
}
+ /// <summary>
+ /// Refreshes from the providers according to the given options.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/> to gather images for.</param>
+ /// <param name="libraryOptions">The library options.</param>
+ /// <param name="providers">The providers to query for images.</param>
+ /// <param name="refreshOptions">The refresh options.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The refresh result.</returns>
public async Task<RefreshResult> RefreshImages(
BaseItem item,
LibraryOptions libraryOptions,
@@ -78,14 +103,10 @@ namespace MediaBrowser.Providers.Manager
ImageRefreshOptions refreshOptions,
CancellationToken cancellationToken)
{
+ var oldBackdropImages = Array.Empty<ItemImageInfo>();
if (refreshOptions.IsReplacingImage(ImageType.Backdrop))
{
- ClearImages(item, ImageType.Backdrop);
- }
-
- if (refreshOptions.IsReplacingImage(ImageType.Screenshot))
- {
- ClearImages(item, ImageType.Screenshot);
+ oldBackdropImages = item.GetImages(ImageType.Backdrop).ToArray();
}
var result = new RefreshResult { UpdateType = ItemUpdateType.None };
@@ -93,16 +114,15 @@ namespace MediaBrowser.Providers.Manager
var typeName = item.GetType().Name;
var typeOptions = libraryOptions.GetTypeOptions(typeName) ?? new TypeOptions { Type = typeName };
- // In order to avoid duplicates, only download these if there are none already
- var backdropLimit = typeOptions.GetLimit(ImageType.Backdrop);
- var screenshotLimit = typeOptions.GetLimit(ImageType.Screenshot);
+ // track library limits, adding buffer to allow lazy replacing of current images
+ var backdropLimit = typeOptions.GetLimit(ImageType.Backdrop) + oldBackdropImages.Length;
var downloadedImages = new List<ImageType>();
foreach (var provider in providers)
{
if (provider is IRemoteImageProvider remoteProvider)
{
- await RefreshFromProvider(item, libraryOptions, remoteProvider, refreshOptions, typeOptions, backdropLimit, screenshotLimit, downloadedImages, result, cancellationToken).ConfigureAwait(false);
+ await RefreshFromProvider(item, remoteProvider, refreshOptions, typeOptions, backdropLimit, downloadedImages, result, cancellationToken).ConfigureAwait(false);
continue;
}
@@ -112,11 +132,17 @@ namespace MediaBrowser.Providers.Manager
}
}
+ // only delete existing multi-images if new ones were added
+ if (oldBackdropImages.Length > 0 && oldBackdropImages.Length < item.GetImages(ImageType.Backdrop).Count())
+ {
+ PruneImages(item, oldBackdropImages);
+ }
+
return result;
}
/// <summary>
- /// Refreshes from provider.
+ /// Refreshes from a dynamic provider.
/// </summary>
private async Task RefreshFromProvider(
BaseItem item,
@@ -151,19 +177,20 @@ namespace MediaBrowser.Providers.Manager
if (response.Protocol == MediaProtocol.Http)
{
_logger.LogDebug("Setting image url into item {0}", item.Id);
+ var index = item.AllowsMultipleImages(imageType) ? item.GetImages(imageType).Count() : 0;
item.SetImage(
new ItemImageInfo
{
Path = response.Path,
Type = imageType
},
- 0);
+ index);
}
else
{
var mimeType = MimeTypes.GetMimeType(response.Path);
- var stream = new FileStream(response.Path, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true);
+ var stream = AsyncFile.OpenRead(response.Path);
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
}
@@ -188,7 +215,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}", provider.Name);
}
}
@@ -204,9 +231,8 @@ namespace MediaBrowser.Providers.Manager
/// <param name="images">The images.</param>
/// <param name="savedOptions">The saved options.</param>
/// <param name="backdropLimit">The backdrop limit.</param>
- /// <param name="screenshotLimit">The screenshot limit.</param>
/// <returns><c>true</c> if the specified item contains images; otherwise, <c>false</c>.</returns>
- private bool ContainsImages(BaseItem item, List<ImageType> images, TypeOptions savedOptions, int backdropLimit, int screenshotLimit)
+ private bool ContainsImages(BaseItem item, List<ImageType> images, TypeOptions savedOptions, int backdropLimit)
{
// Using .Any causes the creation of a DisplayClass aka. variable capture
for (var i = 0; i < _singularImages.Length; i++)
@@ -223,36 +249,27 @@ namespace MediaBrowser.Providers.Manager
return false;
}
- if (images.Contains(ImageType.Screenshot) && item.GetImages(ImageType.Screenshot).Count() < screenshotLimit)
- {
- return false;
- }
-
return true;
}
/// <summary>
- /// Refreshes from provider.
+ /// Refreshes from a remote provider.
/// </summary>
/// <param name="item">The item.</param>
- /// <param name="libraryOptions">The library options.</param>
/// <param name="provider">The provider.</param>
/// <param name="refreshOptions">The refresh options.</param>
/// <param name="savedOptions">The saved options.</param>
/// <param name="backdropLimit">The backdrop limit.</param>
- /// <param name="screenshotLimit">The screenshot limit.</param>
/// <param name="downloadedImages">The downloaded images.</param>
/// <param name="result">The result.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
private async Task RefreshFromProvider(
BaseItem item,
- LibraryOptions libraryOptions,
IRemoteImageProvider provider,
ImageRefreshOptions refreshOptions,
TypeOptions savedOptions,
int backdropLimit,
- int screenshotLimit,
ICollection<ImageType> downloadedImages,
RefreshResult result,
CancellationToken cancellationToken)
@@ -266,7 +283,7 @@ namespace MediaBrowser.Providers.Manager
if (!refreshOptions.ReplaceAllImages &&
refreshOptions.ReplaceImages.Length == 0 &&
- ContainsImages(item, provider.GetSupportedImages(item).ToList(), savedOptions, backdropLimit, screenshotLimit))
+ ContainsImages(item, provider.GetSupportedImages(item).ToList(), savedOptions, backdropLimit))
{
return;
}
@@ -277,7 +294,7 @@ namespace MediaBrowser.Providers.Manager
item,
new RemoteImageQuery(provider.Name)
{
- IncludeAllLanguages = false,
+ IncludeAllLanguages = true,
IncludeDisabledProviders = false,
},
cancellationToken).ConfigureAwait(false);
@@ -295,7 +312,7 @@ namespace MediaBrowser.Providers.Manager
if (!HasImage(item, imageType) || (refreshOptions.IsReplacingImage(imageType) && !downloadedImages.Contains(imageType)))
{
minWidth = savedOptions.GetMinWidth(imageType);
- var downloaded = await DownloadImage(item, libraryOptions, provider, result, list, minWidth, imageType, cancellationToken).ConfigureAwait(false);
+ var downloaded = await DownloadImage(item, provider, result, list, minWidth, imageType, cancellationToken).ConfigureAwait(false);
if (downloaded)
{
@@ -305,13 +322,7 @@ namespace MediaBrowser.Providers.Manager
}
minWidth = savedOptions.GetMinWidth(ImageType.Backdrop);
- await DownloadBackdrops(item, libraryOptions, ImageType.Backdrop, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
-
- if (item is IHasScreenshots hasScreenshots)
- {
- minWidth = savedOptions.GetMinWidth(ImageType.Screenshot);
- await DownloadBackdrops(item, libraryOptions, ImageType.Screenshot, screenshotLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
- }
+ await DownloadMultiImages(item, ImageType.Backdrop, refreshOptions, backdropLimit, provider, result, list, minWidth, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -320,7 +331,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}", provider.Name);
}
}
@@ -329,40 +340,36 @@ namespace MediaBrowser.Providers.Manager
return options.IsEnabled(type);
}
- private void ClearImages(BaseItem item, ImageType type)
+ private void PruneImages(BaseItem item, ItemImageInfo[] images)
{
- var deleted = false;
- var deletedImages = new List<ItemImageInfo>();
-
- foreach (var image in item.GetImages(type))
+ for (var i = 0; i < images.Length; i++)
{
- if (!image.IsLocalFile)
- {
- deletedImages.Add(image);
- continue;
- }
+ var image = images[i];
- try
- {
- _fileSystem.DeleteFile(image.Path);
- deleted = true;
- }
- catch (FileNotFoundException)
+ if (image.IsLocalFile)
{
+ try
+ {
+ _fileSystem.DeleteFile(image.Path);
+ }
+ catch (FileNotFoundException)
+ {
+ }
}
}
- item.RemoveImages(deletedImages);
-
- if (deleted)
- {
- item.ValidateImages(new DirectoryService(_fileSystem));
- }
+ item.RemoveImages(images);
}
- public bool MergeImages(BaseItem item, List<LocalImageInfo> images)
+ /// <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>
+ /// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns>
+ public bool MergeImages(BaseItem item, IReadOnlyList<LocalImageInfo> images)
{
- var changed = false;
+ var changed = item.ValidateImages(new DirectoryService(_fileSystem));
for (var i = 0; i < _singularImages.Length; i++)
{
@@ -398,18 +405,6 @@ namespace MediaBrowser.Providers.Manager
currentImage.DateModified = newDateModified;
}
}
- else
- {
- var existing = item.GetImageInfo(type, 0);
- if (existing != null)
- {
- if (existing.IsLocalFile && !File.Exists(existing.Path))
- {
- item.RemoveImage(existing);
- changed = true;
- }
- }
- }
}
if (UpdateMultiImages(item, images, ImageType.Backdrop))
@@ -417,15 +412,6 @@ namespace MediaBrowser.Providers.Manager
changed = true;
}
- var hasScreenshots = item as IHasScreenshots;
- if (hasScreenshots != null)
- {
- if (UpdateMultiImages(item, images, ImageType.Screenshot))
- {
- changed = true;
- }
- }
-
return changed;
}
@@ -444,12 +430,12 @@ namespace MediaBrowser.Providers.Manager
return null;
}
- private bool UpdateMultiImages(BaseItem item, List<LocalImageInfo> images, ImageType type)
+ private bool UpdateMultiImages(BaseItem item, IReadOnlyList<LocalImageInfo> images, ImageType type)
{
var changed = false;
var newImageFileInfos = images
- .FindAll(i => i.Type == type)
+ .Where(i => i.Type == type)
.Select(i => i.FileInfo)
.ToList();
@@ -463,7 +449,6 @@ namespace MediaBrowser.Providers.Manager
private async Task<bool> DownloadImage(
BaseItem item,
- LibraryOptions libraryOptions,
IRemoteImageProvider provider,
RefreshResult result,
IEnumerable<RemoteImageInfo> images,
@@ -472,10 +457,10 @@ namespace MediaBrowser.Providers.Manager
CancellationToken cancellationToken)
{
var eligibleImages = images
- .Where(i => i.Type == type && !(i.Width.HasValue && i.Width.Value < minWidth))
+ .Where(i => i.Type == type && (i.Width == null || i.Width >= minWidth))
.ToList();
- if (EnableImageStub(item, libraryOptions) && eligibleImages.Count > 0)
+ if (EnableImageStub(item) && eligibleImages.Count > 0)
{
SaveImageStub(item, type, eligibleImages.Select(i => i.Url));
result.UpdateType |= ItemUpdateType.ImageUpdate;
@@ -489,7 +474,20 @@ namespace MediaBrowser.Providers.Manager
try
{
using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
+
+ // Sometimes providers send back bad urls. Just move to the next image
+ if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.Forbidden)
+ {
+ _logger.LogDebug("{Url} returned {StatusCode}, ignoring", url, response.StatusCode);
+ continue;
+ }
+
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogWarning("{Url} returned {StatusCode}, skipping all remaining requests", url, response.StatusCode);
+ break;
+ }
+
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await _providerManager.SaveImage(
@@ -503,15 +501,8 @@ namespace MediaBrowser.Providers.Manager
result.UpdateType |= ItemUpdateType.ImageUpdate;
return true;
}
- catch (HttpRequestException ex)
+ catch (HttpRequestException)
{
- // Sometimes providers send back bad url's. Just move to the next image
- if (ex.StatusCode.HasValue
- && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
- {
- continue;
- }
-
break;
}
}
@@ -519,7 +510,7 @@ namespace MediaBrowser.Providers.Manager
return false;
}
- private bool EnableImageStub(BaseItem item, LibraryOptions libraryOptions)
+ private bool EnableImageStub(BaseItem item)
{
if (item is LiveTvProgram)
{
@@ -531,7 +522,7 @@ namespace MediaBrowser.Providers.Manager
return true;
}
- if (item is IItemByName && !(item is MusicArtist))
+ if (item is IItemByName and not MusicArtist)
{
var hasDualAccess = item as IHasDualAccess;
if (hasDualAccess == null || hasDualAccess.IsAccessedByName)
@@ -539,6 +530,7 @@ namespace MediaBrowser.Providers.Manager
return true;
}
}
+
// We always want to use prefetched images
return false;
}
@@ -563,7 +555,7 @@ namespace MediaBrowser.Providers.Manager
newIndex);
}
- private async Task DownloadBackdrops(BaseItem item, LibraryOptions libraryOptions, ImageType imageType, int limit, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, int minWidth, CancellationToken cancellationToken)
+ private async Task DownloadMultiImages(BaseItem item, ImageType imageType, ImageRefreshOptions refreshOptions, int limit, IRemoteImageProvider provider, RefreshResult result, IEnumerable<RemoteImageInfo> images, int minWidth, CancellationToken cancellationToken)
{
foreach (var image in images.Where(i => i.Type == imageType))
{
@@ -579,7 +571,7 @@ namespace MediaBrowser.Providers.Manager
var url = image.Url;
- if (EnableImageStub(item, libraryOptions))
+ if (EnableImageStub(item))
{
SaveImageStub(item, imageType, new[] { url });
result.UpdateType |= ItemUpdateType.ImageUpdate;
@@ -590,8 +582,21 @@ namespace MediaBrowser.Providers.Manager
{
using var response = await provider.GetImageResponse(url, cancellationToken).ConfigureAwait(false);
- // If there's already an image of the same size, skip it
- if (response.Content.Headers.ContentLength.HasValue)
+ // Sometimes providers send back bad urls. Just move to the next image
+ if (response.StatusCode == HttpStatusCode.NotFound || response.StatusCode == HttpStatusCode.Forbidden)
+ {
+ _logger.LogDebug("{Url} returned {StatusCode}, ignoring", url, response.StatusCode);
+ continue;
+ }
+
+ if (!response.IsSuccessStatusCode)
+ {
+ _logger.LogWarning("{Url} returned {StatusCode}, skipping all remaining requests", url, response.StatusCode);
+ break;
+ }
+
+ // If there's already an image of the same file size, skip it unless doing a full refresh
+ if (response.Content.Headers.ContentLength.HasValue && !refreshOptions.IsReplacingImage(imageType))
{
try
{
@@ -617,15 +622,8 @@ namespace MediaBrowser.Providers.Manager
cancellationToken).ConfigureAwait(false);
result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
}
- catch (HttpRequestException ex)
+ catch (HttpRequestException)
{
- // Sometimes providers send back bad urls. Just move onto the next image
- if (ex.StatusCode.HasValue
- && (ex.StatusCode.Value == HttpStatusCode.NotFound || ex.StatusCode.Value == HttpStatusCode.Forbidden))
- {
- continue;
- }
-
break;
}
}
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 827cb69b9..0af76f75a 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -1,8 +1,11 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
@@ -505,6 +508,11 @@ namespace MediaBrowser.Providers.Manager
/// <summary>
/// Gets the providers.
/// </summary>
+ /// <param name="item">A media item.</param>
+ /// <param name="libraryOptions">The LibraryOptions to use.</param>
+ /// <param name="options">The MetadataRefreshOptions to use.</param>
+ /// <param name="isFirstRefresh">Specifies first refresh mode.</param>
+ /// <param name="requiresRefresh">Specifies refresh mode.</param>
/// <returns>IEnumerable{`0}.</returns>
protected IEnumerable<IMetadataProvider> GetProviders(BaseItem item, LibraryOptions libraryOptions, MetadataRefreshOptions options, bool isFirstRefresh, bool requiresRefresh)
{
@@ -579,7 +587,7 @@ namespace MediaBrowser.Providers.Manager
protected virtual IEnumerable<IImageProvider> GetNonLocalImageProviders(BaseItem item, IEnumerable<IImageProvider> allImageProviders, ImageRefreshOptions options)
{
// Get providers to refresh
- var providers = allImageProviders.Where(i => !(i is ILocalImageProvider));
+ var providers = allImageProviders.Where(i => i is not ILocalImageProvider);
var dateLastImageRefresh = item.DateLastRefreshed;
@@ -617,7 +625,7 @@ namespace MediaBrowser.Providers.Manager
MetadataResult<TItemType> metadata,
TIdType id,
MetadataRefreshOptions options,
- List<IMetadataProvider> providers,
+ ICollection<IMetadataProvider> providers,
ItemImageProvider imageService,
CancellationToken cancellationToken)
{
@@ -672,8 +680,15 @@ namespace MediaBrowser.Providers.Manager
{
foreach (var remoteImage in localItem.RemoteImages)
{
- await ProviderManager.SaveImage(item, remoteImage.url, remoteImage.type, null, cancellationToken).ConfigureAwait(false);
- refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
+ try
+ {
+ await ProviderManager.SaveImage(item, remoteImage.url, remoteImage.type, null, cancellationToken).ConfigureAwait(false);
+ refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
+ }
+ catch (HttpRequestException ex)
+ {
+ Logger.LogError(ex, "Could not save {ImageType} image: {Url}", Enum.GetName(remoteImage.type), remoteImage.url);
+ }
}
if (imageService.MergeImages(item, localItem.Images))
@@ -706,7 +721,7 @@ namespace MediaBrowser.Providers.Manager
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error in {provider}", provider.Name);
+ Logger.LogError(ex, "Error in {Provider}", provider.Name);
// If a local provider fails, consider that a failure
refreshResult.ErrorMessage = ex.Message;
@@ -724,7 +739,7 @@ namespace MediaBrowser.Providers.Manager
refreshResult.Failures += remoteResult.Failures;
}
- if (providers.Any(i => !(i is ICustomMetadataProvider)))
+ if (providers.Any(i => i is not ICustomMetadataProvider))
{
if (refreshResult.UpdateType > ItemUpdateType.None)
{
@@ -743,7 +758,7 @@ namespace MediaBrowser.Providers.Manager
// var isUnidentified = failedProviderCount > 0 && successfulProviderCount == 0;
- foreach (var provider in customProviders.Where(i => !(i is IPreRefreshProvider)))
+ foreach (var provider in customProviders.Where(i => i is not IPreRefreshProvider))
{
await RunCustomProvider(provider, item, logName, options, refreshResult, cancellationToken).ConfigureAwait(false);
}
@@ -778,7 +793,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}", provider.Name);
}
}
@@ -830,7 +845,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}", provider.Name);
}
}
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index 2dfaa372c..0385ce6a7 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/MediaBrowser.Providers/Manager/ProviderManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -9,7 +11,9 @@ using System.Net.Http;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
@@ -24,6 +28,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
@@ -209,8 +214,7 @@ namespace MediaBrowser.Providers.Manager
throw new ArgumentNullException(nameof(source));
}
- var fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, true);
-
+ var fileStream = AsyncFile.OpenRead(source);
return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken);
}
@@ -235,14 +239,7 @@ namespace MediaBrowser.Providers.Manager
var preferredLanguage = item.GetPreferredMetadataLanguage();
- var languages = new List<string>();
- if (!query.IncludeAllLanguages && !string.IsNullOrWhiteSpace(preferredLanguage))
- {
- languages.Add(preferredLanguage);
- }
-
- // TODO include [query.IncludeAllLanguages] as an argument to the providers
- var tasks = providers.Select(i => GetImages(item, i, languages, cancellationToken, query.ImageType));
+ var tasks = providers.Select(i => GetImages(item, i, preferredLanguage, query.IncludeAllLanguages, cancellationToken, query.ImageType));
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
@@ -254,17 +251,21 @@ namespace MediaBrowser.Providers.Manager
/// </summary>
/// <param name="item">The item.</param>
/// <param name="provider">The provider.</param>
- /// <param name="preferredLanguages">The preferred languages.</param>
+ /// <param name="preferredLanguage">The preferred language.</param>
+ /// <param name="includeAllLanguages">Whether to include all languages in results.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="type">The type.</param>
/// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns>
private async Task<IEnumerable<RemoteImageInfo>> GetImages(
BaseItem item,
IRemoteImageProvider provider,
- IReadOnlyCollection<string> preferredLanguages,
+ string preferredLanguage,
+ bool includeAllLanguages,
CancellationToken cancellationToken,
ImageType? type = null)
{
+ bool hasPreferredLanguage = !string.IsNullOrWhiteSpace(preferredLanguage);
+
try
{
var result = await provider.GetImages(item, cancellationToken).ConfigureAwait(false);
@@ -274,14 +275,17 @@ namespace MediaBrowser.Providers.Manager
result = result.Where(i => i.Type == type.Value);
}
- if (preferredLanguages.Count > 0)
+ if (!includeAllLanguages && hasPreferredLanguage)
{
- result = result.Where(i => string.IsNullOrEmpty(i.Language) ||
- preferredLanguages.Contains(i.Language, StringComparer.OrdinalIgnoreCase) ||
+ // Filter out languages that do not match the preferred languages.
+ //
+ // TODO: should exception case of "en" (English) eventually be removed?
+ result = result.Where(i => string.IsNullOrWhiteSpace(i.Language) ||
+ string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase) ||
string.Equals(i.Language, "en", StringComparison.OrdinalIgnoreCase));
}
- return result;
+ return result.OrderByLanguageDescending(preferredLanguage);
}
catch (OperationCanceledException)
{
@@ -323,7 +327,7 @@ namespace MediaBrowser.Providers.Manager
.OrderBy(i =>
{
// See if there's a user-defined order
- if (!(i is ILocalImageProvider))
+ if (i is not ILocalImageProvider)
{
var fetcherOrder = typeFetcherOrder ?? currentOptions.ImageFetcherOrder;
var index = Array.IndexOf(fetcherOrder, i.Name);
@@ -390,7 +394,7 @@ namespace MediaBrowser.Providers.Manager
if (!includeDisabled)
{
// If locked only allow local providers
- if (item.IsLocked && !(provider is ILocalMetadataProvider) && !(provider is IForcedProvider))
+ if (item.IsLocked && provider is not ILocalMetadataProvider && provider is not IForcedProvider)
{
return false;
}
@@ -431,7 +435,7 @@ namespace MediaBrowser.Providers.Manager
if (!includeDisabled)
{
// If locked only allow local providers
- if (item.IsLocked && !(provider is ILocalImageProvider))
+ if (item.IsLocked && provider is not ILocalImageProvider)
{
if (refreshOptions.ImageRefreshMode != MetadataRefreshMode.FullRefresh)
{
@@ -466,7 +470,7 @@ namespace MediaBrowser.Providers.Manager
/// <returns>System.Int32.</returns>
private int GetOrder(IImageProvider provider)
{
- if (!(provider is IHasOrder hasOrder))
+ if (provider is not IHasOrder hasOrder)
{
return 0;
}
@@ -657,7 +661,7 @@ namespace MediaBrowser.Providers.Manager
/// <inheritdoc/>
public void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers)
{
- SaveMetadata(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparer.OrdinalIgnoreCase)));
+ SaveMetadata(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparison.OrdinalIgnoreCase)));
}
/// <summary>
@@ -734,7 +738,7 @@ namespace MediaBrowser.Providers.Manager
{
if (libraryOptions.MetadataSavers == null)
{
- if (options.DisabledMetadataSavers.Contains(saver.Name, StringComparer.OrdinalIgnoreCase))
+ if (options.DisabledMetadataSavers.Contains(saver.Name, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@@ -745,7 +749,7 @@ namespace MediaBrowser.Providers.Manager
{
// Manual edit occurred
// Even if save local is off, save locally anyway if the metadata file already exists
- if (!(saver is IMetadataFileSaver fileSaver) || !File.Exists(fileSaver.GetSavePath(item)))
+ if (saver is not IMetadataFileSaver fileSaver || !File.Exists(fileSaver.GetSavePath(item)))
{
return false;
}
@@ -760,7 +764,7 @@ namespace MediaBrowser.Providers.Manager
}
else
{
- if (!libraryOptions.MetadataSavers.Contains(saver.Name, StringComparer.OrdinalIgnoreCase))
+ if (!libraryOptions.MetadataSavers.Contains(saver.Name, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@@ -1131,7 +1135,7 @@ namespace MediaBrowser.Providers.Manager
var albums = _libraryManager
.GetItemList(new InternalItemsQuery
{
- IncludeItemTypes = new[] { nameof(MusicAlbum) },
+ IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
ArtistIds = new[] { item.Id },
DtoOptions = new DtoOptions(false)
{
diff --git a/MediaBrowser.Providers/Manager/ProviderUtils.cs b/MediaBrowser.Providers/Manager/ProviderUtils.cs
index e5aa64b28..b90136d50 100644
--- a/MediaBrowser.Providers/Manager/ProviderUtils.cs
+++ b/MediaBrowser.Providers/Manager/ProviderUtils.cs
@@ -1,11 +1,13 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
+using Diacritics.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -135,7 +137,7 @@ namespace MediaBrowser.Providers.Manager
{
if (replaceData || !target.RunTimeTicks.HasValue)
{
- if (!(target is Audio) && !(target is Video))
+ if (target is not Audio && target is not Video)
{
target.RunTimeTicks = source.RunTimeTicks;
}
diff --git a/MediaBrowser.Providers/Manager/RefreshResult.cs b/MediaBrowser.Providers/Manager/RefreshResult.cs
index 72fc61e42..663ffc524 100644
--- a/MediaBrowser.Providers/Manager/RefreshResult.cs
+++ b/MediaBrowser.Providers/Manager/RefreshResult.cs
@@ -8,7 +8,7 @@ namespace MediaBrowser.Providers.Manager
{
public ItemUpdateType UpdateType { get; set; }
- public string ErrorMessage { get; set; }
+ public string? ErrorMessage { get; set; }
public int Failures { get; set; }
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index cdb07a15d..dac5aaf56 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -16,9 +16,9 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="5.0.0" />
- <PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
<PackageReference Include="PlaylistsNET" Version="1.1.3" />
@@ -26,11 +26,9 @@
</ItemGroup>
<PropertyGroup>
- <TargetFramework>net5.0</TargetFramework>
+ <TargetFramework>net6.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
- <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
- <AnalysisMode Condition=" '$(Configuration)' == 'Debug'">AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
@@ -48,5 +46,9 @@
<EmbeddedResource Include="Plugins\Omdb\Configuration\config.html" />
<None Remove="Plugins\MusicBrainz\Configuration\config.html" />
<EmbeddedResource Include="Plugins\MusicBrainz\Configuration\config.html" />
+ <None Remove="Plugins\StudioImages\Configuration\config.html" />
+ <EmbeddedResource Include="Plugins\StudioImages\Configuration\config.html" />
+ <None Remove="Plugins\Tmdb\Configuration\config.html" />
+ <EmbeddedResource Include="Plugins\Tmdb\Configuration\config.html" />
</ItemGroup>
</Project>
diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
index 03e45fb86..b4b1895f5 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#nullable disable
using System;
using System.Collections.Generic;
@@ -11,7 +11,9 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -19,38 +21,49 @@ using MediaBrowser.Model.IO;
namespace MediaBrowser.Providers.MediaInfo
{
/// <summary>
- /// Uses ffmpeg to create video images.
+ /// Uses <see cref="IMediaEncoder"/> to extract embedded images.
/// </summary>
public class AudioImageProvider : IDynamicImageProvider
{
+ private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
- public AudioImageProvider(IMediaEncoder mediaEncoder, IServerConfigurationManager config, IFileSystem fileSystem)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioImageProvider"/> class.
+ /// </summary>
+ /// <param name="mediaSourceManager">The media source manager for fetching item streams.</param>
+ /// <param name="mediaEncoder">The media encoder for extracting embedded images.</param>
+ /// <param name="config">The server configuration manager for getting image paths.</param>
+ /// <param name="fileSystem">The filesystem.</param>
+ public AudioImageProvider(IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IServerConfigurationManager config, IFileSystem fileSystem)
{
+ _mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
_config = config;
_fileSystem = fileSystem;
}
- public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images");
+ private string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images");
+ /// <inheritdoc />
public string Name => "Image Extractor";
+ /// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType> { ImageType.Primary };
+ return new[] { ImageType.Primary };
}
+ /// <inheritdoc />
public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
{
- var audio = (Audio)item;
-
- var imageStreams =
- audio.GetMediaStreams(MediaStreamType.EmbeddedImage)
- .Where(i => i.Type == MediaStreamType.EmbeddedImage)
- .ToList();
+ var imageStreams = _mediaSourceManager.GetMediaStreams(new MediaStreamQuery
+ {
+ ItemId = item.Id,
+ Type = MediaStreamType.EmbeddedImage
+ });
// Can't extract if we didn't find a video stream in the file
if (imageStreams.Count == 0)
@@ -61,7 +74,7 @@ namespace MediaBrowser.Providers.MediaInfo
return GetImage((Audio)item, imageStreams, cancellationToken);
}
- public async Task<DynamicImageResponse> GetImage(Audio item, List<MediaStream> imageStreams, CancellationToken cancellationToken)
+ private async Task<DynamicImageResponse> GetImage(Audio item, List<MediaStream> imageStreams, CancellationToken cancellationToken)
{
var path = GetAudioImagePath(item);
@@ -73,7 +86,7 @@ namespace MediaBrowser.Providers.MediaInfo
imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("cover", StringComparison.OrdinalIgnoreCase) != -1) ??
imageStreams.FirstOrDefault();
- var imageStreamIndex = imageStream == null ? (int?)null : imageStream.Index;
+ var imageStreamIndex = imageStream?.Index;
var tempFile = await _mediaEncoder.ExtractAudioImage(item.Path, imageStreamIndex, cancellationToken).ConfigureAwait(false);
@@ -125,6 +138,7 @@ namespace MediaBrowser.Providers.MediaInfo
return Path.Join(AudioImagesPath, prefix, filename);
}
+ /// <inheritdoc />
public bool Supports(BaseItem item)
{
if (item.IsShortcut)
diff --git a/MediaBrowser.Providers/MediaInfo/AudioResolver.cs b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs
new file mode 100644
index 000000000..425913501
--- /dev/null
+++ b/MediaBrowser.Providers/MediaInfo/AudioResolver.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Naming.Audio;
+using Emby.Naming.Common;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.MediaInfo;
+
+namespace MediaBrowser.Providers.MediaInfo
+{
+ /// <summary>
+ /// Resolves external audios for videos.
+ /// </summary>
+ public class AudioResolver
+ {
+ private readonly ILocalizationManager _localizationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly NamingOptions _namingOptions;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AudioResolver"/> class.
+ /// </summary>
+ /// <param name="localizationManager">The localization manager.</param>
+ /// <param name="mediaEncoder">The media encoder.</param>
+ /// <param name="namingOptions">The naming options.</param>
+ public AudioResolver(
+ ILocalizationManager localizationManager,
+ IMediaEncoder mediaEncoder,
+ NamingOptions namingOptions)
+ {
+ _localizationManager = localizationManager;
+ _mediaEncoder = mediaEncoder;
+ _namingOptions = namingOptions;
+ }
+
+ /// <summary>
+ /// Returns the audio streams found in the external audio files for the given video.
+ /// </summary>
+ /// <param name="video">The video to get the external audio streams from.</param>
+ /// <param name="startIndex">The stream index to start adding audio streams at.</param>
+ /// <param name="directoryService">The directory service to search for files.</param>
+ /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+ /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+ /// <returns>A list of external audio streams.</returns>
+ public async IAsyncEnumerable<MediaStream> GetExternalAudioStreams(
+ Video video,
+ int startIndex,
+ IDirectoryService directoryService,
+ bool clearCache,
+ [EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (!video.IsFileProtocol)
+ {
+ yield break;
+ }
+
+ IEnumerable<string> paths = GetExternalAudioFiles(video, directoryService, clearCache);
+ foreach (string path in paths)
+ {
+ string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
+ Model.MediaInfo.MediaInfo mediaInfo = await GetMediaInfo(path, cancellationToken).ConfigureAwait(false);
+
+ foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
+ {
+ mediaStream.Index = startIndex++;
+ mediaStream.Type = MediaStreamType.Audio;
+ mediaStream.IsExternal = true;
+ mediaStream.Path = path;
+ mediaStream.IsDefault = false;
+ mediaStream.Title = null;
+
+ if (string.IsNullOrEmpty(mediaStream.Language))
+ {
+ // Try to translate to three character code
+ // Be flexible and check against both the full and three character versions
+ var language = StringExtensions.RightPart(fileNameWithoutExtension, '.').ToString();
+
+ if (language != fileNameWithoutExtension)
+ {
+ var culture = _localizationManager.FindLanguageInfo(language);
+
+ language = culture == null ? language : culture.ThreeLetterISOLanguageName;
+ mediaStream.Language = language;
+ }
+ }
+
+ yield return mediaStream;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Returns the external audio file paths for the given video.
+ /// </summary>
+ /// <param name="video">The video to get the external audio file paths from.</param>
+ /// <param name="directoryService">The directory service to search for files.</param>
+ /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+ /// <returns>A list of external audio file paths.</returns>
+ public IEnumerable<string> GetExternalAudioFiles(
+ Video video,
+ IDirectoryService directoryService,
+ bool clearCache)
+ {
+ if (!video.IsFileProtocol)
+ {
+ yield break;
+ }
+
+ // Check if video folder exists
+ string folder = video.ContainingFolderPath;
+ if (!Directory.Exists(folder))
+ {
+ yield break;
+ }
+
+ string videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path);
+
+ var files = directoryService.GetFilePaths(folder, clearCache, true);
+ for (int i = 0; i < files.Count; i++)
+ {
+ string file = files[i];
+ if (string.Equals(video.Path, file, StringComparison.OrdinalIgnoreCase)
+ || !AudioFileParser.IsAudioFile(file, _namingOptions)
+ || Path.GetExtension(file.AsSpan()).Equals(".strm", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file);
+ // The audio filename must either be equal to the video filename or start with the video filename followed by a dot
+ if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)
+ || (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length
+ && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.'
+ && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)))
+ {
+ yield return file;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Returns the media info of the given audio file.
+ /// </summary>
+ /// <param name="path">The path to the audio file.</param>
+ /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+ /// <returns>The media info for the given audio file.</returns>
+ private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ return _mediaEncoder.GetMediaInfo(
+ new MediaInfoRequest
+ {
+ MediaType = DlnaProfileType.Audio,
+ MediaSource = new MediaSourceInfo
+ {
+ Path = path,
+ Protocol = MediaProtocol.File
+ }
+ },
+ cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
new file mode 100644
index 000000000..96d7d139a
--- /dev/null
+++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
@@ -0,0 +1,248 @@
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Drawing;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.MediaInfo;
+using MediaBrowser.Model.Net;
+using Microsoft.Extensions.Logging;
+
+namespace MediaBrowser.Providers.MediaInfo
+{
+ /// <summary>
+ /// Uses <see cref="IMediaEncoder"/> to extract embedded images.
+ /// </summary>
+ public class EmbeddedImageProvider : IDynamicImageProvider, IHasOrder
+ {
+ private static readonly string[] _primaryImageFileNames =
+ {
+ "poster",
+ "folder",
+ "cover",
+ "default"
+ };
+
+ private static readonly string[] _backdropImageFileNames =
+ {
+ "backdrop",
+ "fanart",
+ "background",
+ "art"
+ };
+
+ private static readonly string[] _logoImageFileNames =
+ {
+ "logo",
+ };
+
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly ILogger<EmbeddedImageProvider> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EmbeddedImageProvider"/> class.
+ /// </summary>
+ /// <param name="mediaSourceManager">The media source manager for fetching item streams and attachments.</param>
+ /// <param name="mediaEncoder">The media encoder for extracting attached/embedded images.</param>
+ /// <param name="logger">The logger.</param>
+ public EmbeddedImageProvider(IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, ILogger<EmbeddedImageProvider> logger)
+ {
+ _mediaSourceManager = mediaSourceManager;
+ _mediaEncoder = mediaEncoder;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public string Name => "Embedded Image Extractor";
+
+ /// <inheritdoc />
+ // Default to after internet image providers but before Screen Grabber
+ public int Order => 99;
+
+ /// <inheritdoc />
+ public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
+ {
+ if (item is Video)
+ {
+ if (item is Episode)
+ {
+ return new[]
+ {
+ ImageType.Primary,
+ };
+ }
+
+ return new[]
+ {
+ ImageType.Primary,
+ ImageType.Backdrop,
+ ImageType.Logo,
+ };
+ }
+
+ return Array.Empty<ImageType>();
+ }
+
+ /// <inheritdoc />
+ public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
+ {
+ var video = (Video)item;
+
+ // No support for these
+ if (video.IsPlaceHolder || video.VideoType == VideoType.Dvd)
+ {
+ return Task.FromResult(new DynamicImageResponse { HasImage = false });
+ }
+
+ return GetEmbeddedImage(video, type, cancellationToken);
+ }
+
+ private async Task<DynamicImageResponse> GetEmbeddedImage(Video item, ImageType type, CancellationToken cancellationToken)
+ {
+ MediaSourceInfo mediaSource = new MediaSourceInfo
+ {
+ VideoType = item.VideoType,
+ IsoType = item.IsoType,
+ Protocol = item.PathProtocol ?? MediaProtocol.File,
+ };
+
+ string[] imageFileNames = type switch
+ {
+ ImageType.Primary => _primaryImageFileNames,
+ ImageType.Backdrop => _backdropImageFileNames,
+ ImageType.Logo => _logoImageFileNames,
+ _ => Array.Empty<string>()
+ };
+
+ if (imageFileNames.Length == 0)
+ {
+ _logger.LogWarning("Attempted to load unexpected image type: {Type}", type);
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ // Try attachments first
+ var attachmentStream = _mediaSourceManager.GetMediaAttachments(item.Id)
+ .FirstOrDefault(attachment => !string.IsNullOrEmpty(attachment.FileName)
+ && imageFileNames.Any(name => attachment.FileName.Contains(name, StringComparison.OrdinalIgnoreCase)));
+
+ if (attachmentStream != null)
+ {
+ return await ExtractAttachment(item, attachmentStream, mediaSource, cancellationToken);
+ }
+
+ // Fall back to EmbeddedImage streams
+ var imageStreams = _mediaSourceManager.GetMediaStreams(new MediaStreamQuery
+ {
+ ItemId = item.Id,
+ Type = MediaStreamType.EmbeddedImage
+ });
+
+ if (imageStreams.Count == 0)
+ {
+ // Can't extract if we don't have any EmbeddedImage streams
+ return new DynamicImageResponse { HasImage = false };
+ }
+
+ // Extract first stream containing an element of imageFileNames
+ var imageStream = imageStreams
+ .FirstOrDefault(stream => !string.IsNullOrEmpty(stream.Comment)
+ && imageFileNames.Any(name => stream.Comment.Contains(name, StringComparison.OrdinalIgnoreCase)));
+
+ // Primary type only: default to first image if none found by label
+ if (imageStream == null)
+ {
+ if (type == ImageType.Primary)
+ {
+ imageStream = imageStreams[0];
+ }
+ else
+ {
+ // No streams matched, abort
+ return new DynamicImageResponse { HasImage = false };
+ }
+ }
+
+ var format = imageStream.Codec switch
+ {
+ "mjpeg" => ImageFormat.Jpg,
+ "png" => ImageFormat.Png,
+ "gif" => ImageFormat.Gif,
+ _ => ImageFormat.Jpg
+ };
+
+ string extractedImagePath =
+ await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, imageStream, imageStream.Index, format, cancellationToken)
+ .ConfigureAwait(false);
+
+ return new DynamicImageResponse
+ {
+ Format = format,
+ HasImage = true,
+ Path = extractedImagePath,
+ Protocol = MediaProtocol.File
+ };
+ }
+
+ private async Task<DynamicImageResponse> ExtractAttachment(Video item, MediaAttachment attachmentStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken)
+ {
+ var extension = string.IsNullOrEmpty(attachmentStream.MimeType)
+ ? Path.GetExtension(attachmentStream.FileName)
+ : MimeTypes.ToExtension(attachmentStream.MimeType);
+
+ if (string.IsNullOrEmpty(extension))
+ {
+ extension = ".jpg";
+ }
+
+ ImageFormat format = extension switch
+ {
+ ".bmp" => ImageFormat.Bmp,
+ ".gif" => ImageFormat.Gif,
+ ".jpg" => ImageFormat.Jpg,
+ ".png" => ImageFormat.Png,
+ ".webp" => ImageFormat.Webp,
+ _ => ImageFormat.Jpg
+ };
+
+ string extractedAttachmentPath =
+ await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, null, attachmentStream.Index, format, cancellationToken)
+ .ConfigureAwait(false);
+
+ return new DynamicImageResponse
+ {
+ Format = format,
+ HasImage = true,
+ Path = extractedAttachmentPath,
+ Protocol = MediaProtocol.File
+ };
+ }
+
+ /// <inheritdoc />
+ public bool Supports(BaseItem item)
+ {
+ if (item.IsShortcut)
+ {
+ return false;
+ }
+
+ if (!item.IsFileProtocol)
+ {
+ return false;
+ }
+
+ return item is Video video && !video.IsPlaceHolder && video.IsCompleteMedia;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
index cf271e7db..9eb79c39d 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeAudioInfo.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
index 4fff57273..19a435196 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -5,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Emby.Naming.Common;
using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -36,17 +39,10 @@ namespace MediaBrowser.Providers.MediaInfo
IHasItemChangeMonitor
{
private readonly ILogger<FFProbeProvider> _logger;
- private readonly IMediaEncoder _mediaEncoder;
- private readonly IItemRepository _itemRepo;
- private readonly IBlurayExaminer _blurayExaminer;
- private readonly ILocalizationManager _localization;
- private readonly IEncodingManager _encodingManager;
- private readonly IServerConfigurationManager _config;
- private readonly ISubtitleManager _subtitleManager;
- private readonly IChapterManager _chapterManager;
- private readonly ILibraryManager _libraryManager;
- private readonly IMediaSourceManager _mediaSourceManager;
private readonly SubtitleResolver _subtitleResolver;
+ private readonly AudioResolver _audioResolver;
+ private readonly FFProbeVideoInfo _videoProber;
+ private readonly FFProbeAudioInfo _audioProber;
private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None);
@@ -61,21 +57,26 @@ namespace MediaBrowser.Providers.MediaInfo
IServerConfigurationManager config,
ISubtitleManager subtitleManager,
IChapterManager chapterManager,
- ILibraryManager libraryManager)
+ ILibraryManager libraryManager,
+ NamingOptions namingOptions)
{
_logger = logger;
- _mediaEncoder = mediaEncoder;
- _itemRepo = itemRepo;
- _blurayExaminer = blurayExaminer;
- _localization = localization;
- _encodingManager = encodingManager;
- _config = config;
- _subtitleManager = subtitleManager;
- _chapterManager = chapterManager;
- _libraryManager = libraryManager;
- _mediaSourceManager = mediaSourceManager;
-
+ _audioResolver = new AudioResolver(localization, mediaEncoder, namingOptions);
_subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
+ _videoProber = new FFProbeVideoInfo(
+ _logger,
+ mediaSourceManager,
+ mediaEncoder,
+ itemRepo,
+ blurayExaminer,
+ localization,
+ encodingManager,
+ config,
+ subtitleManager,
+ chapterManager,
+ libraryManager,
+ _audioResolver);
+ _audioProber = new FFProbeAudioInfo(mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
}
public string Name => "ffprobe";
@@ -95,7 +96,7 @@ namespace MediaBrowser.Providers.MediaInfo
var file = directoryService.GetFile(path);
if (file != null && file.LastWriteTimeUtc != item.DateModified)
{
- _logger.LogDebug("Refreshing {0} due to date modified timestamp change.", path);
+ _logger.LogDebug("Refreshing {ItemPath} due to date modified timestamp change.", path);
return true;
}
}
@@ -105,7 +106,15 @@ namespace MediaBrowser.Providers.MediaInfo
&& !video.SubtitleFiles.SequenceEqual(
_subtitleResolver.GetExternalSubtitleFiles(video, directoryService, false), StringComparer.Ordinal))
{
- _logger.LogDebug("Refreshing {0} due to external subtitles change.", item.Path);
+ _logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
+ return true;
+ }
+
+ if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder
+ && !video.AudioFiles.SequenceEqual(
+ _audioResolver.GetExternalAudioFiles(video, directoryService, false), StringComparer.Ordinal))
+ {
+ _logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
return true;
}
@@ -175,20 +184,7 @@ namespace MediaBrowser.Providers.MediaInfo
FetchShortcutInfo(item);
}
- var prober = new FFProbeVideoInfo(
- _logger,
- _mediaSourceManager,
- _mediaEncoder,
- _itemRepo,
- _blurayExaminer,
- _localization,
- _encodingManager,
- _config,
- _subtitleManager,
- _chapterManager,
- _libraryManager);
-
- return prober.ProbeVideo(item, options, cancellationToken);
+ return _videoProber.ProbeVideo(item, options, cancellationToken);
}
private string NormalizeStrmLine(string line)
@@ -224,9 +220,7 @@ namespace MediaBrowser.Providers.MediaInfo
FetchShortcutInfo(item);
}
- var prober = new FFProbeAudioInfo(_mediaSourceManager, _mediaEncoder, _itemRepo, _libraryManager);
-
- return prober.Probe(item, options, cancellationToken);
+ return _audioProber.Probe(item, options, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 12e1fbea5..77372e063 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1068, CS1591
using System;
using System.Collections.Generic;
@@ -42,6 +44,7 @@ namespace MediaBrowser.Providers.MediaInfo
private readonly ISubtitleManager _subtitleManager;
private readonly IChapterManager _chapterManager;
private readonly ILibraryManager _libraryManager;
+ private readonly AudioResolver _audioResolver;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks;
@@ -57,7 +60,8 @@ namespace MediaBrowser.Providers.MediaInfo
IServerConfigurationManager config,
ISubtitleManager subtitleManager,
IChapterManager chapterManager,
- ILibraryManager libraryManager)
+ ILibraryManager libraryManager,
+ AudioResolver audioResolver)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
@@ -69,6 +73,7 @@ namespace MediaBrowser.Providers.MediaInfo
_subtitleManager = subtitleManager;
_chapterManager = chapterManager;
_libraryManager = libraryManager;
+ _audioResolver = audioResolver;
_mediaSourceManager = mediaSourceManager;
}
@@ -212,6 +217,8 @@ namespace MediaBrowser.Providers.MediaInfo
await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
+ await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
+
var libraryOptions = _libraryManager.GetLibraryOptions(video);
if (mediaInfo != null)
@@ -557,6 +564,7 @@ namespace MediaBrowser.Providers.MediaInfo
subtitleDownloadLanguages,
libraryOptions.DisabledSubtitleFetchers,
libraryOptions.SubtitleFetcherOrder,
+ true,
cancellationToken).ConfigureAwait(false);
// Rescan
@@ -572,6 +580,31 @@ namespace MediaBrowser.Providers.MediaInfo
}
/// <summary>
+ /// Adds the external audio.
+ /// </summary>
+ /// <param name="video">The video.</param>
+ /// <param name="currentStreams">The current streams.</param>
+ /// <param name="options">The refreshOptions.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ private async Task AddExternalAudioAsync(
+ Video video,
+ List<MediaStream> currentStreams,
+ MetadataRefreshOptions options,
+ CancellationToken cancellationToken)
+ {
+ var startIndex = currentStreams.Count == 0 ? 0 : currentStreams.Max(i => i.Index) + 1;
+ var externalAudioStreams = _audioResolver.GetExternalAudioStreams(video, startIndex, options.DirectoryService, false, cancellationToken);
+
+ await foreach (MediaStream externalAudioStream in externalAudioStreams)
+ {
+ currentStreams.Add(externalAudioStream);
+ }
+
+ // Select all external audio file paths
+ video.AudioFiles = currentStreams.Where(i => i.Type == MediaStreamType.Audio && i.IsExternal).Select(i => i.Path).Distinct().ToArray();
+ }
+
+ /// <summary>
/// Creates dummy chapters.
/// </summary>
/// <param name="video">The video.</param>
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
index 44ab5aa5b..b2b93940a 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CS1591
using System;
using System.Collections.Generic;
@@ -36,6 +38,7 @@ namespace MediaBrowser.Providers.MediaInfo
IEnumerable<string> languages,
string[] disabledSubtitleFetchers,
string[] subtitleFetcherOrder,
+ bool isAutomated,
CancellationToken cancellationToken)
{
var downloadedLanguages = new List<string>();
@@ -51,6 +54,7 @@ namespace MediaBrowser.Providers.MediaInfo
lang,
disabledSubtitleFetchers,
subtitleFetcherOrder,
+ isAutomated,
cancellationToken).ConfigureAwait(false);
if (downloaded)
@@ -71,6 +75,7 @@ namespace MediaBrowser.Providers.MediaInfo
string lang,
string[] disabledSubtitleFetchers,
string[] subtitleFetcherOrder,
+ bool isAutomated,
CancellationToken cancellationToken)
{
if (video.VideoType != VideoType.VideoFile)
@@ -109,6 +114,7 @@ namespace MediaBrowser.Providers.MediaInfo
disabledSubtitleFetchers,
subtitleFetcherOrder,
mediaType,
+ isAutomated,
cancellationToken);
}
@@ -122,6 +128,7 @@ namespace MediaBrowser.Providers.MediaInfo
string[] disabledSubtitleFetchers,
string[] subtitleFetcherOrder,
VideoContentType mediaType,
+ bool isAutomated,
CancellationToken cancellationToken)
{
// There's already subtitles for this language
@@ -169,7 +176,8 @@ namespace MediaBrowser.Providers.MediaInfo
IsPerfectMatch = requirePerfectMatch,
DisabledSubtitleFetchers = disabledSubtitleFetchers,
- SubtitleFetcherOrder = subtitleFetcherOrder
+ SubtitleFetcherOrder = subtitleFetcherOrder,
+ IsAutomated = isAutomated
};
if (video is Episode episode)
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
index 3cd7ec772..ba284187e 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -10,15 +8,30 @@ using MediaBrowser.Model.Globalization;
namespace MediaBrowser.Providers.MediaInfo
{
+ /// <summary>
+ /// Resolves external subtitles for videos.
+ /// </summary>
public class SubtitleResolver
{
private readonly ILocalizationManager _localization;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SubtitleResolver"/> class.
+ /// </summary>
+ /// <param name="localization">The localization manager.</param>
public SubtitleResolver(ILocalizationManager localization)
{
_localization = localization;
}
+ /// <summary>
+ /// Retrieves the external subtitle streams for the provided video.
+ /// </summary>
+ /// <param name="video">The video to search from.</param>
+ /// <param name="startIndex">The stream index to start adding subtitle streams at.</param>
+ /// <param name="directoryService">The directory service to search for files.</param>
+ /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+ /// <returns>The external subtitle streams located.</returns>
public List<MediaStream> GetExternalSubtitleStreams(
Video video,
int startIndex,
@@ -54,6 +67,13 @@ namespace MediaBrowser.Providers.MediaInfo
return streams;
}
+ /// <summary>
+ /// Locates the external subtitle files for the provided video.
+ /// </summary>
+ /// <param name="video">The video to search from.</param>
+ /// <param name="directoryService">The directory service to search for files.</param>
+ /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+ /// <returns>The external subtitle file paths located.</returns>
public IEnumerable<string> GetExternalSubtitleFiles(
Video video,
IDirectoryService directoryService,
@@ -72,6 +92,13 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
+ /// <summary>
+ /// Extracts the subtitle files from the provided list and adds them to the list of streams.
+ /// </summary>
+ /// <param name="streams">The list of streams to add external subtitles to.</param>
+ /// <param name="videoPath">The path to the video file.</param>
+ /// <param name="startIndex">The stream index to start adding subtitle streams at.</param>
+ /// <param name="files">The files to add if they are subtitles.</param>
public void AddExternalSubtitleStreams(
List<MediaStream> streams,
string videoPath,
@@ -118,6 +145,12 @@ namespace MediaBrowser.Providers.MediaInfo
while (languageSpan.Length > 0)
{
var lastDot = languageSpan.LastIndexOf('.');
+ if (lastDot < videoFileNameWithoutExtension.Length)
+ {
+ languageSpan = ReadOnlySpan<char>.Empty;
+ break;
+ }
+
var currentSlice = languageSpan[lastDot..];
if (currentSlice.Equals(".default", StringComparison.OrdinalIgnoreCase)
|| currentSlice.Equals(".forced", StringComparison.OrdinalIgnoreCase)
@@ -131,12 +164,19 @@ namespace MediaBrowser.Providers.MediaInfo
break;
}
- // Try to translate to three character code
- // Be flexible and check against both the full and three character versions
var language = languageSpan.ToString();
- var culture = _localization.FindLanguageInfo(language);
+ if (string.IsNullOrWhiteSpace(language))
+ {
+ language = null;
+ }
+ else
+ {
+ // Try to translate to three character code
+ // Be flexible and check against both the full and three character versions
+ var culture = _localization.FindLanguageInfo(language);
- language = culture == null ? language : culture.ThreeLetterISOLanguageName;
+ language = culture == null ? language : culture.ThreeLetterISOLanguageName;
+ }
mediaStream = new MediaStream
{
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
index 12b23098c..58651d42a 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -5,6 +7,7 @@ using System.Collections.Generic;
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;
@@ -64,7 +67,7 @@ namespace MediaBrowser.Providers.MediaInfo
{
var options = GetOptions();
- var types = new[] { "Episode", "Movie" };
+ var types = new[] { BaseItemKind.Episode, BaseItemKind.Movie };
var dict = new Dictionary<Guid, BaseItem>();
@@ -194,6 +197,7 @@ namespace MediaBrowser.Providers.MediaInfo
subtitleDownloadLanguages,
libraryOptions.DisabledSubtitleFetchers,
libraryOptions.SubtitleFetcherOrder,
+ true,
cancellationToken).ConfigureAwait(false);
// Rescan
diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
index 30af6710a..d4bf62970 100644
--- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs
@@ -1,57 +1,63 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.MediaInfo
{
+ /// <summary>
+ /// Uses <see cref="IMediaEncoder"/> to create still images from the main video.
+ /// </summary>
public class VideoImageProvider : IDynamicImageProvider, IHasOrder
{
+ private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger<VideoImageProvider> _logger;
- private readonly IFileSystem _fileSystem;
- public VideoImageProvider(IMediaEncoder mediaEncoder, ILogger<VideoImageProvider> logger, IFileSystem fileSystem)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="VideoImageProvider"/> class.
+ /// </summary>
+ /// <param name="mediaSourceManager">The media source manager for fetching item streams.</param>
+ /// <param name="mediaEncoder">The media encoder for capturing images.</param>
+ /// <param name="logger">The logger.</param>
+ public VideoImageProvider(IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, ILogger<VideoImageProvider> logger)
{
+ _mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
_logger = logger;
- _fileSystem = fileSystem;
}
+ /// <inheritdoc />
public string Name => "Screen Grabber";
+ /// <inheritdoc />
// Make sure this comes after internet image providers
public int Order => 100;
+ /// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
- return new List<ImageType> { ImageType.Primary };
+ return new[] { ImageType.Primary };
}
+ /// <inheritdoc />
public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
{
var video = (Video)item;
- // No support for this
- if (video.IsPlaceHolder)
- {
- return Task.FromResult(new DynamicImageResponse { HasImage = false });
- }
-
- // No support for this
- if (video.VideoType == VideoType.Dvd)
+ // No support for these
+ if (video.IsPlaceHolder || video.VideoType == VideoType.Dvd)
{
return Task.FromResult(new DynamicImageResponse { HasImage = false });
}
@@ -59,80 +65,45 @@ namespace MediaBrowser.Providers.MediaInfo
// Can't extract if we didn't find a video stream in the file
if (!video.DefaultVideoStreamIndex.HasValue)
{
- _logger.LogInformation("Skipping image extraction due to missing DefaultVideoStreamIndex for {0}.", video.Path ?? string.Empty);
+ _logger.LogInformation("Skipping image extraction due to missing DefaultVideoStreamIndex for {Path}.", video.Path ?? string.Empty);
return Task.FromResult(new DynamicImageResponse { HasImage = false });
}
return GetVideoImage(video, cancellationToken);
}
- public async Task<DynamicImageResponse> GetVideoImage(Video item, CancellationToken cancellationToken)
+ private async Task<DynamicImageResponse> GetVideoImage(Video item, CancellationToken cancellationToken)
{
- var protocol = item.PathProtocol ?? MediaProtocol.File;
-
- var inputPath = item.Path;
-
- var mediaStreams =
- item.GetMediaStreams();
-
- var imageStreams =
- mediaStreams
- .Where(i => i.Type == MediaStreamType.EmbeddedImage)
- .ToList();
-
- var imageStream = imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("front", StringComparison.OrdinalIgnoreCase) != -1) ??
- imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).IndexOf("cover", StringComparison.OrdinalIgnoreCase) != -1) ??
- imageStreams.FirstOrDefault();
+ MediaSourceInfo mediaSource = new MediaSourceInfo
+ {
+ VideoType = item.VideoType,
+ IsoType = item.IsoType,
+ Protocol = item.PathProtocol ?? MediaProtocol.File,
+ };
- string extractedImagePath;
+ // If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
+ // Always use 10 seconds for dvd because our duration could be out of whack
+ var imageOffset = item.VideoType != VideoType.Dvd && item.RunTimeTicks > 0
+ ? TimeSpan.FromTicks(item.RunTimeTicks.Value / 10)
+ : TimeSpan.FromSeconds(10);
- if (imageStream != null)
+ var query = new MediaStreamQuery { ItemId = item.Id, Index = item.DefaultVideoStreamIndex };
+ var videoStream = _mediaSourceManager.GetMediaStreams(query).FirstOrDefault();
+ if (videoStream == null)
{
- // Instead of using the raw stream index, we need to use nth video/embedded image stream
- var videoIndex = -1;
- foreach (var mediaStream in mediaStreams)
- {
- if (mediaStream.Type == MediaStreamType.Video ||
- mediaStream.Type == MediaStreamType.EmbeddedImage)
- {
- videoIndex++;
- }
-
- if (mediaStream == imageStream)
- {
- break;
- }
- }
-
- MediaSourceInfo mediaSource = new MediaSourceInfo
- {
- VideoType = item.VideoType,
- IsoType = item.IsoType,
- Protocol = item.PathProtocol.Value,
- };
-
- extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, imageStream, videoIndex, cancellationToken).ConfigureAwait(false);
+ query.Type = MediaStreamType.Video;
+ query.Index = null;
+ videoStream = _mediaSourceManager.GetMediaStreams(query).FirstOrDefault();
}
- else
+
+ if (videoStream == null)
{
- // If we know the duration, grab it from 10% into the video. Otherwise just 10 seconds in.
- // Always use 10 seconds for dvd because our duration could be out of whack
- var imageOffset = item.VideoType != VideoType.Dvd && item.RunTimeTicks.HasValue &&
- item.RunTimeTicks.Value > 0
- ? TimeSpan.FromTicks(Convert.ToInt64(item.RunTimeTicks.Value * .1))
- : TimeSpan.FromSeconds(10);
-
- var videoStream = mediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
- var mediaSource = new MediaSourceInfo
- {
- VideoType = item.VideoType,
- IsoType = item.IsoType,
- Protocol = item.PathProtocol.Value,
- };
-
- extractedImagePath = await _mediaEncoder.ExtractVideoImage(inputPath, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false);
+ _logger.LogInformation("Skipping image extraction: no video stream found for {Path}.", item.Path ?? string.Empty);
+ return new DynamicImageResponse { HasImage = false };
}
+ string extractedImagePath = await _mediaEncoder.ExtractVideoImage(item.Path, item.Container, mediaSource, videoStream, item.Video3DFormat, imageOffset, cancellationToken).ConfigureAwait(false);
+
return new DynamicImageResponse
{
Format = ImageFormat.Jpg,
@@ -142,6 +113,7 @@ namespace MediaBrowser.Providers.MediaInfo
};
}
+ /// <inheritdoc />
public bool Supports(BaseItem item)
{
if (item.IsShortcut)
@@ -154,12 +126,7 @@ namespace MediaBrowser.Providers.MediaInfo
return false;
}
- if (item is Video video && !video.IsPlaceHolder && video.IsCompleteMedia)
- {
- return true;
- }
-
- return false;
+ return item is Video video && !video.IsPlaceHolder && video.IsCompleteMedia;
}
}
}
diff --git a/MediaBrowser.Providers/Movies/ImdbExternalId.cs b/MediaBrowser.Providers/Movies/ImdbExternalId.cs
index a8d74aa0b..d00f37db5 100644
--- a/MediaBrowser.Providers/Movies/ImdbExternalId.cs
+++ b/MediaBrowser.Providers/Movies/ImdbExternalId.cs
@@ -22,7 +22,7 @@ namespace MediaBrowser.Providers.Movies
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string UrlFormatString => "https://www.imdb.com/title/{0}";
+ public string? UrlFormatString => "https://www.imdb.com/title/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
index 8151ab471..1bb5e1ea8 100644
--- a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
+++ b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Movies
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
/// <inheritdoc />
- public string UrlFormatString => "https://www.imdb.com/name/{0}";
+ public string? UrlFormatString => "https://www.imdb.com/name/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Person;
diff --git a/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs b/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs
index dddfd02e4..d3fce37c7 100644
--- a/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs
+++ b/MediaBrowser.Providers/Music/AlbumInfoExtensions.cs
@@ -8,7 +8,7 @@ namespace MediaBrowser.Providers.Music
{
public static class AlbumInfoExtensions
{
- public static string GetAlbumArtist(this AlbumInfo info)
+ public static string? GetAlbumArtist(this AlbumInfo info)
{
var id = info.SongInfos.SelectMany(i => i.AlbumArtists)
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
@@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.Music
return info.AlbumArtists.Count > 0 ? info.AlbumArtists[0] : default;
}
- public static string GetReleaseGroupId(this AlbumInfo info)
+ public static string? GetReleaseGroupId(this AlbumInfo info)
{
var id = info.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup);
@@ -34,7 +34,7 @@ namespace MediaBrowser.Providers.Music
return id;
}
- public static string GetReleaseId(this AlbumInfo info)
+ public static string? GetReleaseId(this AlbumInfo info)
{
var id = info.GetProviderId(MetadataProvider.MusicBrainzAlbum);
@@ -47,9 +47,9 @@ namespace MediaBrowser.Providers.Music
return id;
}
- public static string GetMusicBrainzArtistId(this AlbumInfo info)
+ public static string? GetMusicBrainzArtistId(this AlbumInfo info)
{
- info.ProviderIds.TryGetValue(MetadataProvider.MusicBrainzAlbumArtist.ToString(), out string id);
+ info.ProviderIds.TryGetValue(MetadataProvider.MusicBrainzAlbumArtist.ToString(), out string? id);
if (string.IsNullOrEmpty(id))
{
@@ -65,7 +65,7 @@ namespace MediaBrowser.Providers.Music
return id;
}
- public static string GetMusicBrainzArtistId(this ArtistInfo info)
+ public static string? GetMusicBrainzArtistId(this ArtistInfo info)
{
info.ProviderIds.TryGetValue(MetadataProvider.MusicBrainzArtist.ToString(), out var id);
diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
index 8c9a1f59b..7c5b80e1e 100644
--- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
@@ -81,7 +81,7 @@ namespace MediaBrowser.Providers.Music
if (!item.AlbumArtists.SequenceEqual(artists, StringComparer.OrdinalIgnoreCase))
{
item.AlbumArtists = artists;
- updateType = updateType | ItemUpdateType.MetadataEdit;
+ updateType |= ItemUpdateType.MetadataEdit;
}
return updateType;
@@ -100,7 +100,7 @@ namespace MediaBrowser.Providers.Music
if (!item.Artists.SequenceEqual(artists, StringComparer.OrdinalIgnoreCase))
{
item.Artists = artists;
- updateType = updateType | ItemUpdateType.MetadataEdit;
+ updateType |= ItemUpdateType.MetadataEdit;
}
return updateType;
diff --git a/MediaBrowser.Providers/Music/ImvdbId.cs b/MediaBrowser.Providers/Music/ImvdbId.cs
index a1726b996..ed69f369c 100644
--- a/MediaBrowser.Providers/Music/ImvdbId.cs
+++ b/MediaBrowser.Providers/Music/ImvdbId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Music
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string UrlFormatString => null;
+ public string? UrlFormatString => null;
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
index 067d585cb..fe9986d42 100644
--- a/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
+++ b/MediaBrowser.Providers/Playlists/PlaylistItemsProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -6,6 +8,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
@@ -42,7 +45,7 @@ namespace MediaBrowser.Providers.Playlists
}
var extension = Path.GetExtension(path);
- if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (!Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
return Task.FromResult(ItemUpdateType.None);
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
index 138cfef19..3a400575b 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
+ public string? UrlFormatString => "https://www.theaudiodb.com/album/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
index 85a28747f..ad0247fb2 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -6,13 +8,14 @@ using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.AudioDb
@@ -57,7 +60,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = AudioDbAlbumProvider.GetAlbumInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = File.OpenRead(path);
+ await using FileStream jsonStream = AsyncFile.OpenRead(path);
var obj = await JsonSerializer.DeserializeAsync<AudioDbAlbumProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.album != null && obj.album.Count > 0)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
index 25bb3f9ce..43f30824b 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1002, CS1591, SA1300
using System;
using System.Collections.Generic;
@@ -9,9 +11,9 @@ using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
@@ -30,7 +32,9 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
private readonly IHttpClientFactory _httpClientFactory;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+#pragma warning disable SA1401, CA2211
public static AudioDbAlbumProvider Current;
+#pragma warning restore SA1401, CA2211
public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClientFactory httpClientFactory)
{
@@ -64,7 +68,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetAlbumInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = File.OpenRead(path);
+ await using FileStream jsonStream = AsyncFile.OpenRead(path);
var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.album != null && obj.album.Count > 0)
@@ -170,8 +174,11 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
+
+ var fileStreamOptions = AsyncFile.WriteOptions;
+ fileStreamOptions.Mode = FileMode.Create;
+ fileStreamOptions.PreallocationSize = stream.Length;
+ await using var xmlFileStream = new FileStream(path, fileStreamOptions);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
@@ -196,6 +203,13 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
return Path.Combine(dataPath, "album.json");
}
+ /// <inheritdoc />
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+#pragma warning disable CA1034, CA2227
public class Album
{
public string idAlbum { get; set; }
@@ -279,11 +293,5 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
{
public List<Album> album { get; set; }
}
-
- /// <inheritdoc />
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- throw new NotImplementedException();
- }
}
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
index 8aceb48c0..b9e57eb26 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
/// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
+ public string? UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is MusicArtist;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
index db8536cc9..9c2447660 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -6,13 +8,14 @@ using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Json;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.AudioDb
@@ -59,7 +62,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = AudioDbArtistProvider.GetArtistInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = File.OpenRead(path);
+ await using FileStream jsonStream = AsyncFile.OpenRead(path);
var obj = await JsonSerializer.DeserializeAsync<AudioDbArtistProvider.RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.artists != null && obj.artists.Count > 0)
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
index cbb61fa35..538dc67c4 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CA1034, CS1591, CA1002, SA1028, SA1300
using System;
using System.Collections.Generic;
@@ -8,9 +10,9 @@ using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
@@ -65,7 +67,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
var path = GetArtistInfoPath(_config.ApplicationPaths, id);
- await using FileStream jsonStream = File.OpenRead(path);
+ await using FileStream jsonStream = AsyncFile.OpenRead(path);
var obj = await JsonSerializer.DeserializeAsync<RootObject>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (obj != null && obj.artists != null && obj.artists.Count > 0)
@@ -154,8 +156,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
Directory.CreateDirectory(Path.GetDirectoryName(path));
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- await using var xmlFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
+ var fileStreamOptions = AsyncFile.WriteOptions;
+ fileStreamOptions.Mode = FileMode.Create;
+ fileStreamOptions.PreallocationSize = stream.Length;
+ await using var xmlFileStream = new FileStream(path, fileStreamOptions);
await stream.CopyToAsync(xmlFileStream, cancellationToken).ConfigureAwait(false);
}
@@ -183,6 +187,12 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
return Path.Combine(dataPath, "artist.json");
}
+ /// <inheritdoc />
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
public class Artist
{
public string idArtist { get; set; }
@@ -268,15 +278,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public string strLocked { get; set; }
}
+#pragma warning disable CA2227
public class RootObject
{
public List<Artist> artists { get; set; }
}
-
- /// <inheritdoc />
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- throw new NotImplementedException();
- }
}
}
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
index 014481da2..f8f6253ff 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
/// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/album/{0}";
+ public string? UrlFormatString => "https://www.theaudiodb.com/album/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
index 787539104..fd598c918 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb
public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
/// <inheritdoc />
- public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
+ public string? UrlFormatString => "https://www.theaudiodb.com/artist/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs
index 664474dcd..d61ec6cb1 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/PluginConfiguration.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using MediaBrowser.Model.Plugins;
diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
index ba0d7b569..6c2ad0573 100644
--- a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
index 0cec9e359..9c27bd7d3 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using MediaBrowser.Model.Plugins;
@@ -12,24 +12,13 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz
public string Server
{
- get
- {
- return _server;
- }
-
- set
- {
- _server = value.TrimEnd('/');
- }
+ get => _server;
+ set => _server = value.TrimEnd('/');
}
public long RateLimit
{
- get
- {
- return _rateLimit;
- }
-
+ get => _rateLimit;
set
{
if (value < Plugin.DefaultRateLimit && _server == Plugin.DefaultServer)
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs
deleted file mode 100644
index 5600c389c..000000000
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/ExternalIds.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-#pragma warning disable CS1591
-
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.MusicBrainz;
-
-namespace MediaBrowser.Providers.Music
-{
- public class MusicBrainzReleaseGroupExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
-
- /// <inheritdoc />
- public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
- }
-
- public class MusicBrainzAlbumArtistExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
-
- /// <inheritdoc />
- public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio;
- }
-
- public class MusicBrainzAlbumExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzAlbum.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
-
- /// <inheritdoc />
- public string UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
- }
-
- public class MusicBrainzArtistExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzArtist.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
-
- /// <inheritdoc />
- public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is MusicArtist;
- }
-
- public class MusicBrainzOtherArtistExternalId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
-
- /// <inheritdoc />
-
- public string Key => MetadataProvider.MusicBrainzArtist.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
-
- /// <inheritdoc />
- public string UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
- }
-
- public class MusicBrainzTrackId : IExternalId
- {
- /// <inheritdoc />
- public string ProviderName => "MusicBrainz";
-
- /// <inheritdoc />
- public string Key => MetadataProvider.MusicBrainzTrack.ToString();
-
- /// <inheritdoc />
- public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
-
- /// <inheritdoc />
- public string UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}";
-
- /// <inheritdoc />
- public bool Supports(IHasProviderIds item) => item is Audio;
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
new file mode 100644
index 000000000..c54cdda3d
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+
+namespace MediaBrowser.Providers.Music
+{
+ public class MusicBrainzAlbumArtistExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzAlbumArtist.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist;
+
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
new file mode 100644
index 000000000..8f7fadd06
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+
+namespace MediaBrowser.Providers.Music
+{
+ public class MusicBrainzAlbumExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzAlbum.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Album;
+
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/release/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index 0023d5959..8a32cb07c 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CS1591, SA1401
using System;
using System.Collections.Generic;
@@ -12,7 +14,6 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
-using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Providers;
@@ -23,7 +24,7 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Music
{
- public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder
+ public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder, IDisposable
{
/// <summary>
/// For each single MB lookup/search, this is the maximum number of
@@ -36,12 +37,11 @@ namespace MediaBrowser.Providers.Music
/// The Jellyfin user-agent is unrestricted but source IP must not exceed
/// one request per second, therefore we rate limit to avoid throttling.
/// Be prudent, use a value slightly above the minimun required.
- /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting
+ /// https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting.
/// </summary>
private readonly long _musicBrainzQueryIntervalMs;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IApplicationHost _appHost;
private readonly ILogger<MusicBrainzAlbumProvider> _logger;
private readonly string _musicBrainzBaseUrl;
@@ -51,11 +51,9 @@ namespace MediaBrowser.Providers.Music
public MusicBrainzAlbumProvider(
IHttpClientFactory httpClientFactory,
- IApplicationHost appHost,
ILogger<MusicBrainzAlbumProvider> logger)
{
_httpClientFactory = httpClientFactory;
- _appHost = appHost;
_logger = logger;
_musicBrainzBaseUrl = Plugin.Instance.Configuration.Server;
@@ -174,10 +172,10 @@ namespace MediaBrowser.Providers.Music
}
/// <inheritdoc />
- public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo id, CancellationToken cancellationToken)
+ public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken)
{
- var releaseId = id.GetReleaseId();
- var releaseGroupId = id.GetReleaseGroupId();
+ var releaseId = info.GetReleaseId();
+ var releaseGroupId = info.GetReleaseGroupId();
var result = new MetadataResult<MusicAlbum>
{
@@ -193,9 +191,9 @@ namespace MediaBrowser.Providers.Music
if (string.IsNullOrWhiteSpace(releaseId))
{
- var artistMusicBrainzId = id.GetMusicBrainzArtistId();
+ var artistMusicBrainzId = info.GetMusicBrainzArtistId();
- var releaseResult = await GetReleaseResult(artistMusicBrainzId, id.GetAlbumArtist(), id.Name, cancellationToken).ConfigureAwait(false);
+ var releaseResult = await GetReleaseResult(artistMusicBrainzId, info.GetAlbumArtist(), info.Name, cancellationToken).ConfigureAwait(false);
if (releaseResult != null)
{
@@ -305,181 +303,6 @@ namespace MediaBrowser.Providers.Music
return ReleaseResult.Parse(reader).FirstOrDefault();
}
- private class ReleaseResult
- {
- public string ReleaseId;
- public string ReleaseGroupId;
- public string Title;
- public string Overview;
- public int? Year;
-
- public List<ValueTuple<string, string>> Artists = new List<ValueTuple<string, string>>();
-
- public static IEnumerable<ReleaseResult> Parse(XmlReader reader)
- {
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "release-list":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- using var subReader = reader.ReadSubtree();
- return ParseReleaseList(subReader).ToList();
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
- }
-
- return Enumerable.Empty<ReleaseResult>();
- }
-
- private static IEnumerable<ReleaseResult> ParseReleaseList(XmlReader reader)
- {
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "release":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- var releaseId = reader.GetAttribute("id");
-
- using var subReader = reader.ReadSubtree();
- var release = ParseRelease(subReader, releaseId);
- if (release != null)
- {
- yield return release;
- }
-
- break;
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
- }
- }
-
- private static ReleaseResult ParseRelease(XmlReader reader, string releaseId)
- {
- var result = new ReleaseResult
- {
- ReleaseId = releaseId
- };
-
- reader.MoveToContent();
- reader.Read();
-
- // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "title":
- {
- result.Title = reader.ReadElementContentAsString();
- break;
- }
-
- case "date":
- {
- var val = reader.ReadElementContentAsString();
- if (DateTime.TryParse(val, out var date))
- {
- result.Year = date.Year;
- }
-
- break;
- }
-
- case "annotation":
- {
- result.Overview = reader.ReadElementContentAsString();
- break;
- }
-
- case "release-group":
- {
- result.ReleaseGroupId = reader.GetAttribute("id");
- reader.Skip();
- break;
- }
-
- case "artist-credit":
- {
- using var subReader = reader.ReadSubtree();
- var artist = ParseArtistCredit(subReader);
-
- if (!string.IsNullOrEmpty(artist.Item1))
- {
- result.Artists.Add(artist);
- }
-
- break;
- }
-
- default:
- {
- reader.Skip();
- break;
- }
- }
- }
- else
- {
- reader.Read();
- }
- }
-
- return result;
- }
- }
-
private static (string, string) ParseArtistCredit(XmlReader reader)
{
reader.MoveToContent();
@@ -496,15 +319,21 @@ namespace MediaBrowser.Providers.Music
{
case "name-credit":
{
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ break;
+ }
+
using var subReader = reader.ReadSubtree();
return ParseArtistNameCredit(subReader);
}
default:
- {
- reader.Skip();
- break;
- }
+ {
+ reader.Skip();
+ break;
+ }
}
}
else
@@ -532,6 +361,12 @@ namespace MediaBrowser.Providers.Music
{
case "artist":
{
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ break;
+ }
+
var id = reader.GetAttribute("id");
using var subReader = reader.ReadSubtree();
return ParseArtistArtistCredit(subReader, id);
@@ -634,8 +469,8 @@ namespace MediaBrowser.Providers.Music
};
using var reader = XmlReader.Create(oReader, settings);
- reader.MoveToContent();
- reader.Read();
+ await reader.MoveToContentAsync().ConfigureAwait(false);
+ await reader.ReadAsync().ConfigureAwait(false);
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
@@ -648,7 +483,7 @@ namespace MediaBrowser.Providers.Music
{
if (reader.IsEmptyElement)
{
- reader.Read();
+ await reader.ReadAsync().ConfigureAwait(false);
continue;
}
@@ -658,14 +493,14 @@ namespace MediaBrowser.Providers.Music
default:
{
- reader.Skip();
+ await reader.SkipAsync().ConfigureAwait(false);
break;
}
}
}
else
{
- reader.Read();
+ await reader.ReadAsync().ConfigureAwait(false);
}
}
@@ -711,6 +546,9 @@ namespace MediaBrowser.Providers.Music
/// A number of retries shall be made in order to try and satisfy the request before
/// giving up and returning null.
/// </summary>
+ /// <param name="url">Address of MusicBrainz server.</param>
+ /// <param name="cancellationToken">CancellationToken to use for method.</param>
+ /// <returns>Returns response from MusicBrainz service.</returns>
internal async Task<HttpResponseMessage> GetMusicBrainzResponse(string url, CancellationToken cancellationToken)
{
await _apiRequestLock.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -766,5 +604,201 @@ namespace MediaBrowser.Providers.Music
{
throw new NotImplementedException();
}
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _apiRequestLock?.Dispose();
+ }
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private class ReleaseResult
+ {
+ public string ReleaseId;
+ public string ReleaseGroupId;
+ public string Title;
+ public string Overview;
+ public int? Year;
+
+ public List<ValueTuple<string, string>> Artists = new List<ValueTuple<string, string>>();
+
+ public static IEnumerable<ReleaseResult> Parse(XmlReader reader)
+ {
+ reader.MoveToContent();
+ reader.Read();
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "release-list":
+ {
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ continue;
+ }
+
+ using var subReader = reader.ReadSubtree();
+ return ParseReleaseList(subReader).ToList();
+ }
+
+ default:
+ {
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ else
+ {
+ reader.Read();
+ }
+ }
+
+ return Enumerable.Empty<ReleaseResult>();
+ }
+
+ private static IEnumerable<ReleaseResult> ParseReleaseList(XmlReader reader)
+ {
+ reader.MoveToContent();
+ reader.Read();
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "release":
+ {
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ continue;
+ }
+
+ var releaseId = reader.GetAttribute("id");
+
+ using var subReader = reader.ReadSubtree();
+ var release = ParseRelease(subReader, releaseId);
+ if (release != null)
+ {
+ yield return release;
+ }
+
+ break;
+ }
+
+ default:
+ {
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ else
+ {
+ reader.Read();
+ }
+ }
+ }
+
+ private static ReleaseResult ParseRelease(XmlReader reader, string releaseId)
+ {
+ var result = new ReleaseResult
+ {
+ ReleaseId = releaseId
+ };
+
+ reader.MoveToContent();
+ reader.Read();
+
+ // http://stackoverflow.com/questions/2299632/why-does-xmlreader-skip-every-other-element-if-there-is-no-whitespace-separator
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ switch (reader.Name)
+ {
+ case "title":
+ {
+ result.Title = reader.ReadElementContentAsString();
+ break;
+ }
+
+ case "date":
+ {
+ var val = reader.ReadElementContentAsString();
+ if (DateTime.TryParse(val, out var date))
+ {
+ result.Year = date.Year;
+ }
+
+ break;
+ }
+
+ case "annotation":
+ {
+ result.Overview = reader.ReadElementContentAsString();
+ break;
+ }
+
+ case "release-group":
+ {
+ result.ReleaseGroupId = reader.GetAttribute("id");
+ reader.Skip();
+ break;
+ }
+
+ case "artist-credit":
+ {
+ if (reader.IsEmptyElement)
+ {
+ reader.Read();
+ break;
+ }
+
+ using var subReader = reader.ReadSubtree();
+ var artist = ParseArtistCredit(subReader);
+
+ if (!string.IsNullOrEmpty(artist.Item1))
+ {
+ result.Artists.Add(artist);
+ }
+
+ break;
+ }
+
+ default:
+ {
+ reader.Skip();
+ break;
+ }
+ }
+ }
+ else
+ {
+ reader.Read();
+ }
+ }
+
+ return result;
+ }
+ }
}
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
new file mode 100644
index 000000000..941ffea72
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+
+namespace MediaBrowser.Providers.Music
+{
+ public class MusicBrainzArtistExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzArtist.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Artist;
+
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is MusicArtist;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
index 2eab95294..1feb7f4ea 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/ArtistProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -11,8 +13,8 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
+using Diacritics.Extensions;
using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
@@ -22,6 +24,8 @@ namespace MediaBrowser.Providers.Music
{
public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo>
{
+ public string Name => "MusicBrainz";
+
/// <inheritdoc />
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken)
{
@@ -53,7 +57,7 @@ namespace MediaBrowser.Providers.Music
}
}
- if (HasDiacritics(searchInfo.Name))
+ if (searchInfo.Name.HasDiacritics())
{
// Try again using the search with accent characters url
url = string.Format(CultureInfo.InvariantCulture, "/ws/2/artist/?query=artistaccent:\"{0}\"", UrlEncode(nameToSearch));
@@ -215,18 +219,19 @@ namespace MediaBrowser.Providers.Music
return result;
}
- public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo id, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo info, CancellationToken cancellationToken)
{
var result = new MetadataResult<MusicArtist>
{
Item = new MusicArtist()
};
- var musicBrainzId = id.GetMusicBrainzArtistId();
+ var musicBrainzId = info.GetMusicBrainzArtistId();
if (string.IsNullOrWhiteSpace(musicBrainzId))
{
- var searchResults = await GetSearchResults(id, cancellationToken).ConfigureAwait(false);
+ var searchResults = await GetSearchResults(info, cancellationToken).ConfigureAwait(false);
var singleResult = searchResults.FirstOrDefault();
@@ -252,16 +257,6 @@ namespace MediaBrowser.Providers.Music
}
/// <summary>
- /// Determines whether the specified text has diacritics.
- /// </summary>
- /// <param name="text">The text.</param>
- /// <returns><c>true</c> if the specified text has diacritics; otherwise, <c>false</c>.</returns>
- private bool HasDiacritics(string text)
- {
- return !string.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
- }
-
- /// <summary>
/// Encodes an URL.
/// </summary>
/// <param name="name">The name.</param>
@@ -271,8 +266,6 @@ namespace MediaBrowser.Providers.Music
return WebUtility.UrlEncode(name);
}
- public string Name => "MusicBrainz";
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
throw new NotImplementedException();
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
new file mode 100644
index 000000000..05db2d98f
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+
+namespace MediaBrowser.Providers.Music
+{
+ public class MusicBrainzOtherArtistExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzArtist.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist;
+
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/artist/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
new file mode 100644
index 000000000..acb652fe0
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+
+namespace MediaBrowser.Providers.Music
+{
+ public class MusicBrainzReleaseGroupExternalId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzReleaseGroup.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup;
+
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/release-group/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
new file mode 100644
index 000000000..14805b9b7
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.MusicBrainz;
+
+namespace MediaBrowser.Providers.Music
+{
+ public class MusicBrainzTrackId : IExternalId
+ {
+ /// <inheritdoc />
+ public string ProviderName => "MusicBrainz";
+
+ /// <inheritdoc />
+ public string Key => MetadataProvider.MusicBrainzTrack.ToString();
+
+ /// <inheritdoc />
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Track;
+
+ /// <inheritdoc />
+ public string? UrlFormatString => Plugin.Instance.Configuration.Server + "/track/{0}";
+
+ /// <inheritdoc />
+ public bool Supports(IHasProviderIds item) => item is Audio;
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
index 43bd3a472..cfa10dd64 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
@@ -11,6 +12,16 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz
{
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
+ public const string DefaultServer = "https://musicbrainz.org";
+
+ public const long DefaultRateLimit = 2000u;
+
+ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
public static Plugin Instance { get; private set; }
public override Guid Id => new Guid("8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a");
@@ -19,19 +30,9 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz
public override string Description => "Get artist and album metadata from any MusicBrainz server.";
- public const string DefaultServer = "https://musicbrainz.org";
-
- public const long DefaultRateLimit = 2000u;
-
// TODO remove when plugin removed from server.
public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml";
- public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
- : base(applicationPaths, xmlSerializer)
- {
- Instance = this;
- }
-
public IEnumerable<PluginPageInfo> GetPages()
{
yield return new PluginPageInfo
diff --git a/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs
index 196f14e7c..099547005 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/Configuration/PluginConfiguration.cs
@@ -1,4 +1,4 @@
-#pragma warning disable CS1591
+#pragma warning disable CS1591
using MediaBrowser.Model.Plugins;
diff --git a/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs
new file mode 100644
index 000000000..8bfdc461e
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableInt32Converter.cs
@@ -0,0 +1,44 @@
+using System;
+using System.ComponentModel;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Providers.Plugins.Omdb
+{
+ /// <summary>
+ /// Converts a string <c>N/A</c> to <c>string.Empty</c>.
+ /// </summary>
+ public class JsonOmdbNotAvailableInt32Converter : JsonConverter<int?>
+ {
+ /// <inheritdoc />
+ public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ var str = reader.GetString();
+ if (str == null || str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ var converter = TypeDescriptor.GetConverter(typeToConvert);
+ return (int?)converter.ConvertFromString(str);
+ }
+
+ return JsonSerializer.Deserialize<int>(ref reader, options);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
+ {
+ if (value.HasValue)
+ {
+ writer.WriteNumberValue(value.Value);
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs
new file mode 100644
index 000000000..f35880a04
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Omdb/JsonOmdbNotAvailableStringConverter.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Providers.Plugins.Omdb
+{
+ /// <summary>
+ /// Converts a string <c>N/A</c> to <c>string.Empty</c>.
+ /// </summary>
+ public class JsonOmdbNotAvailableStringConverter : JsonConverter<string?>
+ {
+ /// <inheritdoc />
+ public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ // GetString can't return null here because we already handled it above
+ var str = reader.GetString()!;
+ if (str.Equals("N/A", StringComparison.OrdinalIgnoreCase))
+ {
+ return null;
+ }
+
+ return str;
+ }
+
+ return JsonSerializer.Deserialize<string?>(ref reader, options);
+ }
+
+ /// <inheritdoc />
+ public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value);
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
index 24ef80a35..d8b33a799 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs
@@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
@@ -17,24 +16,17 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{
- private readonly IHttpClientFactory _httpClientFactory;
private readonly OmdbItemProvider _itemProvider;
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IApplicationHost _appHost;
+ private readonly OmdbProvider _omdbProvider;
public OmdbEpisodeProvider(
- IApplicationHost appHost,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IFileSystem fileSystem,
IServerConfigurationManager configurationManager)
{
- _httpClientFactory = httpClientFactory;
- _fileSystem = fileSystem;
- _configurationManager = configurationManager;
- _appHost = appHost;
- _itemProvider = new OmdbItemProvider(_appHost, httpClientFactory, libraryManager, fileSystem, configurationManager);
+ _itemProvider = new OmdbItemProvider(httpClientFactory, libraryManager, fileSystem, configurationManager);
+ _omdbProvider = new OmdbProvider(httpClientFactory, fileSystem, configurationManager);
}
// After TheTvDb
@@ -44,12 +36,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
- return _itemProvider.GetSearchResults(searchInfo, "episode", cancellationToken);
+ return _itemProvider.GetSearchResults(searchInfo, cancellationToken);
}
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
{
- var result = new MetadataResult<Episode>()
+ var result = new MetadataResult<Episode>
{
Item = new Episode(),
QueriedById = true
@@ -61,13 +53,20 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return result;
}
- if (info.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string seriesImdbId) && !string.IsNullOrEmpty(seriesImdbId))
+ if (info.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string? seriesImdbId)
+ && !string.IsNullOrEmpty(seriesImdbId)
+ && info.IndexNumber.HasValue
+ && info.ParentIndexNumber.HasValue)
{
- if (info.IndexNumber.HasValue && info.ParentIndexNumber.HasValue)
- {
- result.HasMetadata = await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager)
- .FetchEpisodeData(result, info.IndexNumber.Value, info.ParentIndexNumber.Value, info.GetProviderId(MetadataProvider.Imdb), seriesImdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
- }
+ result.HasMetadata = await _omdbProvider.FetchEpisodeData(
+ result,
+ info.IndexNumber.Value,
+ info.ParentIndexNumber.Value,
+ info.GetProviderId(MetadataProvider.Imdb),
+ seriesImdbId,
+ info.MetadataLanguage,
+ info.MetadataCountryCode,
+ cancellationToken).ConfigureAwait(false);
}
return result;
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
index df67aff31..60b373483 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs
@@ -1,11 +1,12 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
-using System.Globalization;
+using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -21,16 +22,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public class OmdbImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IApplicationHost _appHost;
+ private readonly OmdbProvider _omdbProvider;
- public OmdbImageProvider(IApplicationHost appHost, IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
+ public OmdbImageProvider(IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
{
_httpClientFactory = httpClientFactory;
- _fileSystem = fileSystem;
- _configurationManager = configurationManager;
- _appHost = appHost;
+ _omdbProvider = new OmdbProvider(_httpClientFactory, fileSystem, configurationManager);
}
public string Name => "The Open Movie Database";
@@ -50,38 +47,27 @@ namespace MediaBrowser.Providers.Plugins.Omdb
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var imdbId = item.GetProviderId(MetadataProvider.Imdb);
+ if (string.IsNullOrWhiteSpace(imdbId))
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
- var list = new List<RemoteImageInfo>();
-
- var provider = new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager);
+ var rootObject = await _omdbProvider.GetRootObject(imdbId, cancellationToken).ConfigureAwait(false);
- if (!string.IsNullOrWhiteSpace(imdbId))
+ if (string.IsNullOrEmpty(rootObject.Poster))
{
- var rootObject = await provider.GetRootObject(imdbId, cancellationToken).ConfigureAwait(false);
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
- if (!string.IsNullOrEmpty(rootObject.Poster))
+ // the poster url is sometimes higher quality than the poster api
+ return new[]
+ {
+ new RemoteImageInfo
{
- if (item is Episode)
- {
- // img.omdbapi.com is returning 404's
- list.Add(new RemoteImageInfo
- {
- ProviderName = Name,
- Url = rootObject.Poster
- });
- }
- else
- {
- list.Add(new RemoteImageInfo
- {
- ProviderName = Name,
- Url = string.Format(CultureInfo.InvariantCulture, "https://img.omdbapi.com/?i={0}&apikey=2c9d9507", imdbId)
- });
- }
+ ProviderName = Name,
+ Url = rootObject.Poster
}
- }
-
- return list;
+ };
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
index 428b0ded1..e5753b2b5 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs
@@ -1,4 +1,6 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CS1591, SA1300
using System;
using System.Collections.Generic;
@@ -6,12 +8,11 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -30,13 +31,10 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILibraryManager _libraryManager;
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IApplicationHost _appHost;
private readonly JsonSerializerOptions _jsonOptions;
+ private readonly OmdbProvider _omdbProvider;
public OmdbItemProvider(
- IApplicationHost appHost,
IHttpClientFactory httpClientFactory,
ILibraryManager libraryManager,
IFileSystem fileSystem,
@@ -44,9 +42,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
_httpClientFactory = httpClientFactory;
_libraryManager = libraryManager;
- _fileSystem = fileSystem;
- _configurationManager = configurationManager;
- _appHost = appHost;
+ _omdbProvider = new OmdbProvider(_httpClientFactory, fileSystem, configurationManager);
_jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
_jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter());
@@ -58,185 +54,166 @@ namespace MediaBrowser.Providers.Plugins.Omdb
// After primary option
public int Order => 2;
+ public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken)
+ {
+ return GetSearchResultsInternal(searchInfo, true, cancellationToken);
+ }
+
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
{
- return GetSearchResults(searchInfo, "series", cancellationToken);
+ return GetSearchResultsInternal(searchInfo, true, cancellationToken);
}
public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
{
- return GetSearchResults(searchInfo, "movie", cancellationToken);
+ return GetSearchResultsInternal(searchInfo, true, cancellationToken);
}
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ItemLookupInfo searchInfo, string type, CancellationToken cancellationToken)
+ public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
- return GetSearchResultsInternal(searchInfo, type, true, cancellationToken);
+ return GetSearchResultsInternal(searchInfo, true, cancellationToken);
}
- private async Task<IEnumerable<RemoteSearchResult>> GetSearchResultsInternal(ItemLookupInfo searchInfo, string type, bool isSearch, CancellationToken cancellationToken)
+ private async Task<IEnumerable<RemoteSearchResult>> GetSearchResultsInternal(ItemLookupInfo searchInfo, bool isSearch, CancellationToken cancellationToken)
{
+ var type = searchInfo switch
+ {
+ EpisodeInfo => "episode",
+ SeriesInfo => "series",
+ _ => "movie"
+ };
+
+ // This is a bit hacky?
var episodeSearchInfo = searchInfo as EpisodeInfo;
+ var indexNumberEnd = episodeSearchInfo?.IndexNumberEnd;
var imdbId = searchInfo.GetProviderId(MetadataProvider.Imdb);
- var urlQuery = "plot=full&r=json";
- if (type == "episode" && episodeSearchInfo != null)
+ var urlQuery = new StringBuilder("plot=full&r=json");
+ if (episodeSearchInfo != null)
{
episodeSearchInfo.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out imdbId);
- }
-
- var name = searchInfo.Name;
- var year = searchInfo.Year;
+ if (searchInfo.IndexNumber.HasValue)
+ {
+ urlQuery.Append("&Episode=").Append(searchInfo.IndexNumber.Value);
+ }
- if (!string.IsNullOrWhiteSpace(name))
- {
- var parsedName = _libraryManager.ParseName(name);
- var yearInName = parsedName.Year;
- name = parsedName.Name;
- year ??= yearInName;
+ if (searchInfo.ParentIndexNumber.HasValue)
+ {
+ urlQuery.Append("&Season=").Append(searchInfo.ParentIndexNumber.Value);
+ }
}
if (string.IsNullOrWhiteSpace(imdbId))
{
- if (year.HasValue)
+ var name = searchInfo.Name;
+ var year = searchInfo.Year;
+ if (!string.IsNullOrWhiteSpace(name))
{
- urlQuery += "&y=" + year.Value.ToString(CultureInfo.InvariantCulture);
+ var parsedName = _libraryManager.ParseName(name);
+ var yearInName = parsedName.Year;
+ name = parsedName.Name;
+ year ??= yearInName;
}
- // &s means search and returns a list of results as opposed to t
- if (isSearch)
- {
- urlQuery += "&s=" + WebUtility.UrlEncode(name);
- }
- else
+ if (year.HasValue)
{
- urlQuery += "&t=" + WebUtility.UrlEncode(name);
+ urlQuery.Append("&y=").Append(year);
}
- urlQuery += "&type=" + type;
+ // &s means search and returns a list of results as opposed to t
+ urlQuery.Append(isSearch ? "&s=" : "&t=");
+ urlQuery.Append(WebUtility.UrlEncode(name));
+ urlQuery.Append("&type=")
+ .Append(type);
}
else
{
- urlQuery += "&i=" + imdbId;
+ urlQuery.Append("&i=")
+ .Append(imdbId);
isSearch = false;
}
- if (type == "episode")
- {
- if (searchInfo.IndexNumber.HasValue)
- {
- urlQuery += string.Format(CultureInfo.InvariantCulture, "&Episode={0}", searchInfo.IndexNumber);
- }
-
- if (searchInfo.ParentIndexNumber.HasValue)
- {
- urlQuery += string.Format(CultureInfo.InvariantCulture, "&Season={0}", searchInfo.ParentIndexNumber);
- }
- }
-
- var url = OmdbProvider.GetOmdbUrl(urlQuery);
+ var url = OmdbProvider.GetOmdbUrl(urlQuery.ToString());
- using var response = await OmdbProvider.GetOmdbResponse(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
+ using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var resultList = new List<SearchResult>();
if (isSearch)
{
var searchResultList = await JsonSerializer.DeserializeAsync<SearchResultList>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- if (searchResultList != null && searchResultList.Search != null)
+ if (searchResultList?.Search != null)
{
- resultList.AddRange(searchResultList.Search);
+ var resultCount = searchResultList.Search.Count;
+ var result = new RemoteSearchResult[resultCount];
+ for (var i = 0; i < resultCount; i++)
+ {
+ result[i] = ResultToMetadataResult(searchResultList.Search[i], searchInfo, indexNumberEnd);
+ }
+
+ return result;
}
}
else
{
var result = await JsonSerializer.DeserializeAsync<SearchResult>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
- if (string.Equals(result.Response, "true", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(result?.Response, "true", StringComparison.OrdinalIgnoreCase))
{
- resultList.Add(result);
+ return new[] { ResultToMetadataResult(result, searchInfo, indexNumberEnd) };
}
}
- return resultList.Select(result =>
- {
- var item = new RemoteSearchResult
- {
- IndexNumber = searchInfo.IndexNumber,
- Name = result.Title,
- ParentIndexNumber = searchInfo.ParentIndexNumber,
- SearchProviderName = Name
- };
-
- if (episodeSearchInfo != null && episodeSearchInfo.IndexNumberEnd.HasValue)
- {
- item.IndexNumberEnd = episodeSearchInfo.IndexNumberEnd.Value;
- }
-
- item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
-
- if (result.Year.Length > 0
- && int.TryParse(result.Year.AsSpan().Slice(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
- {
- item.ProductionYear = parsedYear;
- }
-
- if (!string.IsNullOrEmpty(result.Released)
- && DateTime.TryParse(result.Released, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var released))
- {
- item.PremiereDate = released;
- }
-
- if (!string.IsNullOrWhiteSpace(result.Poster) && !string.Equals(result.Poster, "N/A", StringComparison.OrdinalIgnoreCase))
- {
- item.ImageUrl = result.Poster;
- }
-
- return item;
- });
+ return Enumerable.Empty<RemoteSearchResult>();
}
public Task<MetadataResult<Trailer>> GetMetadata(TrailerInfo info, CancellationToken cancellationToken)
{
- return GetMovieResult<Trailer>(info, cancellationToken);
+ return GetResult<Trailer>(info, cancellationToken);
}
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken)
+ public Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
{
- return GetSearchResults(searchInfo, "movie", cancellationToken);
+ return GetResult<Series>(info, cancellationToken);
}
- public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
+ public Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
{
- var result = new MetadataResult<Series>
+ return GetResult<Movie>(info, cancellationToken);
+ }
+
+ private RemoteSearchResult ResultToMetadataResult(SearchResult result, ItemLookupInfo searchInfo, int? indexNumberEnd)
+ {
+ var item = new RemoteSearchResult
{
- Item = new Series(),
- QueriedById = true
+ IndexNumber = searchInfo.IndexNumber,
+ Name = result.Title,
+ ParentIndexNumber = searchInfo.ParentIndexNumber,
+ SearchProviderName = Name,
+ IndexNumberEnd = indexNumberEnd
};
- var imdbId = info.GetProviderId(MetadataProvider.Imdb);
- if (string.IsNullOrWhiteSpace(imdbId))
+ item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
+
+ if (OmdbProvider.TryParseYear(result.Year, out var parsedYear))
{
- imdbId = await GetSeriesImdbId(info, cancellationToken).ConfigureAwait(false);
- result.QueriedById = false;
+ item.ProductionYear = parsedYear;
}
- if (!string.IsNullOrEmpty(imdbId))
+ if (!string.IsNullOrEmpty(result.Released)
+ && DateTime.TryParse(result.Released, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces, out var released))
{
- result.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
- result.HasMetadata = true;
-
- await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+ item.PremiereDate = released;
}
- return result;
- }
+ if (!string.IsNullOrWhiteSpace(result.Poster))
+ {
+ item.ImageUrl = result.Poster;
+ }
- public Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
- {
- return GetMovieResult<Movie>(info, cancellationToken);
+ return item;
}
- private async Task<MetadataResult<T>> GetMovieResult<T>(ItemLookupInfo info, CancellationToken cancellationToken)
+ private async Task<MetadataResult<T>> GetResult<T>(ItemLookupInfo info, CancellationToken cancellationToken)
where T : BaseItem, new()
{
var result = new MetadataResult<T>
@@ -248,7 +225,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var imdbId = info.GetProviderId(MetadataProvider.Imdb);
if (string.IsNullOrWhiteSpace(imdbId))
{
- imdbId = await GetMovieImdbId(info, cancellationToken).ConfigureAwait(false);
+ imdbId = await GetImdbId(info, cancellationToken).ConfigureAwait(false);
result.QueriedById = false;
}
@@ -257,22 +234,15 @@ namespace MediaBrowser.Providers.Plugins.Omdb
result.Item.SetProviderId(MetadataProvider.Imdb, imdbId);
result.HasMetadata = true;
- await new OmdbProvider(_httpClientFactory, _fileSystem, _appHost, _configurationManager).Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
+ await _omdbProvider.Fetch(result, imdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
}
return result;
}
- private async Task<string> GetMovieImdbId(ItemLookupInfo info, CancellationToken cancellationToken)
- {
- var results = await GetSearchResultsInternal(info, "movie", false, cancellationToken).ConfigureAwait(false);
- var first = results.FirstOrDefault();
- return first?.GetProviderId(MetadataProvider.Imdb);
- }
-
- private async Task<string> GetSeriesImdbId(SeriesInfo info, CancellationToken cancellationToken)
+ private async Task<string> GetImdbId(ItemLookupInfo info, CancellationToken cancellationToken)
{
- var results = await GetSearchResultsInternal(info, "series", false, cancellationToken).ConfigureAwait(false);
+ var results = await GetSearchResultsInternal(info, false, cancellationToken).ConfigureAwait(false);
var first = results.FirstOrDefault();
return first?.GetProviderId(MetadataProvider.Imdb);
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index 46d303890..12ea2d55b 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -1,17 +1,19 @@
-#pragma warning disable CS1591
+#nullable disable
+
+#pragma warning disable CS159, SA1300
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Net.Http.Json;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Json;
-using MediaBrowser.Common.Json.Converters;
+using Jellyfin.Extensions.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -21,27 +23,38 @@ using MediaBrowser.Model.IO;
namespace MediaBrowser.Providers.Plugins.Omdb
{
+ /// <summary>Provider for OMDB service.</summary>
public class OmdbProvider
{
private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _configurationManager;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
- private readonly IApplicationHost _appHost;
private readonly JsonSerializerOptions _jsonOptions;
- public OmdbProvider(IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IApplicationHost appHost, IServerConfigurationManager configurationManager)
+ /// <summary>Initializes a new instance of the <see cref="OmdbProvider"/> class.</summary>
+ /// <param name="httpClientFactory">HttpClientFactory to use for calls to OMDB service.</param>
+ /// <param name="fileSystem">IFileSystem to use for store OMDB data.</param>
+ /// <param name="configurationManager">IServerConfigurationManager to use.</param>
+ public OmdbProvider(IHttpClientFactory httpClientFactory, IFileSystem fileSystem, IServerConfigurationManager configurationManager)
{
_httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
_configurationManager = configurationManager;
- _appHost = appHost;
_jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
- _jsonOptions.Converters.Add(new JsonOmdbNotAvailableStringConverter());
- _jsonOptions.Converters.Add(new JsonOmdbNotAvailableInt32Converter());
+ // These converters need to take priority
+ _jsonOptions.Converters.Insert(0, new JsonOmdbNotAvailableStringConverter());
+ _jsonOptions.Converters.Insert(0, new JsonOmdbNotAvailableInt32Converter());
}
+ /// <summary>Fetches data from OMDB service.</summary>
+ /// <param name="itemResult">Metadata about media item.</param>
+ /// <param name="imdbId">IMDB ID for media.</param>
+ /// <param name="language">Media language.</param>
+ /// <param name="country">Country of origin.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <typeparam name="T">The first generic type parameter.</typeparam>
+ /// <returns>Returns a Task object that can be awaited.</returns>
public async Task Fetch<T>(MetadataResult<T> itemResult, string imdbId, string language, string country, CancellationToken cancellationToken)
where T : BaseItem
{
@@ -54,8 +67,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var result = await GetRootObject(imdbId, cancellationToken).ConfigureAwait(false);
+ var isEnglishRequested = IsConfiguredForEnglish(item, language);
// Only take the name and rating if the user's language is set to English, since Omdb has no localization
- if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport)
+ if (isEnglishRequested)
{
item.Name = result.Title;
@@ -65,9 +79,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
}
- if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
- && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
- && year >= 0)
+ if (TryParseYear(result.Year, out var year))
{
item.ProductionYear = year;
}
@@ -80,14 +92,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.imdbVotes)
- && int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out var voteCount)
+ && int.TryParse(result.imdbVotes, NumberStyles.Number, CultureInfo.InvariantCulture, out var voteCount)
&& voteCount >= 0)
{
// item.VoteCount = voteCount;
}
if (!string.IsNullOrEmpty(result.imdbRating)
- && float.TryParse(result.imdbRating, NumberStyles.Any, _usCulture, out var imdbRating)
+ && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating)
&& imdbRating >= 0)
{
item.CommunityRating = imdbRating;
@@ -103,9 +115,20 @@ namespace MediaBrowser.Providers.Plugins.Omdb
item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
}
- ParseAdditionalMetadata(itemResult, result);
+ ParseAdditionalMetadata(itemResult, result, isEnglishRequested);
}
+ /// <summary>Gets data about an episode.</summary>
+ /// <param name="itemResult">Metadata about episode.</param>
+ /// <param name="episodeNumber">Episode number.</param>
+ /// <param name="seasonNumber">Season number.</param>
+ /// <param name="episodeImdbId">Episode ID.</param>
+ /// <param name="seriesImdbId">Season ID.</param>
+ /// <param name="language">Episode language.</param>
+ /// <param name="country">Country of origin.</param>
+ /// <param name="cancellationToken">CancellationToken to use for operation.</param>
+ /// <typeparam name="T">The first generic type parameter.</typeparam>
+ /// <returns>Whether operation was successful.</returns>
public async Task<bool> FetchEpisodeData<T>(MetadataResult<T> itemResult, int episodeNumber, int seasonNumber, string episodeImdbId, string seriesImdbId, string language, string country, CancellationToken cancellationToken)
where T : BaseItem
{
@@ -155,8 +178,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return false;
}
+ var isEnglishRequested = IsConfiguredForEnglish(item, language);
// Only take the name and rating if the user's language is set to English, since Omdb has no localization
- if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport)
+ if (isEnglishRequested)
{
item.Name = result.Title;
@@ -166,9 +190,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
}
- if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
- && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
- && year >= 0)
+ if (TryParseYear(result.Year, out var year))
{
item.ProductionYear = year;
}
@@ -181,14 +203,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.imdbVotes)
- && int.TryParse(result.imdbVotes, NumberStyles.Number, _usCulture, out var voteCount)
+ && int.TryParse(result.imdbVotes, NumberStyles.Number, CultureInfo.InvariantCulture, out var voteCount)
&& voteCount >= 0)
{
// item.VoteCount = voteCount;
}
if (!string.IsNullOrEmpty(result.imdbRating)
- && float.TryParse(result.imdbRating, NumberStyles.Any, _usCulture, out var imdbRating)
+ && float.TryParse(result.imdbRating, NumberStyles.Any, CultureInfo.InvariantCulture, out var imdbRating)
&& imdbRating >= 0)
{
item.CommunityRating = imdbRating;
@@ -204,7 +226,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
}
- ParseAdditionalMetadata(itemResult, result);
+ ParseAdditionalMetadata(itemResult, result, isEnglishRequested);
return true;
}
@@ -212,41 +234,54 @@ namespace MediaBrowser.Providers.Plugins.Omdb
internal async Task<RootObject> GetRootObject(string imdbId, CancellationToken cancellationToken)
{
var path = await EnsureItemInfo(imdbId, cancellationToken).ConfigureAwait(false);
- await using var stream = File.OpenRead(path);
- return await JsonSerializer.DeserializeAsync<RootObject>(stream, _jsonOptions, cancellationToken);
+ await using var stream = AsyncFile.OpenRead(path);
+ return await JsonSerializer.DeserializeAsync<RootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
internal async Task<SeasonRootObject> GetSeasonRootObject(string imdbId, int seasonId, CancellationToken cancellationToken)
{
var path = await EnsureSeasonInfo(imdbId, seasonId, cancellationToken).ConfigureAwait(false);
- await using var stream = File.OpenRead(path);
- return await JsonSerializer.DeserializeAsync<SeasonRootObject>(stream, _jsonOptions, cancellationToken);
+ await using var stream = AsyncFile.OpenRead(path);
+ return await JsonSerializer.DeserializeAsync<SeasonRootObject>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
- internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds)
+ /// <summary>Gets OMDB URL.</summary>
+ /// <param name="query">Appends query string to URL.</param>
+ /// <returns>OMDB URL with optional query string.</returns>
+ public static string GetOmdbUrl(string query)
{
- if (seriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out string id) && !string.IsNullOrEmpty(id))
+ const string Url = "https://www.omdbapi.com?apikey=2c9d9507";
+
+ if (string.IsNullOrWhiteSpace(query))
{
- // This check should ideally never be necessary but we're seeing some cases of this and haven't tracked them down yet.
- if (!string.IsNullOrWhiteSpace(id))
- {
- return true;
- }
+ return Url;
}
- return false;
+ return Url + "&" + query;
}
- public static string GetOmdbUrl(string query)
+ /// <summary>
+ /// Extract the year from a string.
+ /// </summary>
+ /// <param name="input">The input string.</param>
+ /// <param name="year">The year.</param>
+ /// <returns>A value indicating whether the input could successfully be parsed as a year.</returns>
+ public static bool TryParseYear(string input, [NotNullWhen(true)] out int? year)
{
- const string Url = "https://www.omdbapi.com?apikey=2c9d9507";
+ if (string.IsNullOrEmpty(input))
+ {
+ year = 0;
+ return false;
+ }
- if (string.IsNullOrWhiteSpace(query))
+ if (int.TryParse(input.AsSpan(0, 4), NumberStyles.Number, CultureInfo.InvariantCulture, out var result))
{
- return Url;
+ year = result;
+ return true;
}
- return Url + "&" + query;
+ year = 0;
+ return false;
}
private async Task<string> EnsureItemInfo(string imdbId, CancellationToken cancellationToken)
@@ -281,8 +316,8 @@ namespace MediaBrowser.Providers.Plugins.Omdb
"i={0}&plot=short&tomatoes=true&r=json",
imdbParam));
- var rootObject = await GetDeserializedOmdbResponse<RootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+ var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync<RootObject>(url, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
@@ -321,26 +356,13 @@ namespace MediaBrowser.Providers.Plugins.Omdb
imdbParam,
seasonId));
- var rootObject = await GetDeserializedOmdbResponse<SeasonRootObject>(_httpClientFactory.CreateClient(NamedClient.Default), url, cancellationToken).ConfigureAwait(false);
- await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
+ var rootObject = await _httpClientFactory.CreateClient(NamedClient.Default).GetFromJsonAsync<SeasonRootObject>(url, _jsonOptions, cancellationToken).ConfigureAwait(false);
+ await using FileStream jsonFileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await JsonSerializer.SerializeAsync(jsonFileStream, rootObject, _jsonOptions, cancellationToken).ConfigureAwait(false);
return path;
}
- public async Task<T> GetDeserializedOmdbResponse<T>(HttpClient httpClient, string url, CancellationToken cancellationToken)
- {
- using var response = await GetOmdbResponse(httpClient, url, cancellationToken).ConfigureAwait(false);
- await using Stream content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-
- return await JsonSerializer.DeserializeAsync<T>(content, _jsonOptions, cancellationToken).ConfigureAwait(false);
- }
-
- public static Task<HttpResponseMessage> GetOmdbResponse(HttpClient httpClient, string url, CancellationToken cancellationToken)
- {
- return httpClient.GetAsync(url, cancellationToken);
- }
-
internal string GetDataFilePath(string imdbId)
{
if (string.IsNullOrEmpty(imdbId))
@@ -369,31 +391,25 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return Path.Combine(dataPath, filename);
}
- private void ParseAdditionalMetadata<T>(MetadataResult<T> itemResult, RootObject result)
+ private static void ParseAdditionalMetadata<T>(MetadataResult<T> itemResult, RootObject result, bool isEnglishRequested)
where T : BaseItem
{
var item = itemResult.Item;
- var isConfiguredForEnglish = IsConfiguredForEnglish(item) || _configurationManager.Configuration.EnableNewOmdbSupport;
-
// Grab series genres because IMDb data is better than TVDB. Leave movies alone
// But only do it if English is the preferred language because this data will not be localized
- if (isConfiguredForEnglish && !string.IsNullOrWhiteSpace(result.Genre))
+ if (isEnglishRequested && !string.IsNullOrWhiteSpace(result.Genre))
{
item.Genres = Array.Empty<string>();
- foreach (var genre in result.Genre
- .Split(',', StringSplitOptions.RemoveEmptyEntries)
- .Select(i => i.Trim())
- .Where(i => !string.IsNullOrWhiteSpace(i)))
+ foreach (var genre in result.Genre.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
item.AddGenre(genre);
}
}
- if (isConfiguredForEnglish)
+ if (isEnglishRequested)
{
- // Omdb is currently English only, so for other languages skip this and let secondary providers fill it in
item.Overview = result.Plot;
}
@@ -406,7 +422,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
var person = new PersonInfo
{
- Name = result.Director.Trim(),
+ Name = result.Director,
Type = PersonType.Director
};
@@ -417,7 +433,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
var person = new PersonInfo
{
- Name = result.Writer.Trim(),
+ Name = result.Writer,
Type = PersonType.Writer
};
@@ -426,29 +442,34 @@ namespace MediaBrowser.Providers.Plugins.Omdb
if (!string.IsNullOrWhiteSpace(result.Actors))
{
- var actorList = result.Actors.Split(',');
+ var actorList = result.Actors.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
foreach (var actor in actorList)
{
- if (!string.IsNullOrWhiteSpace(actor))
+ if (string.IsNullOrWhiteSpace(actor))
{
- var person = new PersonInfo
- {
- Name = actor.Trim(),
- Type = PersonType.Actor
- };
-
- itemResult.AddPerson(person);
+ continue;
}
+
+ var person = new PersonInfo
+ {
+ Name = actor,
+ Type = PersonType.Actor
+ };
+
+ itemResult.AddPerson(person);
}
}
}
- private bool IsConfiguredForEnglish(BaseItem item)
+ private static bool IsConfiguredForEnglish(BaseItem item, string language)
{
- var lang = item.GetPreferredMetadataLanguage();
+ if (string.IsNullOrEmpty(language))
+ {
+ language = item.GetPreferredMetadataLanguage();
+ }
// The data isn't localized and so can only be used for English users
- return string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase);
+ return string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
}
internal class SeasonRootObject
@@ -525,7 +546,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
if (Ratings != null)
{
var rating = Ratings.FirstOrDefault(i => string.Equals(i.Source, "Rotten Tomatoes", StringComparison.OrdinalIgnoreCase));
- if (rating != null && rating.Value != null)
+ if (rating?.Value != null)
{
var value = rating.Value.TrimEnd('%');
if (float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var score))
@@ -539,10 +560,14 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
}
+#pragma warning disable CA1034
+ /// <summary>Describes OMDB rating.</summary>
public class OmdbRating
{
+ /// <summary>Gets or sets rating source.</summary>
public string Source { get; set; }
+ /// <summary>Gets or sets rating value.</summary>
public string Value { get; set; }
}
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
index d7f6781e5..a0fba48f0 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs
@@ -1,3 +1,4 @@
+#nullable disable
#pragma warning disable CS1591
using System;
@@ -11,6 +12,12 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
{
+ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
public static Plugin Instance { get; private set; }
public override Guid Id => new Guid("a628c0da-fac5-4c7e-9d1a-7134223f14c8");
@@ -22,12 +29,6 @@ namespace MediaBrowser.Providers.Plugins.Omdb
// TODO remove when plugin removed from server.
public override string ConfigurationFileName => "Jellyfin.Plugin.Omdb.xml";
- public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
- : base(applicationPaths, xmlSerializer)
- {
- Instance = this;
- }
-
public IEnumerable<PluginPageInfo> GetPages()
{
yield return new PluginPageInfo
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs
new file mode 100644
index 000000000..fad989ab4
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/PluginConfiguration.cs
@@ -0,0 +1,24 @@
+#pragma warning disable CS1591
+
+using MediaBrowser.Model.Plugins;
+
+namespace MediaBrowser.Providers.Plugins.StudioImages
+{
+ public class PluginConfiguration : BasePluginConfiguration
+ {
+ private string _repository = Plugin.DefaultServer;
+
+ public string RepositoryUrl
+ {
+ get
+ {
+ return _repository;
+ }
+
+ set
+ {
+ _repository = value.TrimEnd('/');
+ }
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html
new file mode 100644
index 000000000..f9fe3dc2e
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/StudioImages/Configuration/config.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Studio Images</title>
+</head>
+<body>
+ <div data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
+ <div data-role="content">
+ <div class="content-primary">
+ <form class="configForm">
+ <div class="inputContainer">
+ <input is="emby-input" type="text" id="repository" required label="Repository" />
+ <div class="fieldDescription">This can be any Jellyfin-compatible artwork repository.</div>
+ </div>
+ <br />
+ <div>
+ <button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button>
+ </div>
+ </form>
+ </div>
+ </div>
+ <script type="text/javascript">
+ var PluginConfig = {
+ pluginId: "872a7849-1171-458d-a6fb-3de3d442ad30"
+ };
+
+ document.querySelector('.configPage')
+ .addEventListener('pageshow', function () {
+ Dashboard.showLoadingMsg();
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ var repository = document.querySelector('#repository');
+ repository.value = config.RepositoryUrl;
+ repository.dispatchEvent(new Event('change', {
+ bubbles: true,
+ cancelable: false
+ }));
+
+ Dashboard.hideLoadingMsg();
+ });
+ });
+
+ document.querySelector('.configForm')
+ .addEventListener('submit', function (e) {
+ Dashboard.showLoadingMsg();
+
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ config.RepositoryUrl = document.querySelector('#server').value;
+
+ ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+ });
+
+ e.preventDefault();
+ return false;
+ });
+ </script>
+ </div>
+</body>
+</html>
diff --git a/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
new file mode 100644
index 000000000..69a0569e5
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/StudioImages/Plugin.cs
@@ -0,0 +1,43 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Providers.Plugins.StudioImages
+{
+ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
+ {
+ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
+ public static Plugin Instance { get; private set; }
+
+ public override Guid Id => new Guid("872a7849-1171-458d-a6fb-3de3d442ad30");
+
+ public override string Name => "Studio Images";
+
+ public override string Description => "Get artwork for studios from any Jellyfin-compatible repository.";
+
+ // TODO change this for a Jellyfin-hosted repository.
+ public const string DefaultServer = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname";
+
+ // TODO remove when plugin removed from server.
+ public override string ConfigurationFileName => "Jellyfin.Plugin.StudioImages.xml";
+
+ public IEnumerable<PluginPageInfo> GetPages()
+ {
+ yield return new PluginPageInfo
+ {
+ Name = Name,
+ EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html"
+ };
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
index f6153dd53..6fa34b985 100644
--- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -8,7 +10,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
@@ -16,6 +18,7 @@ using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
+using MediaBrowser.Providers.Plugins.StudioImages;
namespace MediaBrowser.Providers.Studios
{
@@ -24,15 +27,17 @@ namespace MediaBrowser.Providers.Studios
private readonly IServerConfigurationManager _config;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IFileSystem _fileSystem;
+ private readonly String repositoryUrl;
public StudiosImageProvider(IServerConfigurationManager config, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
{
_config = config;
_httpClientFactory = httpClientFactory;
_fileSystem = fileSystem;
+ repositoryUrl = Plugin.Instance.Configuration.RepositoryUrl;
}
- public string Name => "Emby Designs";
+ public string Name => "Artwork Repository";
public int Order => 0;
@@ -105,19 +110,19 @@ namespace MediaBrowser.Providers.Studios
private string GetUrl(string image, string filename)
{
- return string.Format(CultureInfo.InvariantCulture, "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studios/{0}/{1}.jpg", image, filename);
+ return string.Format(CultureInfo.InvariantCulture, "{0}/{1}/{2}.jpg", repositoryUrl, image, filename);
}
private Task<string> EnsureThumbsList(string file, CancellationToken cancellationToken)
{
- const string url = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studiothumbs.txt";
+ string url = string.Format(CultureInfo.InvariantCulture, "{0}/studiothumbs.txt", repositoryUrl);
return EnsureList(url, file, _fileSystem, cancellationToken);
}
private Task<string> EnsurePosterList(string file, CancellationToken cancellationToken)
{
- const string url = "https://raw.github.com/MediaBrowser/MediaBrowser.Resources/master/images/imagesbyname/studioposters.txt";
+ string url = string.Format(CultureInfo.InvariantCulture, "{0}/studioposters.txt", repositoryUrl);
return EnsureList(url, file, _fileSystem, cancellationToken);
}
@@ -146,7 +151,7 @@ namespace MediaBrowser.Providers.Studios
Directory.CreateDirectory(Path.GetDirectoryName(file));
await using var response = await httpClient.GetStreamAsync(url, cancellationToken).ConfigureAwait(false);
- await using var fileStream = new FileStream(file, FileMode.Create);
+ await using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await response.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
@@ -172,19 +177,16 @@ namespace MediaBrowser.Providers.Studios
public IEnumerable<string> GetAvailableImages(string file)
{
- using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
+ using var fileStream = File.OpenRead(file);
using var reader = new StreamReader(fileStream);
- var lines = new List<string>();
foreach (var line in reader.ReadAllLines())
{
if (!string.IsNullOrWhiteSpace(line))
{
- lines.Add(line);
+ yield return line;
}
}
-
- return lines;
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
new file mode 100644
index 000000000..0bab7c3ca
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Api/TmdbController.cs
@@ -0,0 +1,41 @@
+using System.Net.Mime;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using TMDbLib.Objects.General;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Api
+{
+ /// <summary>
+ /// The TMDb api controller.
+ /// </summary>
+ [ApiController]
+ [Authorize(Policy = "DefaultAuthorization")]
+ [Route("[controller]")]
+ [Produces(MediaTypeNames.Application.Json)]
+ public class TmdbController : ControllerBase
+ {
+ private readonly TmdbClientManager _tmdbClientManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TmdbController"/> class.
+ /// </summary>
+ /// <param name="tmdbClientManager">The TMDb client manager.</param>
+ public TmdbController(TmdbClientManager tmdbClientManager)
+ {
+ _tmdbClientManager = tmdbClientManager;
+ }
+
+ /// <summary>
+ /// Gets the TMDb image configuration options.
+ /// </summary>
+ /// <returns>The image portion of the TMDb client configuration.</returns>
+ [HttpGet("ClientConfiguration")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task<ConfigImageTypes> TmdbClientConfiguration()
+ {
+ return (await _tmdbClientManager.GetClientConfiguration().ConfigureAwait(false)).Images;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
index 1f7ec6433..3217ac2f1 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs
@@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet;
/// <inheritdoc />
- public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
+ public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
index 5ad61c567..29a557c31 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -11,9 +13,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
@@ -66,42 +66,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
return Enumerable.Empty<RemoteImageInfo>();
}
- var remoteImages = new List<RemoteImageInfo>();
-
- for (var i = 0; i < collection.Images.Posters.Count; i++)
- {
- var poster = collection.Images.Posters[i];
- remoteImages.Add(new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
- CommunityRating = poster.VoteAverage,
- VoteCount = poster.VoteCount,
- Width = poster.Width,
- Height = poster.Height,
- Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- });
- }
+ var posters = collection.Images.Posters;
+ var backdrops = collection.Images.Backdrops;
+ var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
- for (var i = 0; i < collection.Images.Backdrops.Count; i++)
- {
- var backdrop = collection.Images.Backdrops[i];
- remoteImages.Add(new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath),
- CommunityRating = backdrop.VoteAverage,
- VoteCount = backdrop.VoteCount,
- Width = backdrop.Width,
- Height = backdrop.Height,
- ProviderName = Name,
- Type = ImageType.Backdrop,
- RatingType = RatingType.Score
- });
- }
+ _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
+ _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
index ca1af6c49..62bc9c65f 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -79,16 +81,16 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
return collections;
}
- public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken)
+ public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo info, CancellationToken cancellationToken)
{
- var tmdbId = Convert.ToInt32(id.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- var language = id.MetadataLanguage;
+ var tmdbId = Convert.ToInt32(info.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
+ var language = info.MetadataLanguage;
// We don't already have an Id, need to fetch it
if (tmdbId <= 0)
{
// ParseName is required here.
// Caller provides the filename with extension stripped and NOT the parsed filename
- var parsedName = _libraryManager.ParseName(id.Name);
+ var parsedName = _libraryManager.ParseName(info.Name);
var cleanedName = TmdbUtils.CleanName(parsedName.Name);
var searchResults = await _tmdbClientManager.SearchCollectionAsync(cleanedName, language, cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
new file mode 100644
index 000000000..dec796148
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
@@ -0,0 +1,50 @@
+using MediaBrowser.Model.Plugins;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb
+{
+ /// <summary>
+ /// Plugin configuration class for TMDb library.
+ /// </summary>
+ public class PluginConfiguration : BasePluginConfiguration
+ {
+ /// <summary>
+ /// Gets or sets a value indicating whether include adult content when searching with TMDb.
+ /// </summary>
+ public bool IncludeAdult { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether tags should be imported for series from TMDb.
+ /// </summary>
+ public bool ExcludeTagsSeries { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether tags should be imported for movies from TMDb.
+ /// </summary>
+ public bool ExcludeTagsMovies { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating the maximum number of cast members to fetch for an item.
+ /// </summary>
+ public int MaxCastMembers { get; set; } = 15;
+
+ /// <summary>
+ /// Gets or sets a value indicating the poster image size to fetch.
+ /// </summary>
+ public string? PosterSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating the backdrop image size to fetch.
+ /// </summary>
+ public string? BackdropSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating the profile image size to fetch.
+ /// </summary>
+ public string? ProfileSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating the still image size to fetch.
+ /// </summary>
+ public string? StillSize { get; set; }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
new file mode 100644
index 000000000..52693795b
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
@@ -0,0 +1,137 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>TMDb</title>
+</head>
+<body>
+ <div data-role="page" class="page type-interior pluginConfigurationPage configPage" data-require="emby-input,emby-button,emby-checkbox">
+ <div data-role="content">
+ <div class="content-primary">
+ <form class="configForm">
+ <label class="checkboxContainer">
+ <input is="emby-checkbox" type="checkbox" id="includeAdult" />
+ <span>Include adult content in search results.</span>
+ </label>
+ <label class="checkboxContainer">
+ <input is="emby-checkbox" type="checkbox" id="excludeTagsSeries" />
+ <span>Exclude tags/keywords from metadata fetched for series.</span>
+ </label>
+ <label class="checkboxContainer">
+ <input is="emby-checkbox" type="checkbox" id="excludeTagsMovies" />
+ <span>Exclude tags/keywords from metadata fetched for movies.</span>
+ </label>
+ <div class="inputContainer">
+ <input is="emby-input" type="number" id="maxCastMembers" pattern="[0-9]*" required min="0" max="1000" label="Max Cast Members" />
+ <div class="fieldDescription">The maximum number of cast members to fetch for an item.</div>
+ </div>
+ <div class="verticalSection verticalSection-extrabottompadding">
+ <h2>Image Scaling</h2>
+ <div class="selectContainer">
+ <select is="emby-select" id="selectPosterSize" label="Poster"></select>
+ </div>
+ <div class="selectContainer">
+ <select is="emby-select" id="selectBackdropSize" label="Backdrop"></select>
+ </div>
+ <div class="selectContainer">
+ <select is="emby-select" id="selectProfileSize" label="Profile"></select>
+ </div>
+ <div class="selectContainer">
+ <select is="emby-select" id="selectStillSize" label="Still"></select>
+ </div>
+ </div>
+ <div>
+ <button is="emby-button" type="submit" class="raised button-submit block"><span>Save</span></button>
+ </div>
+ </form>
+ </div>
+ </div>
+ <script type="text/javascript">
+ var PluginConfig = {
+ pluginId: "b8715ed1-6c47-4528-9ad3-f72deb539cd4"
+ };
+
+ document.querySelector('.configPage')
+ .addEventListener('pageshow', function () {
+ Dashboard.showLoadingMsg();
+
+ var clientConfig, pluginConfig;
+ var configureImageScaling = function() {
+ if (clientConfig === null || pluginConfig === null) {
+ return;
+ }
+
+ var sizeOptionsGenerator = function (size) {
+ return '<option value="' + size + '">' + size + '</option>';
+ }
+
+ var selPosterSize = document.querySelector('#selectPosterSize');
+ selPosterSize.innerHTML = clientConfig.PosterSizes.map(sizeOptionsGenerator);
+ selPosterSize.value = pluginConfig.PosterSize;
+
+ var selBackdropSize = document.querySelector('#selectBackdropSize');
+ selBackdropSize.innerHTML = clientConfig.BackdropSizes.map(sizeOptionsGenerator);
+ selBackdropSize.value = pluginConfig.BackdropSize;
+
+ var selProfileSize = document.querySelector('#selectProfileSize');
+ selProfileSize.innerHTML = clientConfig.ProfileSizes.map(sizeOptionsGenerator);
+ selProfileSize.value = pluginConfig.ProfileSize;
+
+ var selStillSize = document.querySelector('#selectStillSize');
+ selStillSize.innerHTML = clientConfig.StillSizes.map(sizeOptionsGenerator);
+ selStillSize.value = pluginConfig.StillSize;
+
+ Dashboard.hideLoadingMsg();
+ }
+
+ const request = {
+ url: ApiClient.getUrl('tmdb/ClientConfiguration'),
+ dataType: 'json',
+ type: 'GET',
+ headers: { accept: 'application/json' }
+ }
+ ApiClient.fetch(request).then(function (config) {
+ clientConfig = config;
+ configureImageScaling();
+ });
+
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ document.querySelector('#includeAdult').checked = config.IncludeAdult;
+ document.querySelector('#excludeTagsSeries').checked = config.ExcludeTagsSeries;
+ document.querySelector('#excludeTagsMovies').checked = config.ExcludeTagsMovies;
+
+ var maxCastMembers = document.querySelector('#maxCastMembers');
+ maxCastMembers.value = config.MaxCastMembers;
+ maxCastMembers.dispatchEvent(new Event('change', {
+ bubbles: true,
+ cancelable: false
+ }));
+
+ pluginConfig = config;
+ configureImageScaling();
+ });
+ });
+
+
+ document.querySelector('.configForm')
+ .addEventListener('submit', function (e) {
+ Dashboard.showLoadingMsg();
+
+ ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) {
+ config.IncludeAdult = document.querySelector('#includeAdult').checked;
+ config.ExcludeTagsSeries = document.querySelector('#excludeTagsSeries').checked;
+ config.ExcludeTagsMovies = document.querySelector('#excludeTagsMovies').checked;
+ config.MaxCastMembers = document.querySelector('#maxCastMembers').value;
+ config.PosterSize = document.querySelector('#selectPosterSize').value;
+ config.BackdropSize = document.querySelector('#selectBackdropSize').value;
+ config.ProfileSize = document.querySelector('#selectProfileSize').value;
+ config.StillSize = document.querySelector('#selectStillSize').value;
+ ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
+ });
+
+ e.preventDefault();
+ return false;
+ });
+ </script>
+ </div>
+</body>
+</html>
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
index f1a1b65d8..31310a8d4 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
@@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
public ExternalIdMediaType? Type => ExternalIdMediaType.Movie;
/// <inheritdoc />
- public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
+ public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
index f34d689c1..f71f7bd10 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -11,9 +13,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
using TMDbLib.Objects.Find;
@@ -83,42 +83,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return Enumerable.Empty<RemoteImageInfo>();
}
- var remoteImages = new List<RemoteImageInfo>();
-
- for (var i = 0; i < movie.Images.Posters.Count; i++)
- {
- var poster = movie.Images.Posters[i];
- remoteImages.Add(new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
- CommunityRating = poster.VoteAverage,
- VoteCount = poster.VoteCount,
- Width = poster.Width,
- Height = poster.Height,
- Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- });
- }
+ var posters = movie.Images.Posters;
+ var backdrops = movie.Images.Backdrops;
+ var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
- for (var i = 0; i < movie.Images.Backdrops.Count; i++)
- {
- var backdrop = movie.Images.Backdrops[i];
- remoteImages.Add(new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetPosterUrl(backdrop.FilePath),
- CommunityRating = backdrop.VoteAverage,
- VoteCount = backdrop.VoteCount,
- Width = backdrop.Width,
- Height = backdrop.Height,
- ProviderName = Name,
- Type = ImageType.Backdrop,
- RatingType = RatingType.Score
- });
- }
+ _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
+ _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index 4a0884c07..e4a56fde9 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,6 +9,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -154,7 +157,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var movieResultFromImdbId = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (movieResultFromImdbId?.MovieResults.Count > 0)
{
- tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString();
+ tmdbId = movieResultFromImdbId.MovieResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
}
@@ -239,8 +242,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
if (movieResult.Credits?.Cast != null)
{
- // TODO configurable
- foreach (var actor in movieResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ foreach (var actor in movieResult.Credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
{
var personInfo = new PersonInfo
{
@@ -278,8 +280,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
- if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) &&
- !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (!keepTypes.Contains(type, StringComparison.OrdinalIgnoreCase) &&
+ !keepTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
index de74a7a4c..9804d60bd 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
public ExternalIdMediaType? Type => ExternalIdMediaType.Person;
/// <inheritdoc />
- public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
+ public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
index e4c908a62..7ce4cfe67 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
@@ -10,7 +10,6 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.People
@@ -61,23 +60,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
return Enumerable.Empty<RemoteImageInfo>();
}
- var remoteImages = new RemoteImageInfo[personResult.Images.Profiles.Count];
+ var profiles = personResult.Images.Profiles;
+ var remoteImages = new List<RemoteImageInfo>(profiles.Count);
- for (var i = 0; i < personResult.Images.Profiles.Count; i++)
- {
- var image = personResult.Images.Profiles[i];
- remoteImages[i] = new RemoteImageInfo
- {
- ProviderName = Name,
- Type = ImageType.Primary,
- Width = image.Width,
- Height = image.Height,
- Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
- Url = _tmdbClientManager.GetProfileUrl(image.FilePath)
- };
- }
+ _tmdbClientManager.ConvertProfilesToRemoteImageInfo(profiles, language, remoteImages);
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index 6db550b1d..8790e3759 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -77,14 +79,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
return remoteSearchResults;
}
- public async Task<MetadataResult<Person>> GetMetadata(PersonLookupInfo id, CancellationToken cancellationToken)
+ public async Task<MetadataResult<Person>> GetMetadata(PersonLookupInfo info, CancellationToken cancellationToken)
{
- var personTmdbId = Convert.ToInt32(id.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
+ var personTmdbId = Convert.ToInt32(info.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
// We don't already have an Id, need to fetch it
if (personTmdbId <= 0)
{
- var personSearchResults = await _tmdbClientManager.SearchPersonAsync(id.Name, cancellationToken).ConfigureAwait(false);
+ var personSearchResults = await _tmdbClientManager.SearchPersonAsync(info.Name, cancellationToken).ConfigureAwait(false);
if (personSearchResults.Count > 0)
{
personTmdbId = personSearchResults[0].Id;
@@ -95,7 +97,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
if (personTmdbId > 0)
{
- var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, id.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+ var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
result.HasMetadata = true;
@@ -103,7 +105,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
{
// Take name from incoming info, don't rename the person
// TODO: This should go in PersonMetadataService, not each person provider
- Name = id.Name,
+ Name = info.Name,
HomePageUrl = person.Homepage,
Overview = person.Biography,
PremiereDate = person.Birthday?.ToUniversalTime(),
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs
new file mode 100644
index 000000000..4adde8366
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Plugin.cs
@@ -0,0 +1,60 @@
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Model.Plugins;
+using MediaBrowser.Model.Serialization;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb
+{
+ /// <summary>
+ /// Plugin class for the TMDb library.
+ /// </summary>
+ public class Plugin : BasePlugin<PluginConfiguration>, IHasWebPages
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Plugin"/> class.
+ /// </summary>
+ /// <param name="applicationPaths">application paths.</param>
+ /// <param name="xmlSerializer">xml serializer.</param>
+ public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
+ : base(applicationPaths, xmlSerializer)
+ {
+ Instance = this;
+ }
+
+ /// <summary>
+ /// Gets the instance of TMDb plugin.
+ /// </summary>
+ public static Plugin Instance { get; private set; }
+
+ /// <inheritdoc/>
+ public override Guid Id => new Guid("b8715ed1-6c47-4528-9ad3-f72deb539cd4");
+
+ /// <inheritdoc/>
+ public override string Name => "TMDb";
+
+ /// <inheritdoc/>
+ public override string Description => "Get metadata for movies and other video content from TheMovieDb.";
+
+ // TODO remove when plugin removed from server.
+
+ /// <inheritdoc/>
+ public override string ConfigurationFileName => "Jellyfin.Plugin.Tmdb.xml";
+
+ /// <summary>
+ /// Return the plugin configuration page.
+ /// </summary>
+ /// <returns>PluginPageInfo.</returns>
+ public IEnumerable<PluginPageInfo> GetPages()
+ {
+ yield return new PluginPageInfo
+ {
+ Name = Name,
+ EmbeddedResourcePath = GetType().Namespace + ".Configuration.config.html"
+ };
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
index ba18c542f..5eec776b5 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -10,9 +12,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
@@ -74,25 +74,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return Enumerable.Empty<RemoteImageInfo>();
}
- var remoteImages = new RemoteImageInfo[stills.Count];
- for (var i = 0; i < stills.Count; i++)
- {
- var image = stills[i];
- remoteImages[i] = new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetStillUrl(image.FilePath),
- CommunityRating = image.VoteAverage,
- VoteCount = image.VoteCount,
- Width = image.Width,
- Height = image.Height,
- Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- };
- }
+ var remoteImages = new List<RemoteImageInfo>(stills.Count);
+
+ _tmdbClientManager.ConvertStillsToRemoteImageInfo(stills, language, remoteImages);
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index 8ec8f6464..f50f15877 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,6 +9,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -152,7 +155,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (credits?.Cast != null)
{
- foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
{
metadataResult.AddPerson(new PersonInfo
{
@@ -166,7 +169,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (credits?.GuestStars != null)
{
- foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
{
metadataResult.AddPerson(new PersonInfo
{
@@ -186,8 +189,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
- if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
- && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparison.OrdinalIgnoreCase)
+ && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
index 0d23c7872..4446fa966 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
@@ -11,9 +11,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
@@ -63,25 +61,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return Enumerable.Empty<RemoteImageInfo>();
}
- var remoteImages = new RemoteImageInfo[posters.Count];
- for (var i = 0; i < posters.Count; i++)
- {
- var image = posters[i];
- remoteImages[i] = new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetPosterUrl(image.FilePath),
- CommunityRating = image.VoteAverage,
- VoteCount = image.VoteCount,
- Width = image.Width,
- Height = image.Height,
- Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- };
- }
+ var remoteImages = new List<RemoteImageInfo>(posters.Count);
+
+ _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 4c1f69763..27c52a5a2 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -7,6 +7,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -33,7 +34,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
var result = new MetadataResult<Season>();
- info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string seriesTmdbId);
+ info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string? seriesTmdbId);
var seasonNumber = info.IndexNumber;
@@ -55,7 +56,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
result.Item = new Season
{
IndexNumber = seasonNumber,
- Overview = seasonResult?.Overview
+ Overview = seasonResult.Overview
};
if (!string.IsNullOrEmpty(seasonResult.ExternalIds?.TvdbId))
@@ -67,7 +68,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var credits = seasonResult.Credits;
if (credits?.Cast != null)
{
- var cast = credits.Cast.OrderBy(c => c.Order).Take(TmdbUtils.MaxCastMembers).ToList();
+ var cast = credits.Cast.OrderBy(c => c.Order).Take(Plugin.Instance.Configuration.MaxCastMembers).ToList();
for (var i = 0; i < cast.Count; i++)
{
result.AddPerson(new PersonInfo
@@ -87,8 +88,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
- if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
- && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparison.OrdinalIgnoreCase)
+ && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
index 6ecc055d7..8a2be80cd 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs
@@ -20,7 +20,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public ExternalIdMediaType? Type => ExternalIdMediaType.Series;
/// <inheritdoc />
- public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
+ public string? UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 326c116b3..5ef3736c4 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -11,9 +11,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
@@ -71,43 +69,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var posters = series.Images.Posters;
var backdrops = series.Images.Backdrops;
+ var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
- var remoteImages = new RemoteImageInfo[posters.Count + backdrops.Count];
+ _tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
+ _tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
- for (var i = 0; i < posters.Count; i++)
- {
- var poster = posters[i];
- remoteImages[i] = new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
- CommunityRating = poster.VoteAverage,
- VoteCount = poster.VoteCount,
- Width = poster.Width,
- Height = poster.Height,
- Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
- ProviderName = Name,
- Type = ImageType.Primary,
- RatingType = RatingType.Score
- };
- }
-
- for (var i = 0; i < backdrops.Count; i++)
- {
- var backdrop = series.Images.Backdrops[i];
- remoteImages[posters.Count + i] = new RemoteImageInfo
- {
- Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath),
- CommunityRating = backdrop.VoteAverage,
- VoteCount = backdrop.VoteCount,
- Width = backdrop.Width,
- Height = backdrop.Height,
- ProviderName = Name,
- Type = ImageType.Backdrop,
- RatingType = RatingType.Score
- };
- }
-
- return remoteImages.OrderByLanguageDescending(language);
+ return remoteImages;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index da76345b5..f565b6569 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,6 +9,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
@@ -329,7 +332,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
if (seriesResult.Credits?.Cast != null)
{
- foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(Plugin.Instance.Configuration.MaxCastMembers))
{
var personInfo = new PersonInfo
{
@@ -363,8 +366,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
// Normalize this
var type = TmdbUtils.MapCrewToPersonType(person);
- if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
- && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ if (!keepTypes.Contains(type, StringComparison.OrdinalIgnoreCase)
+ && !keepTypes.Contains(person.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase))
{
continue;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index 79ec6139d..28d6f4d0c 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -1,8 +1,13 @@
-using System;
+#nullable disable
+
+using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Caching.Memory;
using TMDbLib.Client;
using TMDbLib.Objects.Collections;
@@ -18,7 +23,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <summary>
/// Manager class for abstracting the TMDb API client library.
/// </summary>
- public class TmdbClientManager
+ public class TmdbClientManager : IDisposable
{
private const int CacheDurationInHours = 1;
@@ -55,11 +60,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
+ var extraMethods = MovieMethods.Credits | MovieMethods.Releases | MovieMethods.Images | MovieMethods.Videos;
+ if (!(Plugin.Instance?.Configuration.ExcludeTagsMovies).GetValueOrDefault())
+ {
+ extraMethods |= MovieMethods.Keywords;
+ }
+
movie = await _tmDbClient.GetMovieAsync(
tmdbId,
TmdbUtils.NormalizeLanguage(language),
imageLanguages,
- MovieMethods.Credits | MovieMethods.Releases | MovieMethods.Images | MovieMethods.Keywords | MovieMethods.Videos,
+ extraMethods,
cancellationToken).ConfigureAwait(false);
if (movie != null)
@@ -121,11 +132,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
+ var extraMethods = TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings | TvShowMethods.EpisodeGroups;
+ if (!(Plugin.Instance?.Configuration.ExcludeTagsSeries).GetValueOrDefault())
+ {
+ extraMethods |= TvShowMethods.Keywords;
+ }
+
series = await _tmDbClient.GetTvShowAsync(
tmdbId,
language: TmdbUtils.NormalizeLanguage(language),
includeImageLanguage: imageLanguages,
- extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings | TvShowMethods.EpisodeGroups,
+ extraMethods: extraMethods,
cancellationToken: cancellationToken).ConfigureAwait(false);
if (series != null)
@@ -242,7 +259,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
- var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, cancellationToken);
+ var group = await GetSeriesGroupAsync(tvShowId, displayOrder, language, imageLanguages, cancellationToken).ConfigureAwait(false);
if (group != null)
{
var season = group.Groups.Find(s => s.Order == seasonNumber);
@@ -358,7 +375,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
- .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), firstAirDateYear: year, cancellationToken: cancellationToken)
+ .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configuration.IncludeAdult, firstAirDateYear: year, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
@@ -386,7 +403,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
- .SearchPersonAsync(name, cancellationToken: cancellationToken)
+ .SearchPersonAsync(name, includeAdult: Plugin.Instance.Configuration.IncludeAdult, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
@@ -428,7 +445,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
await EnsureClientConfigAsync().ConfigureAwait(false);
var searchResults = await _tmDbClient
- .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), year: year, cancellationToken: cancellationToken)
+ .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), includeAdult: Plugin.Instance.Configuration.IncludeAdult, year: year, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (searchResults.Results.Count > 0)
@@ -469,33 +486,29 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
}
/// <summary>
- /// Gets the absolute URL of the poster.
+ /// Handles bad path checking and builds the absolute url.
/// </summary>
- /// <param name="posterPath">The relative URL of the poster.</param>
+ /// <param name="size">The image size to fetch.</param>
+ /// <param name="path">The relative URL of the image.</param>
/// <returns>The absolute URL.</returns>
- public string GetPosterUrl(string posterPath)
+ private string GetUrl(string size, string path)
{
- if (string.IsNullOrEmpty(posterPath))
+ if (string.IsNullOrEmpty(path))
{
return null;
}
- return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath).ToString();
+ return _tmDbClient.GetImageUrl(size, path, true).ToString();
}
/// <summary>
- /// Gets the absolute URL of the backdrop image.
+ /// Gets the absolute URL of the poster.
/// </summary>
- /// <param name="posterPath">The relative URL of the backdrop image.</param>
+ /// <param name="posterPath">The relative URL of the poster.</param>
/// <returns>The absolute URL.</returns>
- public string GetBackdropUrl(string posterPath)
+ public string GetPosterUrl(string posterPath)
{
- if (string.IsNullOrEmpty(posterPath))
- {
- return null;
- }
-
- return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.BackdropSizes[^1], posterPath).ToString();
+ return GetUrl(Plugin.Instance.Configuration.PosterSize, posterPath);
}
/// <summary>
@@ -505,32 +518,150 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
/// <returns>The absolute URL.</returns>
public string GetProfileUrl(string actorProfilePath)
{
- if (string.IsNullOrEmpty(actorProfilePath))
- {
- return null;
- }
+ return GetUrl(Plugin.Instance.Configuration.ProfileSize, actorProfilePath);
+ }
- return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath).ToString();
+ /// <summary>
+ /// Converts poster <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
+ /// </summary>
+ /// <param name="images">The input images.</param>
+ /// <param name="requestLanguage">The requested language.</param>
+ /// <param name="results">The collection to add the remote images into.</param>
+ public void ConvertPostersToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
+ {
+ ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.PosterSize, ImageType.Primary, requestLanguage, results);
}
/// <summary>
- /// Gets the absolute URL of the still image.
+ /// Converts backdrop <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
/// </summary>
- /// <param name="filePath">The relative URL of the still image.</param>
- /// <returns>The absolute URL.</returns>
- public string GetStillUrl(string filePath)
+ /// <param name="images">The input images.</param>
+ /// <param name="requestLanguage">The requested language.</param>
+ /// <param name="results">The collection to add the remote images into.</param>
+ public void ConvertBackdropsToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
+ {
+ ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.BackdropSize, ImageType.Backdrop, requestLanguage, results);
+ }
+
+ /// <summary>
+ /// Converts profile <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
+ /// </summary>
+ /// <param name="images">The input images.</param>
+ /// <param name="requestLanguage">The requested language.</param>
+ /// <param name="results">The collection to add the remote images into.</param>
+ public void ConvertProfilesToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
+ {
+ ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.ProfileSize, ImageType.Primary, requestLanguage, results);
+ }
+
+ /// <summary>
+ /// Converts still <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
+ /// </summary>
+ /// <param name="images">The input images.</param>
+ /// <param name="requestLanguage">The requested language.</param>
+ /// <param name="results">The collection to add the remote images into.</param>
+ public void ConvertStillsToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
+ {
+ ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.StillSize, ImageType.Primary, requestLanguage, results);
+ }
+
+ /// <summary>
+ /// Converts <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
+ /// </summary>
+ /// <param name="images">The input images.</param>
+ /// <param name="size">The size of the image to fetch.</param>
+ /// <param name="type">The type of the image.</param>
+ /// <param name="requestLanguage">The requested language.</param>
+ /// <param name="results">The collection to add the remote images into.</param>
+ private void ConvertToRemoteImageInfo(List<ImageData> images, string size, ImageType type, string requestLanguage, List<RemoteImageInfo> results)
{
- if (string.IsNullOrEmpty(filePath))
+ // sizes provided are for original resolution, don't store them when downloading scaled images
+ var scaleImage = !string.Equals(size, "original", StringComparison.OrdinalIgnoreCase);
+
+ for (var i = 0; i < images.Count; i++)
{
- return null;
+ var image = images[i];
+
+ results.Add(new RemoteImageInfo
+ {
+ Url = GetUrl(size, image.FilePath),
+ CommunityRating = image.VoteAverage,
+ VoteCount = image.VoteCount,
+ Width = scaleImage ? null : image.Width,
+ Height = scaleImage ? null : image.Height,
+ Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, requestLanguage),
+ ProviderName = TmdbUtils.ProviderName,
+ Type = type,
+ RatingType = RatingType.Score
+ });
}
+ }
- return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString();
+ private async Task EnsureClientConfigAsync()
+ {
+ if (!_tmDbClient.HasConfig)
+ {
+ var config = await _tmDbClient.GetConfigAsync().ConfigureAwait(false);
+ ValidatePreferences(config);
+ }
}
- private Task EnsureClientConfigAsync()
+ private static void ValidatePreferences(TMDbConfig config)
{
- return !_tmDbClient.HasConfig ? _tmDbClient.GetConfigAsync() : Task.CompletedTask;
+ var imageConfig = config.Images;
+
+ var pluginConfig = Plugin.Instance.Configuration;
+
+ if (!imageConfig.PosterSizes.Contains(pluginConfig.PosterSize))
+ {
+ pluginConfig.PosterSize = imageConfig.PosterSizes[^1];
+ }
+
+ if (!imageConfig.BackdropSizes.Contains(pluginConfig.BackdropSize))
+ {
+ pluginConfig.BackdropSize = imageConfig.BackdropSizes[^1];
+ }
+
+ if (!imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize))
+ {
+ pluginConfig.ProfileSize = imageConfig.ProfileSizes[^1];
+ }
+
+ if (!imageConfig.StillSizes.Contains(pluginConfig.StillSize))
+ {
+ pluginConfig.StillSize = imageConfig.StillSizes[^1];
+ }
+ }
+
+ /// <summary>
+ /// Gets the <see cref="TMDbClient"/> configuration.
+ /// </summary>
+ /// <returns>The configuration.</returns>
+ public async Task<TMDbConfig> GetClientConfiguration()
+ {
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ return _tmDbClient.Config;
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and - optionally - managed resources.
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _memoryCache?.Dispose();
+ _tmDbClient?.Dispose();
+ }
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index b713736a0..a3a78103e 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -1,5 +1,3 @@
-#nullable enable
-
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
@@ -31,11 +29,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public const string ApiKey = "4219e299c89411838049ab0dab19ebd5";
/// <summary>
- /// Maximum number of cast members to pull.
- /// </summary>
- public const int MaxCastMembers = 15;
-
- /// <summary>
/// The crew types to keep.
/// </summary>
public static readonly string[] WantedCrewTypes =
diff --git a/MediaBrowser.Providers/Properties/AssemblyInfo.cs b/MediaBrowser.Providers/Properties/AssemblyInfo.cs
index fe4749c79..bd301b5f0 100644
--- a/MediaBrowser.Providers/Properties/AssemblyInfo.cs
+++ b/MediaBrowser.Providers/Properties/AssemblyInfo.cs
@@ -15,7 +15,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
-[assembly: InternalsVisibleTo("Jellyfin.Common.Tests")]
+[assembly: InternalsVisibleTo("Jellyfin.Providers.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
diff --git a/MediaBrowser.Providers/Studios/StudioMetadataService.cs b/MediaBrowser.Providers/Studios/StudioMetadataService.cs
index 78042b40d..091b33ce0 100644
--- a/MediaBrowser.Providers/Studios/StudioMetadataService.cs
+++ b/MediaBrowser.Providers/Studios/StudioMetadataService.cs
@@ -17,7 +17,8 @@ namespace MediaBrowser.Providers.Studios
IServerConfigurationManager serverConfigurationManager,
ILogger<StudioMetadataService> logger,
IProviderManager providerManager,
- IFileSystem fileSystem, ILibraryManager libraryManager)
+ IFileSystem fileSystem,
+ ILibraryManager libraryManager)
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
}
diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
index 6aacaa15d..34019e582 100644
--- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
+++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System;
@@ -7,6 +9,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Jellyfin.Extensions;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
@@ -21,7 +24,6 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
-using static MediaBrowser.Model.IO.IODefaults;
namespace MediaBrowser.Providers.Subtitles
{
@@ -75,8 +77,7 @@ namespace MediaBrowser.Providers.Subtitles
var contentType = request.ContentType;
var providers = _subtitleProviders
- .Where(i => i.SupportedMediaTypes.Contains(contentType))
- .Where(i => !request.DisabledSubtitleFetchers.Contains(i.Name, StringComparer.OrdinalIgnoreCase))
+ .Where(i => i.SupportedMediaTypes.Contains(contentType) && !request.DisabledSubtitleFetchers.Contains(i.Name, StringComparison.OrdinalIgnoreCase))
.OrderBy(i =>
{
var index = request.SubtitleFetcherOrder.ToList().IndexOf(i.Name);
@@ -187,8 +188,8 @@ namespace MediaBrowser.Providers.Subtitles
{
var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
- using var stream = response.Stream;
- using var memoryStream = new MemoryStream();
+ await using var stream = response.Stream;
+ await using var memoryStream = new MemoryStream();
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
memoryStream.Position = 0;
@@ -236,7 +237,7 @@ namespace MediaBrowser.Providers.Subtitles
foreach (var savePath in savePaths)
{
- _logger.LogInformation("Saving subtitles to {0}", savePath);
+ _logger.LogInformation("Saving subtitles to {SavePath}", savePath);
_monitor.ReportFileSystemChangeBeginning(savePath);
@@ -244,15 +245,20 @@ namespace MediaBrowser.Providers.Subtitles
{
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
- // use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
- using var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, FileStreamBufferSize, true);
+ var fileOptions = AsyncFile.WriteOptions;
+ fileOptions.Mode = FileMode.CreateNew;
+ fileOptions.PreallocationSize = stream.Length;
+ using var fs = new FileStream(savePath, fileOptions);
await stream.CopyToAsync(fs).ConfigureAwait(false);
return;
}
catch (Exception ex)
{
+// Bug in analyzer -- https://github.com/dotnet/roslyn-analyzers/issues/5160
+#pragma warning disable CA1508
(exs ??= new List<Exception>()).Add(ex);
+#pragma warning restore CA1508
}
finally
{
@@ -269,7 +275,7 @@ namespace MediaBrowser.Providers.Subtitles
}
/// <inheritdoc />
- public Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video, string language, bool? isPerfectMatch, CancellationToken cancellationToken)
+ public Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video, string language, bool? isPerfectMatch, bool isAutomated, CancellationToken cancellationToken)
{
if (video.VideoType != VideoType.VideoFile)
{
@@ -303,7 +309,8 @@ namespace MediaBrowser.Providers.Subtitles
ProductionYear = video.ProductionYear,
ProviderIds = video.ProviderIds,
RuntimeTicks = video.RunTimeTicks,
- IsPerfectMatch = isPerfectMatch ?? false
+ IsPerfectMatch = isPerfectMatch ?? false,
+ IsAutomated = isAutomated
};
if (video is Episode episode)
@@ -370,15 +377,15 @@ namespace MediaBrowser.Providers.Subtitles
}
/// <inheritdoc />
- public SubtitleProviderInfo[] GetSupportedProviders(BaseItem video)
+ public SubtitleProviderInfo[] GetSupportedProviders(BaseItem item)
{
VideoContentType mediaType;
- if (video is Episode)
+ if (item is Episode)
{
mediaType = VideoContentType.Episode;
}
- else if (video is Movie)
+ else if (item is Movie)
{
mediaType = VideoContentType.Movie;
}
diff --git a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
index 170f1bdd8..08cb6ced9 100644
--- a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
+++ b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
@@ -25,46 +25,46 @@ namespace MediaBrowser.Providers.TV
}
/// <inheritdoc />
- protected override ItemUpdateType BeforeSaveInternal(Episode item, bool isFullRefresh, ItemUpdateType currentUpdateType)
+ protected override ItemUpdateType BeforeSaveInternal(Episode item, bool isFullRefresh, ItemUpdateType updateType)
{
- var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
+ var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
var seriesName = item.FindSeriesName();
if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal))
{
item.SeriesName = seriesName;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
var seasonName = item.FindSeasonName();
if (!string.Equals(item.SeasonName, seasonName, StringComparison.Ordinal))
{
item.SeasonName = seasonName;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
var seriesId = item.FindSeriesId();
if (!item.SeriesId.Equals(seriesId))
{
item.SeriesId = seriesId;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
var seasonId = item.FindSeasonId();
if (!item.SeasonId.Equals(seasonId))
{
item.SeasonId = seasonId;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey();
if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal))
{
item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
- return updateType;
+ return updatedType;
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
index 4e59f78bc..b173fc7a3 100644
--- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
@@ -31,9 +31,9 @@ namespace MediaBrowser.Providers.TV
protected override bool EnableUpdatingPremiereDateFromChildren => true;
/// <inheritdoc />
- protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType currentUpdateType)
+ protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType updateType)
{
- var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType);
+ var updatedType = base.BeforeSaveInternal(item, isFullRefresh, updateType);
if (item.IndexNumber.HasValue && item.IndexNumber.Value == 0)
{
@@ -42,7 +42,7 @@ namespace MediaBrowser.Providers.TV
if (!string.Equals(item.Name, seasonZeroDisplayName, StringComparison.OrdinalIgnoreCase))
{
item.Name = seasonZeroDisplayName;
- updateType = updateType | ItemUpdateType.MetadataEdit;
+ updatedType |= ItemUpdateType.MetadataEdit;
}
}
@@ -50,24 +50,24 @@ namespace MediaBrowser.Providers.TV
if (!string.Equals(item.SeriesName, seriesName, StringComparison.Ordinal))
{
item.SeriesName = seriesName;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
var seriesPresentationUniqueKey = item.FindSeriesPresentationUniqueKey();
if (!string.Equals(item.SeriesPresentationUniqueKey, seriesPresentationUniqueKey, StringComparison.Ordinal))
{
item.SeriesPresentationUniqueKey = seriesPresentationUniqueKey;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
var seriesId = item.FindSeriesId();
if (!item.SeriesId.Equals(seriesId))
{
item.SeriesId = seriesId;
- updateType |= ItemUpdateType.MetadataImport;
+ updatedType |= ItemUpdateType.MetadataImport;
}
- return updateType;
+ return updatedType;
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 967908197..770dc3e00 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -1,3 +1,5 @@
+#nullable disable
+
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -128,11 +130,12 @@ namespace MediaBrowser.Providers.TV
/// <returns>The async task.</returns>
private async Task FillInMissingSeasonsAsync(Series series, CancellationToken cancellationToken)
{
- var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode)
- .Cast<Episode>()
+ var seriesChildren = series.GetRecursiveChildren(i => i is Episode || i is Season);
+ var episodesInSeriesFolder = seriesChildren
+ .OfType<Episode>()
.Where(i => !i.IsInSeasonFolder);
- List<Season> seasons = series.Children.OfType<Season>().ToList();
+ List<Season> seasons = seriesChildren.OfType<Season>().ToList();
// Loop through the unique season numbers
foreach (var episode in episodesInSeriesFolder)
@@ -187,7 +190,7 @@ namespace MediaBrowser.Providers.TV
SeriesName = series.Name
};
- series.AddChild(season, cancellationToken);
+ series.AddChild(season);
await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
index 3cb18e424..087e4036a 100644
--- a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
+++ b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs
@@ -19,7 +19,7 @@ namespace MediaBrowser.Providers.TV
public ExternalIdMediaType? Type => null;
/// <inheritdoc />
- public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
+ public string? UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}";
/// <inheritdoc />
public bool Supports(IHasProviderIds item) => item is Series;