Add metadata management for shows: implement metadata retrieval, application, and updates in the API and UI. Enhance Show and ShowDto models to include metadata fields, and update the database schema accordingly. Introduce new endpoints for metadata operations and integrate metadata display in the ShowDetail component.

This commit is contained in:
Leonid Pershin
2026-07-25 09:07:55 +03:00
parent ba023bc416
commit 0d2dee815e
42 changed files with 2271 additions and 4 deletions
@@ -0,0 +1,122 @@
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 is null || !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 GetJsonAsync(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 GetJsonAsync(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"))
);
}
private async Task<JsonDocument?> GetJsonAsync(string url, CancellationToken cancellationToken)
{
var client = httpFactory.CreateClient("metadata");
try
{
using var response = await client.GetAsync(url, cancellationToken);
if (!response.IsSuccessStatusCode)
return null;
var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
return await JsonDocument.ParseAsync(stream, cancellationToken: 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;
}