Enhance metadata handling by introducing ShowKind parameter across various components. Update SearchShowMetadataQuery and related handlers to support querying by show type, ensuring accurate metadata retrieval for series and films. Implement validation to prevent episodes from being associated with non-series shows. Update frontend API to include ShowKind in metadata search requests, improving overall functionality and user experience.
ci / build-backend (push) Successful in 3m42s
ci / build-frontend (push) Successful in 51s
ci / tests (push) Successful in 2m10s
ci / sonar (push) Successful in 6m40s

This commit is contained in:
Leonid Pershin
2026-07-26 22:29:16 +03:00
parent fdb0f321e4
commit b0740503bb
14 changed files with 327 additions and 40 deletions
@@ -1,20 +1,40 @@
using TeleWave.Application.Metadata;
using TeleWave.Domain.Library;
namespace TeleWave.Application.Common.Interfaces;
/// <summary>Источник метаданных о шоу/сериях (TMDb, OMDb). Реализация ходит во внешний API.</summary>
/// <summary>
/// Источник метаданных о шоу/сериях (TMDb, OMDb). Реализация ходит во внешний API.
///
/// Тип шоу передаётся отдельным параметром, а не зашивается в <c>externalId</c>: у TMDb сериалы и
/// фильмы нумеруются в разных пространствах, и одно и то же число там означает разное. Хранить
/// в идентификаторе синтетический префикс не пришлось — тип и так известен на месте вызова, а
/// <see cref="Show.Kind"/> неизменяем, поэтому найденный при поиске идентификатор при любом
/// последующем обращении истолкуется тем же типом.
/// </summary>
public interface IMetadataProvider
{
/// <summary>Ключ источника: «tmdb», «omdb».</summary>
string Key { get; }
/// <summary>Кандидаты по названию среди контента указанного типа.</summary>
Task<IReadOnlyList<MetadataCandidate>> SearchShowsAsync(
string query,
ShowKind kind,
CancellationToken cancellationToken
);
Task<ShowMetadata?> GetShowAsync(string externalId, CancellationToken cancellationToken);
/// <summary>
/// Метаданные шоу. <paramref name="kind"/> брать из самого шоу, а не из внешнего запроса:
/// иначе клиент может заставить записать сериалу метаданные одноимённого фильма.
/// </summary>
Task<ShowMetadata?> GetShowAsync(
string externalId,
ShowKind kind,
CancellationToken cancellationToken
);
/// <summary>Метаданные серии. Только для <see cref="ShowKind.Series"/> — у остальных серий нет.</summary>
Task<EpisodeMetadata?> GetEpisodeAsync(
string externalId,
int season,
@@ -5,6 +5,7 @@ using TeleWave.Application.Common.Models;
using TeleWave.Application.Library;
using TeleWave.Application.Library.Genres;
using TeleWave.Domain.Images;
using TeleWave.Domain.Library;
namespace TeleWave.Application.Metadata.ApplyShowMetadata;
@@ -27,11 +28,18 @@ public sealed class ApplyShowMetadataCommandHandler(
if (show is null)
return Result.Failure(ShowErrors.NotFound);
// Ролики метаданными не обогащаются: у них нет ни года, ни постера, а сотня роликов
// в поиске только мешала бы. Раньше это держалось на том, что UI такой кнопки не рисует.
if (show.Kind == ShowKind.Interstitial)
return Result.Failure(MetadataErrors.NotForInterstitials);
var provider = resolver.Resolve(command.Provider);
if (provider is null)
return Result.Failure(MetadataErrors.ProviderNotAvailable);
var meta = await provider.GetShowAsync(command.ExternalId, cancellationToken);
// Тип берём у шоу, а не из команды: иначе клиент смог бы заставить TMDb отдать метаданные
// одноимённого фильма для сериала — идентификаторы у них независимые.
var meta = await provider.GetShowAsync(command.ExternalId, show.Kind, cancellationToken);
if (meta is null)
return Result.Failure(MetadataErrors.NotFound);
@@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Application.Library;
using TeleWave.Domain.Library;
namespace TeleWave.Application.Metadata.FindMissingEpisodes;
@@ -23,6 +24,11 @@ public sealed class FindMissingEpisodesQueryHandler(
if (show is null)
return Result.Failure<MissingEpisodesReport>(ShowErrors.NotFound);
// Сезоны и серии есть только у сериала: у полнометражки и ролика запрос к источнику ушёл бы
// в /tv/{id} с идентификатором фильма и вернул бы либо пусто, либо чужие данные.
if (show.Kind != ShowKind.Series)
return Result.Failure<MissingEpisodesReport>(MetadataErrors.EpisodesForSeriesOnly);
if (string.IsNullOrEmpty(show.MetadataExternalId))
return Result.Failure<MissingEpisodesReport>(MetadataErrors.NoLinkedSource);
@@ -23,4 +23,14 @@ public static class MetadataErrors
"Metadata.ProviderRequestFailed",
"Источник метаданных вернул ошибку — проверьте API-ключ."
);
public static readonly Error NotForInterstitials = Error.Validation(
"Metadata.NotForInterstitials",
"Ролики-врезки метаданными не обогащаются."
);
public static readonly Error EpisodesForSeriesOnly = Error.Validation(
"Metadata.EpisodesForSeriesOnly",
"Серии есть только у сериала."
);
}
@@ -4,6 +4,7 @@ using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Application.Library;
using TeleWave.Domain.Images;
using TeleWave.Domain.Library;
namespace TeleWave.Application.Metadata.RefreshEpisodes;
@@ -25,6 +26,10 @@ public sealed class RefreshShowEpisodesMetadataCommandHandler(
if (show is null)
return Result.Failure<int>(ShowErrors.NotFound);
// Серии есть только у сериала — см. FindMissingEpisodesQueryHandler.
if (show.Kind != ShowKind.Series)
return Result.Failure<int>(MetadataErrors.EpisodesForSeriesOnly);
if (string.IsNullOrEmpty(show.MetadataExternalId))
return Result.Failure<int>(MetadataErrors.NoLinkedSource);
@@ -1,7 +1,12 @@
using LiteCqrs;
using TeleWave.Application.Common.Models;
using TeleWave.Domain.Library;
namespace TeleWave.Application.Metadata.SearchShows;
public sealed record SearchShowMetadataQuery(string Provider, string Query)
/// <summary>
/// Поиск кандидатов в источнике. <paramref name="Kind"/> определяет, среди чего искать: у TMDb
/// сериалы и фильмы лежат в разных пространствах идентификаторов.
/// </summary>
public sealed record SearchShowMetadataQuery(string Provider, string Query, ShowKind Kind)
: IQuery<Result<IReadOnlyList<MetadataCandidate>>>;
@@ -23,7 +23,11 @@ public sealed class SearchShowMetadataQueryHandler(IMetadataProviderResolver res
try
{
var results = await provider.SearchShowsAsync(query.Query, cancellationToken);
var results = await provider.SearchShowsAsync(
query.Query,
query.Kind,
cancellationToken
);
return Result.Success(results);
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)