From 2523808e3b8d6a06a93b4585345841b8a8e918c1 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Fri, 24 Jul 2026 22:18:50 +0300 Subject: [PATCH] Enhance schedule and show management: add season and episode information to ScheduleEntryDto, update ListShowsQueryHandler to include season counts, and modify frontend components to display this new data. Improve EPG handling by consolidating program entries into blocks for better clarity in the streaming interface. --- .../GetChannelScheduleQueryHandler.cs | 15 ++++- .../Broadcast/ScheduleEntryDto.cs | 3 +- .../Library/EpisodeName.cs | 40 +++++++++++++ .../ListShows/ListShowsQueryHandler.cs | 23 +++++++- .../TeleWave.Application/Library/ShowDtos.cs | 1 + .../GetPublicEpg/GetPublicEpgQueryHandler.cs | 3 +- .../features/admin/channels/ChannelDetail.tsx | 8 ++- .../src/features/admin/shows/ShowsPanel.tsx | 4 +- frontend/src/features/streaming/AirPage.tsx | 59 +++++++++++++------ frontend/src/shared/api/types.ts | 2 + frontend/src/shared/lib/i18n.ts | 2 + 11 files changed, 134 insertions(+), 26 deletions(-) create mode 100644 backend/src/TeleWave.Application/Library/EpisodeName.cs diff --git a/backend/src/TeleWave.Application/Broadcast/GetSchedule/GetChannelScheduleQueryHandler.cs b/backend/src/TeleWave.Application/Broadcast/GetSchedule/GetChannelScheduleQueryHandler.cs index edf2747..b6a3cf9 100644 --- a/backend/src/TeleWave.Application/Broadcast/GetSchedule/GetChannelScheduleQueryHandler.cs +++ b/backend/src/TeleWave.Application/Broadcast/GetSchedule/GetChannelScheduleQueryHandler.cs @@ -2,6 +2,7 @@ using LiteCqrs; using Microsoft.EntityFrameworkCore; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Common.Models; +using TeleWave.Application.Library; namespace TeleWave.Application.Broadcast.GetSchedule; @@ -36,6 +37,17 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext) .Select(s => new { s.Id, s.Name }) .ToDictionaryAsync(s => s.Id, s => s.Name, cancellationToken); + // Имена ассетов программ — чтобы показать реальную метку S16E03 в расписании админки. + var assetIds = entries + .Where(e => e.Kind == Domain.Broadcast.ScheduleEntryKind.Program) + .Select(e => e.MediaAssetId) + .Distinct() + .ToList(); + var assetNames = 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 dtos = entries .Select(e => new ScheduleEntryDto( e.Id, @@ -45,7 +57,8 @@ public sealed class GetChannelScheduleQueryHandler(IAppDbContext dbContext) e.EndsAtUtc, e.ShowId, e.ShowId is { } sid ? showNames.GetValueOrDefault(sid) : null, - e.EpisodeIndex + e.EpisodeIndex, + assetNames.TryGetValue(e.MediaAssetId, out var name) ? EpisodeName.ParseLabel(name) : null )) .ToList(); diff --git a/backend/src/TeleWave.Application/Broadcast/ScheduleEntryDto.cs b/backend/src/TeleWave.Application/Broadcast/ScheduleEntryDto.cs index 62c1eb5..b6f58ad 100644 --- a/backend/src/TeleWave.Application/Broadcast/ScheduleEntryDto.cs +++ b/backend/src/TeleWave.Application/Broadcast/ScheduleEntryDto.cs @@ -10,5 +10,6 @@ public sealed record ScheduleEntryDto( DateTimeOffset EndsAtUtc, Guid? ShowId, string? ShowName, - int? EpisodeIndex + int? EpisodeIndex, + string? SeasonEpisode ); diff --git a/backend/src/TeleWave.Application/Library/EpisodeName.cs b/backend/src/TeleWave.Application/Library/EpisodeName.cs new file mode 100644 index 0000000..62efabd --- /dev/null +++ b/backend/src/TeleWave.Application/Library/EpisodeName.cs @@ -0,0 +1,40 @@ +using System.Globalization; +using System.Text.RegularExpressions; + +namespace TeleWave.Application.Library; + +/// Разбор сезона/серии из имени файла (SxxEyy либо NxNN). Номера в модели не хранятся — +/// вытаскиваются из имён по мере надобности (сводка в списке шоу, метка в расписании админки). +public static class EpisodeName +{ + private static readonly Regex SxxEyy = new( + @"[Ss](\d{1,2})[ ._-]*[Ee](\d{1,3})", + RegexOptions.Compiled + ); + + private static readonly Regex NxNN = new( + @"(?:^|[^\d])(\d{1,2})x(\d{1,3})", + RegexOptions.Compiled | RegexOptions.IgnoreCase + ); + + public static (int Season, int Episode)? Parse(string? name) + { + if (string.IsNullOrEmpty(name)) + return null; + + var match = SxxEyy.Match(name); + if (!match.Success) + match = NxNN.Match(name); + + return match.Success + ? (int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture), + int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture)) + : null; + } + + public static int? ParseSeason(string? name) => Parse(name)?.Season; + + /// Метка вида «S16E03», либо null если распознать не удалось. + public static string? ParseLabel(string? name) => + Parse(name) is { } p ? $"S{p.Season:D2}E{p.Episode:D2}" : null; +} diff --git a/backend/src/TeleWave.Application/Library/ListShows/ListShowsQueryHandler.cs b/backend/src/TeleWave.Application/Library/ListShows/ListShowsQueryHandler.cs index 4c933f7..4c4811d 100644 --- a/backend/src/TeleWave.Application/Library/ListShows/ListShowsQueryHandler.cs +++ b/backend/src/TeleWave.Application/Library/ListShows/ListShowsQueryHandler.cs @@ -12,9 +12,28 @@ public sealed class ListShowsQueryHandler(IAppDbContext dbContext) CancellationToken cancellationToken ) { - return await dbContext.Shows.AsNoTracking() + var shows = await dbContext.Shows.AsNoTracking() + .Include(s => s.Episodes) .OrderBy(s => s.Name) - .Select(s => new ShowSummaryDto(s.Id, s.Name, s.Kind, s.Episodes.Count, s.CreatedAt)) .ToListAsync(cancellationToken); + + // Имена ассетов нужны, чтобы распознать сезоны (номера в модели не хранятся). + var assetIds = shows.SelectMany(s => s.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); + + return shows + .Select(s => + { + var seasons = s.Episodes + .Select(e => names.TryGetValue(e.MediaAssetId, out var n) ? EpisodeName.ParseSeason(n) : null) + .Where(season => season is not null) + .Distinct() + .Count(); + return new ShowSummaryDto(s.Id, s.Name, s.Kind, s.Episodes.Count, seasons, s.CreatedAt); + }) + .ToList(); } } diff --git a/backend/src/TeleWave.Application/Library/ShowDtos.cs b/backend/src/TeleWave.Application/Library/ShowDtos.cs index 38faf2d..16b8fce 100644 --- a/backend/src/TeleWave.Application/Library/ShowDtos.cs +++ b/backend/src/TeleWave.Application/Library/ShowDtos.cs @@ -8,6 +8,7 @@ public sealed record ShowSummaryDto( string Name, ShowKind Kind, int EpisodeCount, + int SeasonCount, DateTimeOffset CreatedAt ); diff --git a/backend/src/TeleWave.Application/Streaming/GetPublicEpg/GetPublicEpgQueryHandler.cs b/backend/src/TeleWave.Application/Streaming/GetPublicEpg/GetPublicEpgQueryHandler.cs index 66bc4f0..9838d52 100644 --- a/backend/src/TeleWave.Application/Streaming/GetPublicEpg/GetPublicEpgQueryHandler.cs +++ b/backend/src/TeleWave.Application/Streaming/GetPublicEpg/GetPublicEpgQueryHandler.cs @@ -45,7 +45,8 @@ public sealed class GetPublicEpgQueryHandler(IAppDbContext dbContext) e.EndsAtUtc, e.ShowId, e.ShowId is { } sid ? showNames.GetValueOrDefault(sid) : null, - e.EpisodeIndex + e.EpisodeIndex, + null // сезон/серию зрителю не показываем (и не светим имена файлов) )) .ToList(); diff --git a/frontend/src/features/admin/channels/ChannelDetail.tsx b/frontend/src/features/admin/channels/ChannelDetail.tsx index aed7ace..d0d5f06 100644 --- a/frontend/src/features/admin/channels/ChannelDetail.tsx +++ b/frontend/src/features/admin/channels/ChannelDetail.tsx @@ -614,8 +614,12 @@ function SchedulePreview({ entries }: { entries: ScheduleEntryDto[] }) { ) : ( {e.showName ?? '—'} - {e.episodeIndex != null && ( - · {t('air.episode')} {e.episodeIndex + 1} + {e.seasonEpisode ? ( + · {e.seasonEpisode} + ) : ( + e.episodeIndex != null && ( + · {t('air.episode')} {e.episodeIndex + 1} + ) )} )} diff --git a/frontend/src/features/admin/shows/ShowsPanel.tsx b/frontend/src/features/admin/shows/ShowsPanel.tsx index 1bffd9c..f0ac573 100644 --- a/frontend/src/features/admin/shows/ShowsPanel.tsx +++ b/frontend/src/features/admin/shows/ShowsPanel.tsx @@ -67,6 +67,7 @@ export function ShowsPanel() { {t('admin.shows.name')} {t('admin.shows.kind')} + {t('admin.shows.seasons')} {t('admin.shows.episodes')} {t('common.actions')} @@ -74,7 +75,7 @@ export function ShowsPanel() { {isLoading && ( - + {t('common.loading')} @@ -93,6 +94,7 @@ export function ShowsPanel() { {t(`admin.shows.kinds.${show.kind}`)} + {show.seasonCount} {show.episodeCount}