using System.Globalization; using System.Text.Json; using Microsoft.Extensions.Options; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Metadata; namespace TeleWave.Infrastructure.Metadata; /// Провайдер метаданных OMDb (omdbapi.com, данные IMDb). Требует API-ключ. public sealed class OmdbMetadataProvider( IHttpClientFactory httpFactory, IOptions options ) : IMetadataProvider { private readonly OmdbOptions _omdb = options.Value.Omdb; public string Key => "omdb"; public async Task> 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(); 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 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 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 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(); } /// GET+parse; бросает при не-2xx/сетевой ошибке (для поиска — чтобы показать сбой). private async Task 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); } /// Как GetJsonAsync, но глотает ошибки в null (для get/episode — деградируем мягко). private async Task 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; /// OMDb отдаёт «N/A» вместо отсутствующих значений — приводим к null. 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; }