Files
TeleWave/backend/src/TeleWave.Infrastructure/Metadata/OmdbMetadataProvider.cs
T

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;
}