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} |
|