Files
TeleWave/backend/src/TeleWave.Infrastructure/Metadata/OmdbMetadataProvider.cs
T
Leonid Pershin 986f7ff52d
ci / build-backend (push) Successful in 4m7s
ci / build-frontend (push) Successful in 41s
ci / tests (push) Successful in 2m4s
ci / sonar (push) Successful in 4m41s
Add missing seasons endpoint and update metadata handling
Implemented a new endpoint to retrieve missing seasons for shows, enhancing the MetadataEndpoints class. Updated the IMetadataProvider interface to include a method for fetching season numbers. Adjusted the ShowSummaryDto to include other genres and modified the ListShowsQueryHandler to handle multiple genres. Enhanced the Omdb and Tmdb metadata providers to support season number retrieval. Updated frontend components to integrate missing seasons functionality, including API calls and UI elements for displaying missing seasons reports.
2026-07-27 02:40:09 +03:00

180 lines
7.6 KiB
C#

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;
/// <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,
ShowKind kind,
CancellationToken cancellationToken
)
{
// Тип сужает выдачу, но не влияет на идентификатор: imdbID уникален и для фильма, и для
// сериала, поэтому GetShowAsync ходит по нему одинаково — в отличие от TMDb, где tv и movie
// нумеруются врозь.
var type = kind == ShowKind.Series ? "series" : "movie";
var url =
$"{_omdb.BaseUrl}/?apikey={_omdb.ApiKey}&type={type}&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<MetadataCandidate>();
foreach (var item in search.EnumerateArray())
{
var id = Clean(GetString(item, "imdbID"));
if (id is null)
continue;
// Тип читаем из ответа, а не подставляем запрошенный: фильтр в запросе OMDb соблюдает,
// но отдельные серии (Type = episode) иногда просачиваются, а шоу из них не собирают.
var found = KindFrom(Clean(GetString(item, "Type")));
if (found is null)
continue;
list.Add(
new MetadataCandidate(
id,
Clean(GetString(item, "Title")) ?? "—",
YearFrom(GetString(item, "Year")),
null,
Clean(GetString(item, "Poster")),
found
)
);
}
return list;
}
/// <summary>Тип здесь не используется: imdbID однозначен для фильмов и сериалов одинаково.</summary>
public async Task<ShowMetadata?> GetShowAsync(
string externalId,
ShowKind kind,
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"))
);
}
/// <summary>Тип из выдачи OMDb: «series»/«movie»; «episode» и незнакомое — null (в шоу не годятся).</summary>
private static ShowKind? KindFrom(string? type) =>
type?.ToLowerInvariant() switch
{
"series" => ShowKind.Series,
"movie" => ShowKind.Single,
_ => null,
};
/// <summary>OMDb отдаёт жанры одной строкой через запятую: «Action, Sci-Fi».</summary>
private static List<string> GenresFrom(string? value) =>
string.IsNullOrWhiteSpace(value)
? []
: value
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToList();
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 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<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 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();
}
/// <summary>
/// OMDb перечня сезонов не отдаёт — только их количество (<c>totalSeasons</c>, строкой) в ответе
/// по id. Считаем сезоны сплошной нумерацией с единицы: разрывов в ней у OMDb не бывает.
/// </summary>
public async Task<IReadOnlyList<int>> GetSeasonNumbersAsync(
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 [];
var total = Clean(GetString(doc.RootElement, "totalSeasons"));
return int.TryParse(total, CultureInfo.InvariantCulture, out var count) && count > 0
? Enumerable.Range(1, count).ToList()
: [];
}
private static bool IsResponseTrue(JsonElement root) =>
GetString(root, "Response") is { } r
&& r.Equals("True", StringComparison.OrdinalIgnoreCase);
/// <summary>OMDb отдаёт «N/A» вместо отсутствующих значений — приводим к null.</summary>
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;
}