jellyfin/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs

434 lines
17 KiB
C#
Raw Normal View History

using System;
2013-09-15 19:33:23 +00:00
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
2013-02-21 01:33:05 +00:00
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
2016-10-27 07:58:33 +00:00
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
2019-01-29 22:09:48 +00:00
using TvDbSharper;
using TvDbSharper.Dto;
using Series = MediaBrowser.Controller.Entities.TV.Series;
2013-02-21 01:33:05 +00:00
namespace MediaBrowser.Providers.TV.TheTVDB
2013-02-21 01:33:05 +00:00
{
2016-05-26 17:18:58 +00:00
public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
2013-02-21 01:33:05 +00:00
{
internal static TvdbSeriesProvider Current { get; private set; }
2014-02-03 20:51:28 +00:00
private readonly IHttpClient _httpClient;
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly ILogger _logger;
2014-11-16 22:46:01 +00:00
private readonly ILibraryManager _libraryManager;
2016-10-27 07:58:33 +00:00
private readonly ILocalizationManager _localizationManager;
2019-01-30 22:28:43 +00:00
private readonly TvDbClientManager _tvDbClientManager;
2019-01-30 20:32:38 +00:00
public TvdbSeriesProvider(IHttpClient httpClient, ILogger logger, ILibraryManager libraryManager, ILocalizationManager localizationManager)
2013-02-25 00:13:45 +00:00
{
2014-02-03 20:51:28 +00:00
_httpClient = httpClient;
_logger = logger;
2014-11-16 22:46:01 +00:00
_libraryManager = libraryManager;
2016-10-27 07:58:33 +00:00
_localizationManager = localizationManager;
2013-03-04 05:43:06 +00:00
Current = this;
2019-01-30 22:28:43 +00:00
_tvDbClientManager = TvDbClientManager.Instance;
2013-03-04 05:43:06 +00:00
}
2016-01-25 04:12:47 +00:00
private string NormalizeLanguage(string language)
{
if (string.IsNullOrWhiteSpace(language))
{
return language;
}
// pt-br is just pt to tvdb
2019-01-27 11:03:43 +00:00
return language.Split('-')[0].ToLowerInvariant();
2016-01-25 04:12:47 +00:00
}
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
{
2015-10-21 05:09:50 +00:00
if (IsValidSeries(searchInfo.ProviderIds))
2014-03-02 15:42:21 +00:00
{
2015-10-21 05:09:50 +00:00
var metadata = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
2014-08-19 01:42:53 +00:00
2015-10-21 05:09:50 +00:00
if (metadata.HasMetadata)
2014-08-19 01:42:53 +00:00
{
2015-10-21 05:09:50 +00:00
return new List<RemoteSearchResult>
{
new RemoteSearchResult
{
Name = metadata.Item.Name,
PremiereDate = metadata.Item.PremiereDate,
ProductionYear = metadata.Item.ProductionYear,
ProviderIds = metadata.Item.ProviderIds,
SearchProviderName = Name
}
};
}
2014-08-19 01:42:53 +00:00
}
2015-10-21 05:09:50 +00:00
return await FindSeries(searchInfo.Name, searchInfo.Year, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
}
2014-02-07 03:10:13 +00:00
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo itemId, CancellationToken cancellationToken)
2013-02-21 01:33:05 +00:00
{
2019-01-30 20:23:23 +00:00
var result = new MetadataResult<Series>
{
QueriedById = true
};
2015-10-21 05:09:50 +00:00
if (!IsValidSeries(itemId.ProviderIds))
2013-02-21 01:33:05 +00:00
{
result.QueriedById = false;
2015-09-19 21:25:19 +00:00
await Identify(itemId).ConfigureAwait(false);
}
2015-10-21 05:09:50 +00:00
cancellationToken.ThrowIfCancellationRequested();
2015-10-21 05:09:50 +00:00
if (IsValidSeries(itemId.ProviderIds))
{
2014-02-03 20:51:28 +00:00
result.Item = new Series();
result.HasMetadata = true;
FetchSeriesData(result, itemId.MetadataLanguage, itemId.ProviderIds, cancellationToken);
2013-02-21 01:33:05 +00:00
}
2014-02-03 20:51:28 +00:00
return result;
2013-02-21 01:33:05 +00:00
}
2019-01-30 20:23:23 +00:00
private async Task FetchSeriesData(MetadataResult<Series> result, string metadataLanguage, Dictionary<string, string> seriesProviderIds, CancellationToken cancellationToken)
2013-02-21 01:33:05 +00:00
{
2019-01-30 22:28:43 +00:00
_tvDbClientManager.TvDbClient.AcceptedLanguage = NormalizeLanguage(metadataLanguage);
2015-06-29 01:10:45 +00:00
var series = result.Item;
2019-01-30 20:23:23 +00:00
if (seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out var tvdbId) && !string.IsNullOrEmpty(tvdbId))
2015-11-29 20:01:14 +00:00
{
2019-01-29 22:09:48 +00:00
series.SetProviderId(MetadataProviders.Tvdb, tvdbId);
2015-11-29 20:01:14 +00:00
}
2013-10-04 15:22:03 +00:00
2019-01-30 20:23:23 +00:00
if (seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out var imdbId) && !string.IsNullOrEmpty(imdbId))
2015-11-29 20:01:14 +00:00
{
2019-01-29 22:09:48 +00:00
series.SetProviderId(MetadataProviders.Imdb, imdbId);
2019-01-30 20:23:23 +00:00
tvdbId = await GetSeriesByRemoteId(imdbId, MetadataProviders.Imdb.ToString(), metadataLanguage, cancellationToken);
2015-11-29 20:01:14 +00:00
}
2019-01-30 20:23:23 +00:00
if (seriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out var zap2It) && !string.IsNullOrEmpty(zap2It))
2018-09-12 17:26:21 +00:00
{
2019-01-29 22:09:48 +00:00
series.SetProviderId(MetadataProviders.Zap2It, zap2It);
2019-01-30 20:23:23 +00:00
tvdbId = await GetSeriesByRemoteId(zap2It, MetadataProviders.Zap2It.ToString(), metadataLanguage, cancellationToken);
2018-09-12 17:26:21 +00:00
}
2019-01-30 20:23:23 +00:00
// TODO call this function elsewhere?
2019-01-30 22:28:43 +00:00
var seriesResult = await _tvDbClientManager.TvDbClient.Series.GetAsync(Convert.ToInt32(tvdbId), cancellationToken);
2019-01-30 20:23:23 +00:00
// TODO error handling
MapSeriesToResult(result, seriesResult.Data, cancellationToken);
2013-09-16 17:52:14 +00:00
2014-02-03 20:51:28 +00:00
cancellationToken.ThrowIfCancellationRequested();
2013-10-16 01:44:23 +00:00
2015-07-24 02:48:10 +00:00
result.ResetPeople();
2015-10-21 05:09:50 +00:00
2019-01-30 22:28:43 +00:00
var actorsResult = await _tvDbClientManager.TvDbClient.Series.GetActorsAsync(Convert.ToInt32(tvdbId), cancellationToken);
2019-01-30 20:23:23 +00:00
MapActorsToResult(result, actorsResult.Data);
}
2019-01-30 20:23:23 +00:00
private async Task<string> GetSeriesByRemoteId(string id, string idType, string language, CancellationToken cancellationToken)
2015-12-18 16:43:42 +00:00
{
2019-01-30 22:28:43 +00:00
_tvDbClientManager.TvDbClient.AcceptedLanguage = NormalizeLanguage(language);
2019-01-29 22:09:48 +00:00
TvDbResponse<SeriesSearchResult[]> result;
2018-09-12 17:26:21 +00:00
if (string.Equals(idType, MetadataProviders.Zap2It.ToString(), StringComparison.OrdinalIgnoreCase))
{
2019-01-30 22:28:43 +00:00
result = await _tvDbClientManager.TvDbClient.Search.SearchSeriesByZap2ItIdAsync(id, cancellationToken);
2018-09-12 17:26:21 +00:00
}
else
{
2019-01-30 22:28:43 +00:00
result = await _tvDbClientManager.TvDbClient.Search.SearchSeriesByImdbIdAsync(id, cancellationToken);
2016-10-27 07:58:33 +00:00
}
2019-01-30 20:23:23 +00:00
return result.Data.First().Id.ToString();
2015-12-18 16:43:42 +00:00
}
2015-10-21 05:09:50 +00:00
internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds)
2014-02-03 05:35:43 +00:00
{
2019-01-29 22:09:48 +00:00
return seriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), out _) ||
seriesProviderIds.TryGetValue(MetadataProviders.Imdb.ToString(), out _) ||
seriesProviderIds.TryGetValue(MetadataProviders.Zap2It.ToString(), out _);
2015-10-21 05:09:50 +00:00
}
2019-01-29 22:09:48 +00:00
// TODO caching
2015-10-21 05:09:50 +00:00
private bool IsCacheValid(string seriesDataPath, string preferredMetadataLanguage)
{
2019-01-30 20:32:38 +00:00
return true;
// try
// {
// var files = _fileSystem.GetFiles(seriesDataPath, new[] { ".xml" }, true, false)
// .ToList();
//
// var seriesXmlFilename = preferredMetadataLanguage + ".xml";
//
// const int cacheHours = 12;
//
// var seriesFile = files.FirstOrDefault(i => string.Equals(seriesXmlFilename, i.Name, StringComparison.OrdinalIgnoreCase));
// // No need to check age if automatic updates are enabled
// if (seriesFile == null || !seriesFile.Exists || (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(seriesFile)).TotalHours > cacheHours)
// {
// return false;
// }
//
// var actorsXml = files.FirstOrDefault(i => string.Equals("actors.xml", i.Name, StringComparison.OrdinalIgnoreCase));
// // No need to check age if automatic updates are enabled
// if (actorsXml == null || !actorsXml.Exists || (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(actorsXml)).TotalHours > cacheHours)
// {
// return false;
// }
//
// var bannersXml = files.FirstOrDefault(i => string.Equals("banners.xml", i.Name, StringComparison.OrdinalIgnoreCase));
// // No need to check age if automatic updates are enabled
// if (bannersXml == null || !bannersXml.Exists || (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(bannersXml)).TotalHours > cacheHours)
// {
// return false;
// }
// return true;
// }
// catch (FileNotFoundException)
// {
// return false;
// }
// catch (IOException)
// {
// return false;
// }
2014-02-03 05:35:43 +00:00
}
2014-02-03 20:51:28 +00:00
/// <summary>
/// Finds the series.
/// </summary>
/// <param name="name">The name.</param>
2015-10-21 05:09:50 +00:00
/// <param name="year">The year.</param>
2015-02-20 18:27:01 +00:00
/// <param name="language">The language.</param>
2014-02-03 20:51:28 +00:00
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{System.String}.</returns>
2015-10-21 05:09:50 +00:00
private async Task<IEnumerable<RemoteSearchResult>> FindSeries(string name, int? year, string language, CancellationToken cancellationToken)
2014-08-19 01:42:53 +00:00
{
2019-01-29 22:09:48 +00:00
var results = await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false);
2014-08-19 01:42:53 +00:00
if (results.Count == 0)
{
2014-11-16 22:46:01 +00:00
var parsedName = _libraryManager.ParseName(name);
var nameWithoutYear = parsedName.Name;
2014-08-19 01:42:53 +00:00
2015-02-20 18:27:01 +00:00
if (!string.IsNullOrWhiteSpace(nameWithoutYear) && !string.Equals(nameWithoutYear, name, StringComparison.OrdinalIgnoreCase))
2014-08-19 01:42:53 +00:00
{
2019-01-29 22:09:48 +00:00
results = await FindSeriesInternal(nameWithoutYear, language, cancellationToken).ConfigureAwait(false);
2014-08-19 01:42:53 +00:00
}
}
2015-10-21 05:09:50 +00:00
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;
});
2014-08-19 01:42:53 +00:00
}
private async Task<List<RemoteSearchResult>> FindSeriesInternal(string name, string language, CancellationToken cancellationToken)
2013-10-24 17:49:24 +00:00
{
2019-01-30 22:28:43 +00:00
_tvDbClientManager.TvDbClient.AcceptedLanguage = NormalizeLanguage(language);
var comparableName = GetComparableName(name);
2018-09-12 17:26:21 +00:00
var list = new List<Tuple<List<string>, RemoteSearchResult>>();
2019-01-30 22:28:43 +00:00
TvDbResponse<SeriesSearchResult[]> result = await _tvDbClientManager.TvDbClient.Search.SearchSeriesByNameAsync(comparableName, cancellationToken);
2018-09-12 17:26:21 +00:00
2019-01-29 22:09:48 +00:00
foreach (var seriesSearchResult in result.Data)
2013-10-24 17:49:24 +00:00
{
2019-01-29 22:09:48 +00:00
var tvdbTitles = new List<string>
2017-10-20 16:16:56 +00:00
{
2019-01-29 22:09:48 +00:00
GetComparableName(seriesSearchResult.SeriesName)
};
tvdbTitles.AddRange(seriesSearchResult.Aliases.Select(GetComparableName));
2017-10-20 16:16:56 +00:00
2019-01-29 22:09:48 +00:00
DateTime.TryParse(seriesSearchResult.FirstAired, out var firstAired);
var remoteSearchResult = new RemoteSearchResult
{
Name = tvdbTitles.FirstOrDefault(),
ProductionYear = firstAired.Year,
SearchProviderName = Name,
2019-01-30 20:23:23 +00:00
ImageUrl = TVUtils.BannerUrl + seriesSearchResult.Banner
2013-10-10 16:55:07 +00:00
2019-01-29 22:09:48 +00:00
};
2019-01-30 20:23:23 +00:00
// TODO requires another query, is it worth it?
2019-01-29 22:09:48 +00:00
// remoteSearchResult.SetProviderId(MetadataProviders.Imdb, seriesSearchResult.Id);
remoteSearchResult.SetProviderId(MetadataProviders.Tvdb, seriesSearchResult.Id.ToString());
list.Add(new Tuple<List<string>, RemoteSearchResult>(tvdbTitles, remoteSearchResult));
}
2014-02-03 20:51:28 +00:00
2018-09-12 17:26:21 +00:00
return list
.OrderBy(i => i.Item1.Contains(comparableName, StringComparer.OrdinalIgnoreCase) ? 0 : 1)
.ThenBy(i => list.IndexOf(i))
.Select(i => i.Item2)
.ToList();
}
2013-10-10 16:55:07 +00:00
/// <summary>
2014-02-03 20:51:28 +00:00
/// The remove
2013-10-10 16:55:07 +00:00
/// </summary>
2014-02-03 20:51:28 +00:00
const string remove = "\"'!`?";
/// <summary>
/// The spacers
/// </summary>
const string spacers = "/,.:;\\(){}[]+-_=*"; // (there are not actually two - in the they are different char codes)
/// <summary>
/// Gets the name of the comparable.
/// </summary>
/// <param name="name">The name.</param>
2013-10-10 16:55:07 +00:00
/// <returns>System.String.</returns>
2016-10-27 07:58:33 +00:00
private string GetComparableName(string name)
2013-10-10 16:55:07 +00:00
{
2019-01-27 11:03:43 +00:00
name = name.ToLowerInvariant();
2016-10-27 07:58:33 +00:00
name = _localizationManager.NormalizeFormKD(name);
2014-02-03 20:51:28 +00:00
var sb = new StringBuilder();
foreach (var c in name)
2013-10-10 16:55:07 +00:00
{
2014-02-03 20:51:28 +00:00
if ((int)c >= 0x2B0 && (int)c <= 0x0333)
2013-10-10 16:55:07 +00:00
{
2019-01-07 23:27:46 +00:00
// skip char modifier and diacritics
2014-02-03 20:51:28 +00:00
}
else if (remove.IndexOf(c) > -1)
{
// skip chars we are removing
}
else if (spacers.IndexOf(c) > -1)
{
sb.Append(" ");
}
else if (c == '&')
{
sb.Append(" and ");
}
else
{
sb.Append(c);
2013-10-10 16:55:07 +00:00
}
}
2014-02-03 20:51:28 +00:00
name = sb.ToString();
name = name.Replace(", the", "");
name = name.Replace("the ", " ");
name = name.Replace(" the ", " ");
2014-02-03 20:51:28 +00:00
string prevName;
do
{
2014-02-03 20:51:28 +00:00
prevName = name;
name = name.Replace(" ", " ");
} while (name.Length != prevName.Length);
2014-02-03 20:51:28 +00:00
return name.Trim();
}
2019-01-30 20:23:23 +00:00
private static void MapSeriesToResult(MetadataResult<Series> result, TvDbSharper.Dto.Series tvdbSeries, CancellationToken cancellationToken)
{
2019-01-29 22:09:48 +00:00
var episodeAirDates = new List<DateTime>();
2019-01-30 20:23:23 +00:00
Series series = result.Item;
series.SetProviderId(MetadataProviders.Tvdb, tvdbSeries.Id.ToString());
series.Name = tvdbSeries.SeriesName;
series.Overview = (tvdbSeries.Overview ?? string.Empty).Trim();
2019-01-29 22:09:48 +00:00
// TODO result.ResultLanguage = (seriesResponse.Data. ?? string.Empty).Trim();
2019-01-30 20:23:23 +00:00
series.AirDays = TVUtils.GetAirDays(tvdbSeries.AirsDayOfWeek);
series.AirTime = tvdbSeries.AirsTime;
2019-01-30 20:23:23 +00:00
series.CommunityRating = (float?)tvdbSeries.SiteRating;
series.SetProviderId(MetadataProviders.Imdb, tvdbSeries.ImdbId);
series.SetProviderId(MetadataProviders.Zap2It, tvdbSeries.Zap2itId);
if (Enum.TryParse(tvdbSeries.Status, true, out SeriesStatus seriesStatus))
2019-01-29 22:09:48 +00:00
{
series.Status = seriesStatus;
}
2019-01-30 20:23:23 +00:00
if (DateTime.TryParse(tvdbSeries.FirstAired, out var date))
{
2019-01-29 22:09:48 +00:00
date = date.ToUniversalTime();
2014-02-03 20:51:28 +00:00
2019-01-29 22:09:48 +00:00
series.PremiereDate = date;
series.ProductionYear = date.Year;
}
2016-10-27 07:58:33 +00:00
2019-01-30 20:23:23 +00:00
series.RunTimeTicks = TimeSpan.FromMinutes(Convert.ToDouble(tvdbSeries.Runtime)).Ticks;
foreach (var genre in tvdbSeries.Genre)
2019-01-29 22:09:48 +00:00
{
2019-01-30 20:23:23 +00:00
series.AddGenre(genre);
}
2019-01-30 20:23:23 +00:00
// TODO is network == studio?
series.AddStudio(tvdbSeries.Network);
2019-01-29 22:09:48 +00:00
2019-01-30 20:23:23 +00:00
// TODO is this necessary?
2019-01-29 22:09:48 +00:00
if (result.Item.Status.HasValue && result.Item.Status.Value == SeriesStatus.Ended && episodeAirDates.Count > 0)
{
result.Item.EndDate = episodeAirDates.Max();
}
2014-02-03 20:51:28 +00:00
}
2019-01-30 20:23:23 +00:00
private static void MapActorsToResult(MetadataResult<Series> result, IEnumerable<Actor> actors)
{
2019-01-30 20:23:23 +00:00
foreach (Actor actor in actors)
2014-02-03 20:51:28 +00:00
{
2019-01-30 20:23:23 +00:00
var personInfo = new PersonInfo
2018-09-12 17:26:21 +00:00
{
2019-01-30 20:23:23 +00:00
Type = PersonType.Actor,
Name = (actor.Name ?? string.Empty).Trim(),
Role = actor.Role,
ImageUrl = actor.Image,
SortOrder = actor.SortOrder
};
2014-02-03 20:51:28 +00:00
2019-01-30 20:23:23 +00:00
if (!string.IsNullOrWhiteSpace(personInfo.Name))
2013-02-21 01:33:05 +00:00
{
2019-01-30 20:23:23 +00:00
result.AddPerson(personInfo);
2013-02-21 01:33:05 +00:00
}
2014-02-03 20:51:28 +00:00
}
}
public string Name => "TheTVDB";
2014-02-15 16:36:09 +00:00
2015-09-19 21:25:19 +00:00
public async Task Identify(SeriesInfo info)
{
2015-10-21 05:09:50 +00:00
if (!string.IsNullOrWhiteSpace(info.GetProviderId(MetadataProviders.Tvdb)))
2014-08-19 01:42:53 +00:00
{
2015-10-21 05:09:50 +00:00
return;
}
2014-08-19 01:42:53 +00:00
2015-10-21 05:09:50 +00:00
var srch = await FindSeries(info.Name, info.Year, info.MetadataLanguage, CancellationToken.None).ConfigureAwait(false);
2014-08-19 01:42:53 +00:00
2015-10-21 05:09:50 +00:00
var entry = srch.FirstOrDefault();
if (entry != null)
{
var id = entry.GetProviderId(MetadataProviders.Tvdb);
info.SetProviderId(MetadataProviders.Tvdb, id);
2014-08-19 01:42:53 +00:00
}
}
public int Order => 0;
public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken)
{
2014-03-02 15:42:21 +00:00
return _httpClient.GetResponse(new HttpRequestOptions
{
CancellationToken = cancellationToken,
Url = url,
2017-01-05 18:09:12 +00:00
BufferContent = false
2014-03-02 15:42:21 +00:00
});
}
2013-02-21 01:33:05 +00:00
}
2016-04-21 05:13:33 +00:00
}