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.
This commit is contained in:
@@ -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>
|
||||
|
||||
+20
@@ -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
|
||||
);
|
||||
+78
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user