154 lines
6.3 KiB
C#
154 lines
6.3 KiB
C#
using System.Globalization;
|
|
using System.Text.Json;
|
|
using Microsoft.Extensions.Options;
|
|
using TeleWave.Application.Common.Interfaces;
|
|
using TeleWave.Application.Metadata;
|
|
using TeleWave.Domain.Library;
|
|
using static TeleWave.Infrastructure.Metadata.MetadataJson;
|
|
|
|
namespace TeleWave.Infrastructure.Metadata;
|
|
|
|
/// <summary>Провайдер метаданных OMDb (omdbapi.com, данные IMDb). Требует API-ключ.</summary>
|
|
public sealed class OmdbMetadataProvider(
|
|
IHttpClientFactory httpFactory,
|
|
IOptions<MetadataOptions> options
|
|
) : IMetadataProvider
|
|
{
|
|
private readonly OmdbOptions _omdb = options.Value.Omdb;
|
|
|
|
public string Key => "omdb";
|
|
|
|
public async Task<IReadOnlyList<MetadataCandidate>> SearchShowsAsync(
|
|
string query,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
// Тип в запросе не фиксируем: imdbID уникален и для фильма, и для сериала, а GetShowAsync
|
|
// ходит по нему же — значит полнометражки находятся и обогащаются тем же путём, без
|
|
// разделения пространств идентификаторов (в отличие от TMDb, где tv и movie нумеруются врозь).
|
|
var url = $"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&s={Uri.EscapeDataString(query)}";
|
|
using var doc = await GetAsync(httpFactory, url, cancellationToken);
|
|
if (!doc.RootElement.TryGetProperty("Search", out var search))
|
|
return [];
|
|
|
|
var list = new List<MetadataCandidate>();
|
|
foreach (var item in search.EnumerateArray())
|
|
{
|
|
var id = Clean(GetString(item, "imdbID"));
|
|
if (id is null)
|
|
continue;
|
|
|
|
// Отдельные серии в выдаче — шум: шоу из них не собирают, а список они забивают.
|
|
var kind = KindFrom(Clean(GetString(item, "Type")));
|
|
if (kind is null)
|
|
continue;
|
|
|
|
list.Add(
|
|
new MetadataCandidate(
|
|
id,
|
|
Clean(GetString(item, "Title")) ?? "—",
|
|
YearFrom(GetString(item, "Year")),
|
|
null,
|
|
Clean(GetString(item, "Poster")),
|
|
kind
|
|
)
|
|
);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
public async Task<ShowMetadata?> GetShowAsync(
|
|
string externalId,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
var url = $"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&i={Uri.EscapeDataString(externalId)}";
|
|
using var doc = await TryGetAsync(httpFactory, url, cancellationToken);
|
|
if (doc is null || !IsResponseTrue(doc.RootElement))
|
|
return null;
|
|
var root = doc.RootElement;
|
|
return new ShowMetadata(
|
|
externalId,
|
|
Clean(GetString(root, "Title")) ?? "—",
|
|
YearFrom(GetString(root, "Year")),
|
|
Clean(GetString(root, "Plot")),
|
|
Clean(GetString(root, "Poster")),
|
|
GenresFrom(Clean(GetString(root, "Genre"))),
|
|
// «Rated» приезжает только в ответе по id — то есть ровно здесь, без лишнего запроса.
|
|
// Отдаём как есть: приводить к шкале будет приложение (ContentRating).
|
|
Clean(GetString(root, "Rated"))
|
|
);
|
|
}
|
|
|
|
/// <summary>Тип из выдачи OMDb: «series»/«movie»; «episode» и незнакомое — null (в шоу не годятся).</summary>
|
|
private static ShowKind? KindFrom(string? type) =>
|
|
type?.ToLowerInvariant() switch
|
|
{
|
|
"series" => ShowKind.Series,
|
|
"movie" => ShowKind.Single,
|
|
_ => null,
|
|
};
|
|
|
|
/// <summary>OMDb отдаёт жанры одной строкой через запятую: «Action, Sci-Fi».</summary>
|
|
private static List<string> GenresFrom(string? value) =>
|
|
string.IsNullOrWhiteSpace(value)
|
|
? []
|
|
: value
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.ToList();
|
|
|
|
public async Task<EpisodeMetadata?> GetEpisodeAsync(
|
|
string externalId,
|
|
int season,
|
|
int episode,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
var url =
|
|
$"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&i={Uri.EscapeDataString(externalId)}"
|
|
+ $"&Season={season}&Episode={episode}";
|
|
using var doc = await TryGetAsync(httpFactory, url, cancellationToken);
|
|
if (doc is null || !IsResponseTrue(doc.RootElement))
|
|
return null;
|
|
var root = doc.RootElement;
|
|
return new EpisodeMetadata(
|
|
Clean(GetString(root, "Title")) ?? "—",
|
|
Clean(GetString(root, "Plot")),
|
|
Clean(GetString(root, "Poster")),
|
|
DateFrom(GetString(root, "Released"))
|
|
);
|
|
}
|
|
|
|
public async Task<int?> GetSeasonEpisodeCountAsync(
|
|
string externalId,
|
|
int season,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
var url =
|
|
$"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&i={Uri.EscapeDataString(externalId)}&Season={season}";
|
|
using var doc = await TryGetAsync(httpFactory, url, cancellationToken);
|
|
if (
|
|
doc is null
|
|
|| !IsResponseTrue(doc.RootElement)
|
|
|| !doc.RootElement.TryGetProperty("Episodes", out var episodes)
|
|
|| episodes.ValueKind != JsonValueKind.Array
|
|
)
|
|
return null;
|
|
return episodes.GetArrayLength();
|
|
}
|
|
|
|
private static bool IsResponseTrue(JsonElement root) =>
|
|
GetString(root, "Response") is { } r
|
|
&& r.Equals("True", StringComparison.OrdinalIgnoreCase);
|
|
|
|
/// <summary>OMDb отдаёт «N/A» вместо отсутствующих значений — приводим к null.</summary>
|
|
private static string? Clean(string? value) =>
|
|
string.IsNullOrWhiteSpace(value) || value == "N/A" ? null : value;
|
|
|
|
private static DateOnly? DateFrom(string? released) =>
|
|
DateTime.TryParse(released, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt)
|
|
? DateOnly.FromDateTime(dt)
|
|
: null;
|
|
}
|