Add missing episodes feature: implement FindMissingEpisodes endpoint, update metadata providers to retrieve season episode counts, and enhance frontend components for displaying missing episodes report. Update translations for new UI elements.
build / backend (push) Successful in 3m5s
build / frontend (push) Successful in 44s
tests / backend-tests (push) Successful in 2m38s

This commit is contained in:
Leonid Pershin
2026-07-26 02:15:47 +03:00
parent 8962840654
commit 44e954920c
11 changed files with 344 additions and 2 deletions
@@ -21,6 +21,13 @@ public interface IMetadataProvider
int episode,
CancellationToken cancellationToken
);
/// <summary>Сколько серий в указанном сезоне по данным источника (null — сезон не найден/нет данных).</summary>
Task<int?> GetSeasonEpisodeCountAsync(
string externalId,
int season,
CancellationToken cancellationToken
);
}
/// <summary>Резолвит провайдер по ключу и перечисляет реально настроенные (с API-ключом) источники.</summary>
@@ -0,0 +1,20 @@
using LiteCqrs;
using TeleWave.Application.Common.Models;
namespace TeleWave.Application.Metadata.FindMissingEpisodes;
/// <summary>Отчёт: каких серий не хватает в загруженных сезонах шоу (по данным привязанного источника).</summary>
public sealed record FindMissingEpisodesQuery(Guid ShowId) : IQuery<Result<MissingEpisodesReport>>;
public sealed record MissingEpisodesReport(IReadOnlyList<SeasonGapDto> Seasons);
/// <param name="Season">Номер сезона (есть хотя бы одна загруженная серия).</param>
/// <param name="Expected">Сколько серий в сезоне по источнику (null — источник не отдал данные).</param>
/// <param name="Loaded">Сколько серий этого сезона загружено.</param>
/// <param name="Missing">Отсутствующие номера серий (пусто — все на месте либо Expected неизвестен).</param>
public sealed record SeasonGapDto(
int Season,
int? Expected,
int Loaded,
IReadOnlyList<int> Missing
);
@@ -0,0 +1,78 @@
using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Application.Library;
namespace TeleWave.Application.Metadata.FindMissingEpisodes;
public sealed class FindMissingEpisodesQueryHandler(
IAppDbContext dbContext,
IMetadataProviderResolver resolver
) : IQueryHandler<FindMissingEpisodesQuery, Result<MissingEpisodesReport>>
{
public async Task<Result<MissingEpisodesReport>> Handle(
FindMissingEpisodesQuery query,
CancellationToken cancellationToken
)
{
var show = await dbContext
.Shows.AsNoTracking()
.Include(s => s.Episodes)
.FirstOrDefaultAsync(s => s.Id == query.ShowId, cancellationToken);
if (show is null)
return Result.Failure<MissingEpisodesReport>(ShowErrors.NotFound);
if (string.IsNullOrEmpty(show.MetadataExternalId))
return Result.Failure<MissingEpisodesReport>(MetadataErrors.NoLinkedSource);
var provider = show.MetadataProvider is { } key ? resolver.Resolve(key) : null;
if (provider is null)
return Result.Failure<MissingEpisodesReport>(MetadataErrors.ProviderNotAvailable);
// Имена файлов — чтобы распознать номера у серий, где они ещё не проставлены.
var assetIds = show.Episodes.Select(e => e.MediaAssetId).Distinct().ToList();
var names = await dbContext
.MediaAssets.AsNoTracking()
.Where(a => assetIds.Contains(a.Id))
.Select(a => new { a.Id, a.OriginalFileName })
.ToDictionaryAsync(a => a.Id, a => a.OriginalFileName, cancellationToken);
// Загруженные номера серий по сезонам (только сезоны с хотя бы одной серией).
var loadedBySeason = new Dictionary<int, HashSet<int>>();
foreach (var episode in show.Episodes)
{
var season = episode.Season;
var number = episode.Episode;
if ((season is null || number is null) && names.TryGetValue(episode.MediaAssetId, out var name))
{
if (EpisodeName.Parse(name) is { } parsed)
(season, number) = (parsed.Season, parsed.Episode);
}
if (season is null || number is null)
continue;
if (!loadedBySeason.TryGetValue(season.Value, out var set))
loadedBySeason[season.Value] = set = [];
set.Add(number.Value);
}
var seasons = new List<SeasonGapDto>();
foreach (var season in loadedBySeason.Keys.OrderBy(s => s))
{
var loaded = loadedBySeason[season];
var expected = await provider.GetSeasonEpisodeCountAsync(
show.MetadataExternalId,
season,
cancellationToken
);
var missing =
expected is { } exp
? Enumerable.Range(1, exp).Where(n => !loaded.Contains(n)).ToList()
: [];
seasons.Add(new SeasonGapDto(season, expected, loaded.Count, missing));
}
return Result.Success(new MissingEpisodesReport(seasons));
}
}