161 lines
5.8 KiB
C#
161 lines
5.8 KiB
C#
using System.Globalization;
|
|
using System.Text.Json;
|
|
using Microsoft.Extensions.Options;
|
|
using TeleWave.Application.Common.Interfaces;
|
|
using TeleWave.Application.Metadata;
|
|
|
|
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
|
|
)
|
|
{
|
|
var url =
|
|
$"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&type=series&s={Uri.EscapeDataString(query)}";
|
|
using var doc = await GetJsonAsync(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;
|
|
list.Add(
|
|
new MetadataCandidate(
|
|
id,
|
|
Clean(GetString(item, "Title")) ?? "—",
|
|
YearFrom(GetString(item, "Year")),
|
|
null,
|
|
Clean(GetString(item, "Poster"))
|
|
)
|
|
);
|
|
}
|
|
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 TryGetJsonAsync(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"))
|
|
);
|
|
}
|
|
|
|
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 TryGetJsonAsync(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 TryGetJsonAsync(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();
|
|
}
|
|
|
|
/// <summary>GET+parse; бросает при не-2xx/сетевой ошибке (для поиска — чтобы показать сбой).</summary>
|
|
private async Task<JsonDocument> GetJsonAsync(string url, CancellationToken cancellationToken)
|
|
{
|
|
var client = httpFactory.CreateClient("metadata");
|
|
using var response = await client.GetAsync(url, cancellationToken);
|
|
response.EnsureSuccessStatusCode();
|
|
var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
|
return await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
|
|
}
|
|
|
|
/// <summary>Как GetJsonAsync, но глотает ошибки в null (для get/episode — деградируем мягко).</summary>
|
|
private async Task<JsonDocument?> TryGetJsonAsync(
|
|
string url,
|
|
CancellationToken cancellationToken
|
|
)
|
|
{
|
|
try
|
|
{
|
|
return await GetJsonAsync(url, cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
when (ex is HttpRequestException or JsonException or TaskCanceledException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static bool IsResponseTrue(JsonElement root) =>
|
|
GetString(root, "Response") is { } r
|
|
&& r.Equals("True", StringComparison.OrdinalIgnoreCase);
|
|
|
|
private static string? GetString(JsonElement el, string name) =>
|
|
el.TryGetProperty(name, out var v) && v.ValueKind == JsonValueKind.String
|
|
? v.GetString()
|
|
: null;
|
|
|
|
/// <summary>OMDb отдаёт «N/A» вместо отсутствующих значений — приводим к null.</summary>
|
|
private static string? Clean(string? value) =>
|
|
string.IsNullOrWhiteSpace(value) || value == "N/A" ? null : value;
|
|
|
|
private static int? YearFrom(string? year)
|
|
{
|
|
if (year is { Length: >= 4 } && int.TryParse(year.AsSpan(0, 4), out var y))
|
|
return y;
|
|
return null;
|
|
}
|
|
|
|
private static DateOnly? DateFrom(string? released) =>
|
|
DateTime.TryParse(released, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt)
|
|
? DateOnly.FromDateTime(dt)
|
|
: null;
|
|
}
|