diff --git a/Jellyfin.Plugin.Tvdb/Providers/ExternalId/TvdbMovieExternalId.cs b/Jellyfin.Plugin.Tvdb/Providers/ExternalId/TvdbMovieExternalId.cs
new file mode 100644
index 0000000..8048230
--- /dev/null
+++ b/Jellyfin.Plugin.Tvdb/Providers/ExternalId/TvdbMovieExternalId.cs
@@ -0,0 +1,26 @@
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace Jellyfin.Plugin.Tvdb.Providers.ExternalId
+{
+ ///
+ public class TvdbMovieExternalId : IExternalId
+ {
+ ///
+ public string ProviderName => TvdbPlugin.ProviderId;
+
+ ///
+ public string Key => TvdbPlugin.ProviderName;
+
+ ///
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Movie;
+
+ ///
+ public string? UrlFormatString => null;
+
+ ///
+ public bool Supports(IHasProviderIds item) => item is Movie;
+ }
+}
diff --git a/Jellyfin.Plugin.Tvdb/Providers/ExternalId/TvdbMovieSlugExternalId.cs b/Jellyfin.Plugin.Tvdb/Providers/ExternalId/TvdbMovieSlugExternalId.cs
new file mode 100644
index 0000000..1c0a03c
--- /dev/null
+++ b/Jellyfin.Plugin.Tvdb/Providers/ExternalId/TvdbMovieSlugExternalId.cs
@@ -0,0 +1,26 @@
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+
+namespace Jellyfin.Plugin.Tvdb.Providers.ExternalId
+{
+ ///
+ public class TvdbMovieSlugExternalId : IExternalId
+ {
+ ///
+ public string ProviderName => TvdbPlugin.ProviderId;
+
+ ///
+ public string Key => TvdbPlugin.SlugProviderId;
+
+ ///
+ public ExternalIdMediaType? Type => ExternalIdMediaType.Movie;
+
+ ///
+ public string? UrlFormatString => TvdbUtils.TvdbBaseUrl + "movies/{0}";
+
+ ///
+ public bool Supports(IHasProviderIds item) => item is Movie;
+ }
+}
diff --git a/Jellyfin.Plugin.Tvdb/Providers/TvdbMovieImageProvider.cs b/Jellyfin.Plugin.Tvdb/Providers/TvdbMovieImageProvider.cs
new file mode 100644
index 0000000..b5b32fd
--- /dev/null
+++ b/Jellyfin.Plugin.Tvdb/Providers/TvdbMovieImageProvider.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using Microsoft.Extensions.Logging;
+using Tvdb.Sdk;
+
+namespace Jellyfin.Plugin.Tvdb.Providers
+{
+ ///
+ /// Tvdb movie image provider.
+ ///
+ public class TvdbMovieImageProvider : IRemoteImageProvider
+ {
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly ILogger _logger;
+ private readonly TvdbClientManager _tvdbClientManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of .
+ public TvdbMovieImageProvider(
+ IHttpClientFactory httpClientFactory,
+ ILogger logger,
+ TvdbClientManager tvdbClientManager)
+ {
+ _httpClientFactory = httpClientFactory;
+ _logger = logger;
+ _tvdbClientManager = tvdbClientManager;
+ }
+
+ ///
+ public string Name => TvdbPlugin.ProviderName;
+
+ ///
+ public bool Supports(BaseItem item)
+ {
+ return item is Movie;
+ }
+
+ ///
+ public IEnumerable GetSupportedImages(BaseItem item)
+ {
+ yield return ImageType.Primary;
+ yield return ImageType.Banner;
+ yield return ImageType.Backdrop;
+ yield return ImageType.Logo;
+ yield return ImageType.Art;
+ }
+
+ ///
+ public async Task> GetImages(BaseItem item, CancellationToken cancellationToken)
+ {
+ if (!item.IsSupported())
+ {
+ return Enumerable.Empty();
+ }
+
+ var languages = await _tvdbClientManager.GetLanguagesAsync(cancellationToken)
+ .ConfigureAwait(false);
+ var languageLookup = languages
+ .ToDictionary(l => l.Id, StringComparer.OrdinalIgnoreCase);
+
+ var artworkTypes = await _tvdbClientManager.GetArtworkTypeAsync(cancellationToken)
+ .ConfigureAwait(false);
+ var movieArtworkTypeLookup = artworkTypes
+ .Where(t => string.Equals(t.RecordType, "movie", StringComparison.OrdinalIgnoreCase))
+ .Where(t => t.Id.HasValue)
+ .ToDictionary(t => t.Id!.Value);
+
+ var movieTvdbId = item.GetTvdbId();
+ var movieArtworks = await GetMovieArtworks(movieTvdbId, cancellationToken)
+ .ConfigureAwait(false);
+
+ var remoteImages = new List();
+ foreach (var artwork in movieArtworks)
+ {
+ var artworkType = artwork.Type is null ? null : movieArtworkTypeLookup.GetValueOrDefault(artwork.Type!.Value);
+ var imageType = artworkType.GetImageType();
+ var artworkLanguage = artwork.Language is null ? null : languageLookup.GetValueOrDefault(artwork.Language);
+
+ // only add if valid RemoteImageInfo
+ remoteImages.AddIfNotNull(artwork.CreateImageInfo(Name, imageType, artworkLanguage));
+ }
+
+ return remoteImages.OrderByLanguageDescending(item.GetPreferredMetadataLanguage());
+ }
+
+ private async Task> GetMovieArtworks(int movieTvdbId, CancellationToken cancellationToken)
+ {
+ var movieInfo = await _tvdbClientManager.GetMovieExtendedByIdAsync(movieTvdbId, cancellationToken)
+ .ConfigureAwait(false);
+ return movieInfo?.Artworks ?? Array.Empty();
+ }
+
+ ///
+ public Task GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(new Uri(url), cancellationToken);
+ }
+ }
+}
diff --git a/Jellyfin.Plugin.Tvdb/Providers/TvdbMovieProvider.cs b/Jellyfin.Plugin.Tvdb/Providers/TvdbMovieProvider.cs
new file mode 100644
index 0000000..21bbdf8
--- /dev/null
+++ b/Jellyfin.Plugin.Tvdb/Providers/TvdbMovieProvider.cs
@@ -0,0 +1,471 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Providers;
+using Microsoft.Extensions.Logging;
+using Tvdb.Sdk;
+
+namespace Jellyfin.Plugin.Tvdb.Providers
+{
+ ///
+ /// The TVDB movie provider.
+ ///
+ public class TvdbMovieProvider : IRemoteMetadataProvider
+ {
+ private const int MaxSearchResults = 10;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly ILogger _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly TvdbClientManager _tvdbClientManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of .
+ /// Instance of .
+ /// Instance of .
+ /// Instance of .
+ public TvdbMovieProvider(
+ IHttpClientFactory httpClientFactory,
+ ILogger logger,
+ ILibraryManager libraryManager,
+ TvdbClientManager tvdbClientManager)
+ {
+ _httpClientFactory = httpClientFactory;
+ _logger = logger;
+ _libraryManager = libraryManager;
+ _tvdbClientManager = tvdbClientManager;
+ }
+
+ ///
+ public string Name => TvdbPlugin.ProviderName;
+
+ ///
+ public async Task> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
+ {
+ if (searchInfo.IsSupported())
+ {
+ return await FetchMovieSearchResult(searchInfo, cancellationToken).ConfigureAwait(false);
+ }
+
+ return await FindMovie(searchInfo.Name, searchInfo.Year, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
+ {
+ var result = new MetadataResult
+ {
+ QueriedById = true,
+ };
+
+ if (!info.IsSupported())
+ {
+ result.QueriedById = false;
+ await Identify(info).ConfigureAwait(false);
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (info.IsSupported())
+ {
+ result.Item = new Movie();
+ result.HasMetadata = true;
+
+ await FetchMovieMetadata(result, info, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ return result;
+ }
+
+ private async Task> FetchMovieSearchResult(MovieInfo movieInfo, CancellationToken cancellationToken)
+ {
+ async Task TryGetTvdbIdWithRemoteId(MetadataProvider metadataProvider)
+ {
+ var id = movieInfo.GetProviderId(metadataProvider);
+ if (string.IsNullOrEmpty(id))
+ {
+ return null;
+ }
+
+ return await GetMovieByRemoteId(
+ id,
+ cancellationToken).ConfigureAwait(false);
+ }
+
+ int? tvdbId;
+ if (movieInfo.HasTvdbId())
+ {
+ tvdbId = movieInfo.GetTvdbId();
+ }
+ else
+ {
+ var tvdbIdTxt = await TryGetTvdbIdWithRemoteId(MetadataProvider.Imdb).ConfigureAwait(false)
+ ?? await TryGetTvdbIdWithRemoteId(MetadataProvider.Zap2It).ConfigureAwait(false)
+ ?? await TryGetTvdbIdWithRemoteId(MetadataProvider.Tmdb).ConfigureAwait(false);
+
+ tvdbId = tvdbIdTxt is null ? null : Convert.ToInt32(tvdbIdTxt, CultureInfo.InvariantCulture);
+ }
+
+ if (!tvdbId.HasValue)
+ {
+ _logger.LogWarning("No valid tvdb id found for movie {TvdbId}:{MovieName}", tvdbId, movieInfo.Name);
+ return Array.Empty();
+ }
+
+ try
+ {
+ var movieResult =
+ await _tvdbClientManager
+ .GetMovieExtendedByIdAsync(tvdbId.Value, cancellationToken)
+ .ConfigureAwait(false);
+ return new[] { MapMovieToRemoteSearchResult(movieResult, movieInfo.MetadataLanguage) };
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Failed to retrieve movie with id {TvdbId}:{MovieName}", tvdbId, movieInfo.Name);
+ return Array.Empty();
+ }
+ }
+
+ private RemoteSearchResult MapMovieToRemoteSearchResult(MovieExtendedRecord movie, string language)
+ {
+ var remoteResult = new RemoteSearchResult
+ {
+ Name = movie.Translations.GetTranslatedNamedOrDefault(language) ?? TvdbUtils.ReturnOriginalLanguageOrDefault(movie.Name),
+ Overview = movie.Translations.GetTranslatedOverviewOrDefault(language)?.Trim(),
+ SearchProviderName = Name,
+ ImageUrl = movie.Image
+ };
+
+ if (DateTime.TryParse(movie.First_release.Date, out var date))
+ {
+ // Dates from tvdb are either EST or capital of primary airing country.
+ remoteResult.PremiereDate = date;
+ remoteResult.ProductionYear = date.Year;
+ }
+
+ var imdbID = movie.RemoteIds?.FirstOrDefault(x => string.Equals(x.SourceName, "IMDB", StringComparison.OrdinalIgnoreCase))?.Id;
+ remoteResult.SetProviderIdIfHasValue(MetadataProvider.Imdb, imdbID);
+ remoteResult.SetTvdbId(movie.Id);
+
+ return remoteResult;
+ }
+
+ private async Task GetMovieByRemoteId(string remoteId, CancellationToken cancellationToken)
+ {
+ IReadOnlyList resultData;
+ try
+ {
+ resultData = await _tvdbClientManager.GetMovieByRemoteIdAsync(remoteId, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ catch (SearchException ex) when (ex.InnerException is JsonException)
+ {
+ _logger.LogError(ex, "Failed to retrieve movie with {RemoteId}", remoteId);
+ return null;
+ }
+
+ if (resultData is null || resultData.Count == 0 || resultData[0]?.Movie?.Id is null)
+ {
+ _logger.LogWarning("TvdbSearch: No movie found for remote id: {RemoteId}", remoteId);
+ return null;
+ }
+
+ return resultData[0].Movie.Id?.ToString(CultureInfo.InvariantCulture);
+ }
+
+ private async Task> FindMovie(string name, int? year, string language, CancellationToken cancellationToken)
+ {
+ _logger.LogDebug("TvdbSearch: Finding id for item: {Name} ({Year})", name, year);
+ var results = await FindMovieInternal(name, language, cancellationToken).ConfigureAwait(false);
+
+ return results.Where(i =>
+ {
+ if (year.HasValue && i.ProductionYear.HasValue)
+ {
+ // Allow one year tolerance
+ return Math.Abs(year.Value - i.ProductionYear.Value) <= 1;
+ }
+
+ return true;
+ });
+ }
+
+ private async Task> FindMovieInternal(string name, string language, CancellationToken cancellationToken)
+ {
+ var parsedName = _libraryManager.ParseName(name);
+ var comparableName = TvdbUtils.GetComparableName(parsedName.Name);
+
+ var list = new List, RemoteSearchResult>>();
+ IReadOnlyList result;
+ try
+ {
+ result = await _tvdbClientManager.GetMovieByNameAsync(comparableName, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "No movie results found for {Name}", comparableName);
+ return new List();
+ }
+
+ foreach (var movieSearchResult in result)
+ {
+ var tvdbTitles = new List
+ {
+ movieSearchResult.Translations.GetTranslatedNamedOrDefault(language) ?? movieSearchResult.Name
+ };
+ if (movieSearchResult.Aliases is not null)
+ {
+ tvdbTitles.AddRange(movieSearchResult.Aliases);
+ }
+
+ DateTime? firstAired = null;
+ if (DateTime.TryParse(movieSearchResult.First_air_time, out var parsedFirstAired))
+ {
+ firstAired = parsedFirstAired;
+ }
+
+ var remoteSearchResult = new RemoteSearchResult
+ {
+ Name = tvdbTitles.FirstOrDefault(),
+ ProductionYear = firstAired?.Year,
+ SearchProviderName = Name
+ };
+
+ if (!string.IsNullOrEmpty(movieSearchResult.Image_url))
+ {
+ remoteSearchResult.ImageUrl = movieSearchResult.Image_url;
+ }
+
+ try
+ {
+ var movieResult =
+ await _tvdbClientManager.GetMovieExtendedByIdAsync(Convert.ToInt32(movieSearchResult.Tvdb_id, CultureInfo.InvariantCulture), cancellationToken)
+ .ConfigureAwait(false);
+
+ var imdbId = movieResult.RemoteIds?.FirstOrDefault(x => string.Equals(x.SourceName, "IMDB", StringComparison.OrdinalIgnoreCase))?.Id.ToString();
+ remoteSearchResult.SetProviderIdIfHasValue(MetadataProvider.Imdb, imdbId);
+
+ var zap2ItId = movieResult.RemoteIds?.FirstOrDefault(x => string.Equals(x.SourceName, "Zap2It", StringComparison.OrdinalIgnoreCase))?.Id.ToString();
+ remoteSearchResult.SetProviderIdIfHasValue(MetadataProvider.Zap2It, zap2ItId);
+
+ var tmdbId = movieResult.RemoteIds?.FirstOrDefault(x => string.Equals(x.SourceName, "TheMovieDB.com", StringComparison.OrdinalIgnoreCase))?.Id.ToString();
+
+ // Sometimes, tvdb will return tmdbid as {tmdbid}-{title} like in the tmdb url. Grab the tmdbid only.
+ var tmdbIdLeft = StringExtensions.LeftPart(tmdbId, '-').ToString();
+ remoteSearchResult.SetProviderIdIfHasValue(MetadataProvider.Tmdb, tmdbIdLeft);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Unable to retrieve movie with id {TvdbId}:{MovieName}", movieSearchResult.Tvdb_id, movieSearchResult.Name);
+ }
+
+ remoteSearchResult.SetTvdbId(movieSearchResult.Tvdb_id);
+ list.Add(new Tuple, RemoteSearchResult>(tvdbTitles, remoteSearchResult));
+ }
+
+ return list
+ .OrderBy(i => i.Item1.Contains(name, StringComparer.OrdinalIgnoreCase) ? 0 : 1)
+ .ThenBy(i => i.Item1.Any(title => title.Contains(parsedName.Name, StringComparison.OrdinalIgnoreCase)) ? 0 : 1)
+ .ThenBy(i => i.Item2.ProductionYear.HasValue && i.Item2.ProductionYear.Equals(parsedName.Year) ? 0 : 1)
+ .ThenBy(i => i.Item1.Any(title => title.Contains(comparableName, StringComparison.OrdinalIgnoreCase)) ? 0 : 1)
+ .ThenBy(i => list.IndexOf(i))
+ .Select(i => i.Item2)
+ .Take(MaxSearchResults) // TVDB returns a lot of unrelated results
+ .ToList();
+ }
+
+ private async Task Identify(MovieInfo info)
+ {
+ if (info.HasTvdbId())
+ {
+ return;
+ }
+
+ var remoteSearchResults = await FindMovie(info.Name, info.Year, info.MetadataLanguage, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ var entry = remoteSearchResults.FirstOrDefault();
+ if (entry.HasTvdbId(out var tvdbId))
+ {
+ info.SetTvdbId(tvdbId);
+ }
+ }
+
+ private async Task FetchMovieMetadata(
+ MetadataResult result,
+ MovieInfo movieInfo,
+ CancellationToken cancellationToken)
+ {
+ var movieMetadata = result.Item;
+ async Task TryGetTvdbIdWithRemoteId(string id)
+ {
+ return await GetMovieByRemoteId(
+ id,
+ cancellationToken).ConfigureAwait(false);
+ }
+
+ if (movieInfo.HasTvdbId(out var tvdbIdTxt))
+ {
+ movieMetadata.SetTvdbId(tvdbIdTxt);
+ }
+
+ if (movieInfo.HasProviderId(MetadataProvider.Imdb, out var imdbId))
+ {
+ movieMetadata.SetProviderId(MetadataProvider.Imdb, imdbId!);
+ tvdbIdTxt ??= await TryGetTvdbIdWithRemoteId(imdbId!).ConfigureAwait(false);
+ }
+
+ if (movieInfo.HasProviderId(MetadataProvider.Zap2It, out var zap2It))
+ {
+ movieMetadata.SetProviderId(MetadataProvider.Zap2It, zap2It!);
+ tvdbIdTxt ??= await TryGetTvdbIdWithRemoteId(zap2It!).ConfigureAwait(false);
+ }
+
+ if (movieInfo.HasProviderId(MetadataProvider.Tmdb, out var tmdbId))
+ {
+ movieMetadata.SetProviderId(MetadataProvider.Tmdb, tmdbId!);
+ tvdbIdTxt ??= await TryGetTvdbIdWithRemoteId(tmdbId!).ConfigureAwait(false);
+ }
+
+ if (string.IsNullOrWhiteSpace(tvdbIdTxt))
+ {
+ _logger.LogWarning("No valid tvdb id found for movie {TvdbId}:{MovieName}", tvdbIdTxt, movieInfo.Name);
+ return;
+ }
+
+ var tvdbId = Convert.ToInt32(tvdbIdTxt, CultureInfo.InvariantCulture);
+ try
+ {
+ var movieResult =
+ await _tvdbClientManager
+ .GetMovieExtendedByIdAsync(tvdbId, cancellationToken)
+ .ConfigureAwait(false);
+ MapMovieToResult(result, movieResult, movieInfo);
+
+ result.ResetPeople();
+
+ List people = new List();
+ if (movieResult.Characters is not null)
+ {
+ foreach (Character character in movieResult.Characters)
+ {
+ people.Add(character);
+ }
+
+ MapActorsToResult(result, people);
+ }
+ else
+ {
+ _logger.LogError("Failed to retrieve actors for movie {TvdbId}:{MovieName}", tvdbId, movieInfo.Name);
+ }
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Failed to retrieve movie with id {TvdbId}:{MovieName}", tvdbId, movieInfo.Name);
+ return;
+ }
+ }
+
+ private void MapMovieToResult(MetadataResult result, MovieExtendedRecord tvdbMovie, MovieInfo info)
+ {
+ Movie movie = result.Item;
+ movie.SetTvdbId(tvdbMovie.Id);
+ // Tvdb uses 3 letter code for language (prob ISO 639-2)
+ // Reverts to OriginalName if no translation is found
+ movie.Name = tvdbMovie.Translations.GetTranslatedNamedOrDefault(info.MetadataLanguage) ?? TvdbUtils.ReturnOriginalLanguageOrDefault(tvdbMovie.Name);
+ movie.Overview = tvdbMovie.Translations.GetTranslatedOverviewOrDefault(info.MetadataLanguage);
+ movie.OriginalTitle = tvdbMovie.Name;
+ result.ResultLanguage = info.MetadataLanguage;
+ // Attempts to default to USA if not found
+ movie.OfficialRating = tvdbMovie.ContentRatings.FirstOrDefault(x => string.Equals(x.Country, TvdbCultureInfo.GetCountryInfo(info.MetadataCountryCode)?.ThreeLetterISORegionName, StringComparison.OrdinalIgnoreCase))?.Name ?? tvdbMovie.ContentRatings.FirstOrDefault(x => string.Equals(x.Country, "usa", StringComparison.OrdinalIgnoreCase))?.Name;
+
+ var collectionIds = tvdbMovie.Lists.Where(x => x.IsOfficial is true)
+ .Select(x => x.Id?.ToString(CultureInfo.InvariantCulture))
+ .Aggregate(new StringBuilder(), (sb, id) => sb.Append(id).Append(';'));
+ movie.SetProviderIdIfHasValue(TvdbPlugin.CollectionProviderId, collectionIds.ToString());
+
+ movie.SetProviderIdIfHasValue(TvdbPlugin.SlugProviderId, tvdbMovie.Slug);
+
+ var imdbId = tvdbMovie.RemoteIds?.FirstOrDefault(x => string.Equals(x.SourceName, "IMDB", StringComparison.OrdinalIgnoreCase))?.Id.ToString();
+ movie.SetProviderIdIfHasValue(MetadataProvider.Imdb, imdbId);
+
+ var zap2ItId = tvdbMovie.RemoteIds?.FirstOrDefault(x => string.Equals(x.SourceName, "Zap2It", StringComparison.OrdinalIgnoreCase))?.Id.ToString();
+ movie.SetProviderIdIfHasValue(MetadataProvider.Zap2It, zap2ItId);
+
+ var tmdbId = tvdbMovie.RemoteIds?.FirstOrDefault(x => string.Equals(x.SourceName, "TheMovieDB.com", StringComparison.OrdinalIgnoreCase))?.Id.ToString();
+ movie.SetProviderIdIfHasValue(MetadataProvider.Tmdb, tmdbId);
+
+ if (DateTime.TryParse(tvdbMovie.First_release.Date, out var date))
+ {
+ // dates from tvdb are UTC but without offset or Z
+ movie.PremiereDate = date;
+ movie.ProductionYear = date.Year;
+ }
+
+ if (tvdbMovie.Runtime is not null)
+ {
+ movie.RunTimeTicks = TimeSpan.FromMinutes(tvdbMovie.Runtime.Value).Ticks;
+ }
+
+ foreach (var genre in tvdbMovie.Genres)
+ {
+ movie.AddGenre(genre.Name);
+ }
+
+ if (tvdbMovie.Companies.Studio is not null)
+ {
+ foreach (var studio in tvdbMovie.Companies.Studio)
+ {
+ movie.AddStudio(studio.Name);
+ }
+ }
+ }
+
+ private static void MapActorsToResult(MetadataResult result, IEnumerable actors)
+ {
+ foreach (Character actor in actors)
+ {
+ var personInfo = new PersonInfo
+ {
+ Type = PersonKind.Actor,
+ Name = (actor.PersonName ?? string.Empty).Trim(),
+ Role = actor.Name
+ };
+
+ if (!string.IsNullOrEmpty(actor.PersonImgURL))
+ {
+ personInfo.ImageUrl = actor.PersonImgURL;
+ }
+
+ if (!string.IsNullOrWhiteSpace(personInfo.Name))
+ {
+ result.AddPerson(personInfo);
+ }
+ }
+ }
+
+ ///
+ public Task GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(new Uri(url), cancellationToken);
+ }
+ }
+}
diff --git a/Jellyfin.Plugin.Tvdb/Providers/TvdbSeriesProvider.cs b/Jellyfin.Plugin.Tvdb/Providers/TvdbSeriesProvider.cs
index 5bb3195..d42ca04 100644
--- a/Jellyfin.Plugin.Tvdb/Providers/TvdbSeriesProvider.cs
+++ b/Jellyfin.Plugin.Tvdb/Providers/TvdbSeriesProvider.cs
@@ -5,7 +5,6 @@
using System.Net.Http;
using System.Text;
using System.Text.Json;
-using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
@@ -300,7 +299,7 @@ private async Task> FindSeries(string name, int?
private async Task> FindSeriesInternal(string name, string language, CancellationToken cancellationToken)
{
var parsedName = _libraryManager.ParseName(name);
- var comparableName = GetComparableName(parsedName.Name);
+ var comparableName = TvdbUtils.GetComparableName(parsedName.Name);
var list = new List, RemoteSearchResult>>();
IReadOnlyList result;
@@ -382,24 +381,6 @@ await _tvdbClientManager.GetSeriesExtendedByIdAsync(Convert.ToInt32(seriesSearch
.ToList();
}
- ///
- /// Gets the name of the comparable.
- ///
- /// The name.
- /// System.String.
- private static string GetComparableName(string name)
- {
- name = name.ToLowerInvariant();
- name = name.Normalize(NormalizationForm.FormC);
- name = name.Replace(", the", string.Empty, StringComparison.OrdinalIgnoreCase)
- .Replace("the ", " ", StringComparison.OrdinalIgnoreCase)
- .Replace(" the ", " ", StringComparison.OrdinalIgnoreCase);
- name = name.Replace("&", " and ", StringComparison.OrdinalIgnoreCase);
- name = Regex.Replace(name, @"[\p{Lm}\p{Mn}]", string.Empty); // Remove diacritics, etc
- name = Regex.Replace(name, @"[\W\p{Pc}]+", " "); // Replace sequences of non-word characters and _ with " "
- return name.Trim();
- }
-
private static void MapActorsToResult(MetadataResult result, IEnumerable actors)
{
foreach (Character actor in actors)
diff --git a/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs b/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs
index d5aa322..b7faa57 100644
--- a/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs
+++ b/Jellyfin.Plugin.Tvdb/TvdbClientManager.cs
@@ -100,6 +100,81 @@ bool IsTokenInvalid() =>
|| _tokenUpdatedAt < DateTime.UtcNow.Subtract(TimeSpan.FromDays(25));
}
+ ///
+ /// Gets movie by name.
+ ///
+ /// Movie Name.
+ /// Cancellation token.
+ /// The movie search result.
+ public async Task> GetMovieByNameAsync(
+ string name,
+ CancellationToken cancellationToken)
+ {
+ var key = $"TvdbMovieSearch_{name}";
+ if (_memoryCache.TryGetValue(key, out IReadOnlyList? movies)
+ && movies is not null)
+ {
+ return movies;
+ }
+
+ var searchClient = _serviceProvider.GetRequiredService();
+ await LoginAsync().ConfigureAwait(false);
+ var searchResult = await searchClient.GetSearchResultsAsync(query: name, type: "movie", limit: 5, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+ _memoryCache.Set(key, searchResult.Data, TimeSpan.FromHours(CacheDurationInHours));
+ return searchResult.Data;
+ }
+
+ ///
+ /// Get movie by remoteId.
+ ///
+ /// Remote Id.
+ /// Cancellation token.
+ /// The movie search result.
+ public async Task> GetMovieByRemoteIdAsync(
+ string remoteId,
+ CancellationToken cancellationToken)
+ {
+ var key = $"TvdbMovieRemoteId_{remoteId}";
+ if (_memoryCache.TryGetValue(key, out IReadOnlyList? movies)
+ && movies is not null)
+ {
+ return movies;
+ }
+
+ var searchClient = _serviceProvider.GetRequiredService();
+ await LoginAsync().ConfigureAwait(false);
+ var searchResult = await searchClient.GetSearchResultsByRemoteIdAsync(remoteId: remoteId, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+ _memoryCache.Set(key, searchResult.Data, TimeSpan.FromHours(CacheDurationInHours));
+ return searchResult.Data;
+ }
+
+ ///
+ /// Get movie by id.
+ ///
+ /// The movie tvdb id.
+ /// Cancellation token.
+ /// The movie response.
+ public async Task GetMovieExtendedByIdAsync(
+ int tvdbId,
+ CancellationToken cancellationToken)
+ {
+ var key = $"TvdbMovie_{tvdbId.ToString(CultureInfo.InvariantCulture)}";
+ if (_memoryCache.TryGetValue(key, out MovieExtendedRecord? movie)
+ && movie is not null)
+ {
+ return movie;
+ }
+
+ var movieClient = _serviceProvider.GetRequiredService();
+ await LoginAsync().ConfigureAwait(false);
+ var movieResult = await movieClient.GetMovieExtendedAsync(id: tvdbId, meta: Meta2.Translations, @short: false, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+ _memoryCache.Set(key, movieResult.Data, TimeSpan.FromHours(CacheDurationInHours));
+ return movieResult.Data;
+ }
+
///
/// Get series by name.
///
@@ -621,6 +696,7 @@ private ServiceProvider ConfigureService(IApplicationHost applicationHost)
services.AddTransient(_ => new Artwork_TypesClient(_sdkClientSettings, _httpClientFactory.CreateClient(TvdbHttpClient)));
services.AddTransient(_ => new LanguagesClient(_sdkClientSettings, _httpClientFactory.CreateClient(TvdbHttpClient)));
services.AddTransient(_ => new UpdatesClient(_sdkClientSettings, _httpClientFactory.CreateClient(TvdbHttpClient)));
+ services.AddTransient(_ => new MoviesClient(_sdkClientSettings, _httpClientFactory.CreateClient(TvdbHttpClient)));
return services.BuildServiceProvider();
}
diff --git a/Jellyfin.Plugin.Tvdb/TvdbPlugin.cs b/Jellyfin.Plugin.Tvdb/TvdbPlugin.cs
index 064a0ed..bda780b 100644
--- a/Jellyfin.Plugin.Tvdb/TvdbPlugin.cs
+++ b/Jellyfin.Plugin.Tvdb/TvdbPlugin.cs
@@ -28,6 +28,11 @@ public class TvdbPlugin : BasePlugin, IHasWebPages
///
public const string CollectionProviderId = "TvdbCollection";
+ ///
+ /// Gets the slug provider id.
+ ///
+ public const string SlugProviderId = "TvdbSlug";
+
///
/// Initializes a new instance of the class.
///
diff --git a/Jellyfin.Plugin.Tvdb/TvdbUtils.cs b/Jellyfin.Plugin.Tvdb/TvdbUtils.cs
index 22af158..21ac92e 100644
--- a/Jellyfin.Plugin.Tvdb/TvdbUtils.cs
+++ b/Jellyfin.Plugin.Tvdb/TvdbUtils.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
-
+using System.Text;
+using System.Text.RegularExpressions;
using Tvdb.Sdk;
namespace Jellyfin.Plugin.Tvdb
@@ -69,5 +70,23 @@ public static IEnumerable GetAirDays(SeriesAirsDays seriesAirsDays)
{
return FallbackToOriginalLanguage ? text : null;
}
+
+ ///
+ /// Gets the name of the comparable.
+ ///
+ /// The name.
+ /// System.String.
+ public static string GetComparableName(string name)
+ {
+ name = name.ToLowerInvariant();
+ name = name.Normalize(NormalizationForm.FormC);
+ name = name.Replace(", the", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Replace("the ", " ", StringComparison.OrdinalIgnoreCase)
+ .Replace(" the ", " ", StringComparison.OrdinalIgnoreCase);
+ name = name.Replace("&", " and ", StringComparison.OrdinalIgnoreCase);
+ name = Regex.Replace(name, @"[\p{Lm}\p{Mn}]", string.Empty); // Remove diacritics, etc
+ name = Regex.Replace(name, @"[\W\p{Pc}]+", " "); // Replace sequences of non-word characters and _ with " "
+ return name.Trim();
+ }
}
}