Update scheduling parameters and refactor channel endpoints: extend HorizonDays to 7 and RetentionDays to 90 in appsettings.json. Consolidate channel-related endpoint logic by removing obsolete files and enhancing the ShowEndpoints with audience and genre management capabilities. Improve error handling and streamline command handlers for channel operations.
This commit is contained in:
+11
@@ -0,0 +1,11 @@
|
||||
using LiteCqrs;
|
||||
using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Programming.Groups.FindGroupCandidates;
|
||||
|
||||
/// <summary>
|
||||
/// Подбирает позиции по правилу набора. <paramref name="Filter"/> = null — берём правило, сохранённое
|
||||
/// у группы; передача фильтра явно нужна редактору, где его крутят до сохранения.
|
||||
/// </summary>
|
||||
public sealed record FindGroupCandidatesQuery(Guid GroupId, GroupFilter? Filter = null)
|
||||
: IQuery<Result<IReadOnlyList<GroupCandidateDto>>>;
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
using TeleWave.Domain.Programming;
|
||||
|
||||
namespace TeleWave.Application.Programming.Groups.FindGroupCandidates;
|
||||
|
||||
public sealed class FindGroupCandidatesQueryHandler(
|
||||
IAppDbContext dbContext,
|
||||
GroupElementResolver resolver
|
||||
) : IQueryHandler<FindGroupCandidatesQuery, Result<IReadOnlyList<GroupCandidateDto>>>
|
||||
{
|
||||
public async Task<Result<IReadOnlyList<GroupCandidateDto>>> Handle(
|
||||
FindGroupCandidatesQuery query,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var group = await dbContext
|
||||
.Groups.AsNoTracking()
|
||||
.Include(g => g.Items)
|
||||
.FirstOrDefaultAsync(g => g.Id == query.GroupId, cancellationToken);
|
||||
if (group is null)
|
||||
return Result.Failure<IReadOnlyList<GroupCandidateDto>>(GroupErrors.NotFound);
|
||||
|
||||
var filter = query.Filter ?? GroupFilter.FromJson(group.FilterJson);
|
||||
if (filter is null)
|
||||
return Result.Failure<IReadOnlyList<GroupCandidateDto>>(GroupErrors.FilterNotSet);
|
||||
|
||||
var wantsShows =
|
||||
filter.ElementKinds is null or { Count: 0 }
|
||||
|| filter.ElementKinds.Contains(GroupElementKind.Show);
|
||||
var wantsCollections =
|
||||
filter.ElementKinds is null or { Count: 0 }
|
||||
|| filter.ElementKinds.Contains(GroupElementKind.Collection);
|
||||
|
||||
var matchingShowIds = await MatchShowIdsAsync(filter, cancellationToken);
|
||||
|
||||
var elements = new List<(GroupElementKind Kind, Guid Id)>();
|
||||
if (wantsShows)
|
||||
elements.AddRange(matchingShowIds.Select(id => (GroupElementKind.Show, id)));
|
||||
|
||||
if (wantsCollections)
|
||||
{
|
||||
// Коллекция подходит, только если подходят все её части: иначе во «взрослую ночь» через
|
||||
// франшизу просочилось бы детское, а в «боевики» — комедия из той же серии фильмов.
|
||||
var collections = await dbContext
|
||||
.Collections.AsNoTracking()
|
||||
.Select(c => new { c.Id, ShowIds = c.Items.Select(i => i.ShowId).ToList() })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var matching = matchingShowIds.ToHashSet();
|
||||
elements.AddRange(
|
||||
collections
|
||||
.Where(c => c.ShowIds.Count > 0 && c.ShowIds.All(matching.Contains))
|
||||
.Select(c => (GroupElementKind.Collection, c.Id))
|
||||
);
|
||||
}
|
||||
|
||||
var info = await resolver.ResolveAsync(elements, cancellationToken);
|
||||
var inGroup = group
|
||||
.Items.Select(i => (i.ElementKind, i.ElementId))
|
||||
.ToHashSet();
|
||||
|
||||
var result = new List<GroupCandidateDto>();
|
||||
foreach (var (kind, id) in elements)
|
||||
{
|
||||
if (!info.TryGetValue((kind, id), out var element))
|
||||
continue;
|
||||
if (!MatchesUnitDuration(filter, element))
|
||||
continue;
|
||||
|
||||
result.Add(
|
||||
new GroupCandidateDto(
|
||||
kind,
|
||||
id,
|
||||
element.Name,
|
||||
element.UnitCount,
|
||||
element.ShowKind,
|
||||
element.Audience,
|
||||
element.Year,
|
||||
element.PosterImageId,
|
||||
inGroup.Contains((kind, id))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.Success<IReadOnlyList<GroupCandidateDto>>(
|
||||
result.OrderBy(c => c.ElementName, StringComparer.CurrentCultureIgnoreCase).ToList()
|
||||
);
|
||||
}
|
||||
|
||||
private async Task<List<Guid>> MatchShowIdsAsync(
|
||||
GroupFilter filter,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var shows = dbContext.Shows.AsNoTracking();
|
||||
|
||||
if (filter.ShowKinds is { Count: > 0 } kinds)
|
||||
shows = shows.Where(s => kinds.Contains(s.Kind));
|
||||
|
||||
// Категории упорядочены по возрастанию строгости, поэтому «не строже» — обычное сравнение.
|
||||
if (filter.MaxAudience is { } maxAudience)
|
||||
shows = shows.Where(s => s.Audience <= maxAudience);
|
||||
|
||||
if (filter.YearMin is { } yearMin)
|
||||
shows = shows.Where(s => s.Year != null && s.Year >= yearMin);
|
||||
if (filter.YearMax is { } yearMax)
|
||||
shows = shows.Where(s => s.Year != null && s.Year <= yearMax);
|
||||
|
||||
// Жанры — «любой из»: шоу редко подходит под все перечисленные сразу.
|
||||
if (filter.GenreIds is { Count: > 0 } genreIds)
|
||||
shows = shows.Where(s => s.Genres.Any(g => genreIds.Contains(g.GenreId)));
|
||||
|
||||
return await shows.Select(s => s.Id).ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Длительность проверяется по средней единице, а не по сумме: у сериала в фильтре «серии
|
||||
/// по 20–25 минут» осмысленна именно длина серии. Элементы без готовых ассетов длительности
|
||||
/// не имеют — их этот фильтр не отбрасывает, иначе из группы выпадало бы всё, что ещё
|
||||
/// обрабатывается.
|
||||
/// </summary>
|
||||
private static bool MatchesUnitDuration(GroupFilter filter, GroupElementInfo element)
|
||||
{
|
||||
if (filter.UnitMinutesMin is null && filter.UnitMinutesMax is null)
|
||||
return true;
|
||||
if (element.UnitCount == 0 || element.TotalDuration <= TimeSpan.Zero)
|
||||
return true;
|
||||
|
||||
var averageMinutes = element.TotalDuration.TotalMinutes / element.UnitCount;
|
||||
if (filter.UnitMinutesMin is { } min && averageMinutes < min)
|
||||
return false;
|
||||
if (filter.UnitMinutesMax is { } max && averageMinutes > max)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user