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; /// Провайдер метаданных 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 ) { // Тип в запросе не фиксируем: 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(); 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 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")) ); } /// Тип из выдачи OMDb: «series»/«movie»; «episode» и незнакомое — null (в шоу не годятся). private static ShowKind? KindFrom(string? type) => type?.ToLowerInvariant() switch { "series" => ShowKind.Series, "movie" => ShowKind.Single, _ => null, }; /// OMDb отдаёт жанры одной строкой через запятую: «Action, Sci-Fi». private static List GenresFrom(string? value) => string.IsNullOrWhiteSpace(value) ? [] : value .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .ToList(); 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 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 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); /// OMDb отдаёт «N/A» вместо отсутствующих значений — приводим к null. 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; }