From 7a164dbd9021d236d9a47eeac50b3cdd686b995a Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 2 Aug 2026 17:07:24 +0300 Subject: [PATCH] Enhance channel settings to include icon image support for IPTV Updated the Channel and related DTOs to incorporate an optional IconImageId property, allowing for the specification of a channel icon for external IPTV players. Modified the UpdateChannelSettings command and its handler to accommodate this new property. Adjusted frontend components to manage the icon image, including selection and display functionality. Updated localization files to support new UI elements related to the IPTV icon. This enhancement improves the user experience by providing a visual representation of channels in IPTV applications. --- .../Endpoints/ChannelEndpoints.cs | 15 +- .../TeleWave.Api/Endpoints/IptvEndpoints.cs | 17 +- .../Broadcast/ChannelDtos.cs | 2 + .../GetChannel/GetChannelQueryHandler.cs | 1 + .../UpdateChannelSettingsCommand.cs | 3 +- .../UpdateChannelSettingsCommandHandler.cs | 7 +- .../GetIptvGuide/GetIptvGuideQueryHandler.cs | 4 +- .../Streaming/GetIptvGuide/IptvDtos.cs | 2 +- .../src/TeleWave.Domain/Broadcast/Channel.cs | 11 +- ...0802110152_AddChannelIconImage.Designer.cs | 1605 +++++++++++++++++ .../20260802110152_AddChannelIconImage.cs | 28 + .../Migrations/AppDbContextModelSnapshot.cs | 3 + .../Broadcast/ChannelDetailsTests.cs | 2 +- .../Broadcast/ChannelHandlersTests.cs | 2 +- .../Library/DeleteGuardsTests.cs | 2 +- .../Notifications/TelegramBotTests.cs | 8 +- .../Streaming/IptvGuideTests.cs | 4 +- .../Streaming/ListPublicChannelsTests.cs | 6 +- .../Validators/ValidatorTests.cs | 2 +- .../Broadcast/ChannelTests.cs | 2 +- .../GridScheduleGeneratorIntegrationTests.cs | 4 +- docs/tv-scheduler-architecture.md | 8 + frontend/src/features/admin/channels/api.ts | 1 + .../channels/components/SettingsCard.tsx | 35 + frontend/src/shared/api/types.ts | 2 + frontend/src/shared/lib/locales/en.ts | 5 + frontend/src/shared/lib/locales/ru.ts | 5 + 27 files changed, 1758 insertions(+), 28 deletions(-) create mode 100644 backend/src/TeleWave.Infrastructure/Migrations/20260802110152_AddChannelIconImage.Designer.cs create mode 100644 backend/src/TeleWave.Infrastructure/Migrations/20260802110152_AddChannelIconImage.cs diff --git a/backend/src/TeleWave.Api/Endpoints/ChannelEndpoints.cs b/backend/src/TeleWave.Api/Endpoints/ChannelEndpoints.cs index 370a6d8..9262621 100644 --- a/backend/src/TeleWave.Api/Endpoints/ChannelEndpoints.cs +++ b/backend/src/TeleWave.Api/Endpoints/ChannelEndpoints.cs @@ -156,7 +156,13 @@ public static class ChannelEndpoints ) { var result = await sender.Send( - new UpdateChannelSettingsCommand(id, body.Name, body.IsEnabled, body.FillerAssetId), + new UpdateChannelSettingsCommand( + id, + body.Name, + body.IsEnabled, + body.FillerAssetId, + body.IconImageId + ), cancellationToken ); return result.ToHttpResult(); @@ -187,7 +193,12 @@ public sealed record UpdateChannelTimeBody( TimeOnly DayStartTime ); -public sealed record UpdateChannelSettingsBody(string Name, bool IsEnabled, Guid? FillerAssetId); +public sealed record UpdateChannelSettingsBody( + string Name, + bool IsEnabled, + Guid? FillerAssetId, + Guid? IconImageId +); /// Оверлеи и аналоговый фильтр — как канал выглядит у зрителя (см. 6.8). public sealed record UpdateViewerSettingsBody( diff --git a/backend/src/TeleWave.Api/Endpoints/IptvEndpoints.cs b/backend/src/TeleWave.Api/Endpoints/IptvEndpoints.cs index bd5b475..d09024e 100644 --- a/backend/src/TeleWave.Api/Endpoints/IptvEndpoints.cs +++ b/backend/src/TeleWave.Api/Endpoints/IptvEndpoints.cs @@ -21,6 +21,9 @@ public static class IptvEndpoints /// Сколько суток программы класть в XMLTV: горизонт ленты и так неделя. private static readonly TimeSpan GuideWindow = TimeSpan.FromDays(7); + /// Заглушка иконки канала, когда своя не задана: фавикон сайта из wwwroot. + private const string FaviconPath = "/favicon.svg"; + public static IEndpointRouteBuilder MapIptvEndpoints(this IEndpointRouteBuilder app) { var iptv = app.MapGroup("/api/iptv").WithTags("IPTV"); @@ -109,9 +112,8 @@ public static class IptvEndpoints foreach (var channel in channels) { - var logo = channel.LogoImageId is { } imageId - ? $" tvg-logo=\"{origin}/api/images/{imageId}\"" - : string.Empty; + // tvg-logo ставим всегда: своя иконка либо фавикон-заглушка — иначе в плеере пустой квадрат. + var logo = $" tvg-logo=\"{ChannelIcon(origin, channel.IconImageId)}\""; var number = channel.Number is { } value ? $" tvg-chno=\"{value.ToString(CultureInfo.InvariantCulture)}\"" : string.Empty; @@ -189,7 +191,10 @@ public static class IptvEndpoints "display-name", number.ToString(CultureInfo.InvariantCulture) ); - WriteIcon(writer, channel.LogoImageId, origin); + // Иконку канала пишем всегда: своя либо фавикон-заглушка (в отличие от иконок передач ниже). + writer.WriteStartElement("icon"); + writer.WriteAttributeString("src", ChannelIcon(origin, channel.IconImageId)); + writer.WriteEndElement(); writer.WriteEndElement(); } @@ -225,6 +230,10 @@ public static class IptvEndpoints writer.WriteEndElement(); } + /// URL иконки канала: своя из реестра либо фавикон-заглушка сайта (абсолютный адрес). + private static string ChannelIcon(string origin, Guid? iconImageId) => + iconImageId is { } id ? $"{origin}/api/images/{id}" : $"{origin}{FaviconPath}"; + /// Время XMLTV: YYYYMMDDHHMMSS +0000. Отдаём в UTC — плеер сдвинет сам. private static string XmltvTime(DateTimeOffset moment) => moment.ToUniversalTime().ToString("yyyyMMddHHmmss", CultureInfo.InvariantCulture) diff --git a/backend/src/TeleWave.Application/Broadcast/ChannelDtos.cs b/backend/src/TeleWave.Application/Broadcast/ChannelDtos.cs index 6a5f016..aee4b31 100644 --- a/backend/src/TeleWave.Application/Broadcast/ChannelDtos.cs +++ b/backend/src/TeleWave.Application/Broadcast/ChannelDtos.cs @@ -14,6 +14,8 @@ public sealed record ChannelDto( TimeOnly DayStartTime, Guid? TemplateId, Guid? FillerAssetId, + /// Иконка канала для внешних IPTV-плееров (M3U/XMLTV); null — отдаётся заглушка. + Guid? IconImageId, /// Оверлеи и фильтр зрительской части — всё опционально (см. 6.8). ViewerSettingsDto Viewer ); diff --git a/backend/src/TeleWave.Application/Broadcast/GetChannel/GetChannelQueryHandler.cs b/backend/src/TeleWave.Application/Broadcast/GetChannel/GetChannelQueryHandler.cs index f34041b..c0013aa 100644 --- a/backend/src/TeleWave.Application/Broadcast/GetChannel/GetChannelQueryHandler.cs +++ b/backend/src/TeleWave.Application/Broadcast/GetChannel/GetChannelQueryHandler.cs @@ -32,6 +32,7 @@ public sealed class GetChannelQueryHandler(IAppDbContext dbContext) channel.DayStartTime, channel.TemplateId, channel.FillerAssetId, + channel.IconImageId, new ViewerSettingsDto( channel.LogoImageId, channel.LogoCorner, diff --git a/backend/src/TeleWave.Application/Broadcast/UpdateChannelSettings/UpdateChannelSettingsCommand.cs b/backend/src/TeleWave.Application/Broadcast/UpdateChannelSettings/UpdateChannelSettingsCommand.cs index 410578d..ccfbc9a 100644 --- a/backend/src/TeleWave.Application/Broadcast/UpdateChannelSettings/UpdateChannelSettingsCommand.cs +++ b/backend/src/TeleWave.Application/Broadcast/UpdateChannelSettings/UpdateChannelSettingsCommand.cs @@ -7,5 +7,6 @@ public sealed record UpdateChannelSettingsCommand( Guid ChannelId, string Name, bool IsEnabled, - Guid? FillerAssetId + Guid? FillerAssetId, + Guid? IconImageId ) : ICommand; diff --git a/backend/src/TeleWave.Application/Broadcast/UpdateChannelSettings/UpdateChannelSettingsCommandHandler.cs b/backend/src/TeleWave.Application/Broadcast/UpdateChannelSettings/UpdateChannelSettingsCommandHandler.cs index a437509..03b7730 100644 --- a/backend/src/TeleWave.Application/Broadcast/UpdateChannelSettings/UpdateChannelSettingsCommandHandler.cs +++ b/backend/src/TeleWave.Application/Broadcast/UpdateChannelSettings/UpdateChannelSettingsCommandHandler.cs @@ -30,7 +30,12 @@ public sealed class UpdateChannelSettingsCommandHandler(IAppDbContext dbContext) return Result.Failure(ChannelErrors.AssetNotFound); } - channel.UpdateSettings(command.Name, command.IsEnabled, command.FillerAssetId); + channel.UpdateSettings( + command.Name, + command.IsEnabled, + command.FillerAssetId, + command.IconImageId + ); return Result.Success(); } } diff --git a/backend/src/TeleWave.Application/Streaming/GetIptvGuide/GetIptvGuideQueryHandler.cs b/backend/src/TeleWave.Application/Streaming/GetIptvGuide/GetIptvGuideQueryHandler.cs index b95a525..b1045cd 100644 --- a/backend/src/TeleWave.Application/Streaming/GetIptvGuide/GetIptvGuideQueryHandler.cs +++ b/backend/src/TeleWave.Application/Streaming/GetIptvGuide/GetIptvGuideQueryHandler.cs @@ -25,7 +25,7 @@ public sealed class GetIptvGuideQueryHandler(IAppDbContext dbContext) c.Slug, c.Name, c.Number, - c.LogoImageId, + c.IconImageId, }) .ToListAsync(cancellationToken); if (channels.Count == 0) @@ -129,7 +129,7 @@ public sealed class GetIptvGuideQueryHandler(IAppDbContext dbContext) channel.Slug, channel.Name, channel.Number, - channel.LogoImageId, + channel.IconImageId, programmes ) ); diff --git a/backend/src/TeleWave.Application/Streaming/GetIptvGuide/IptvDtos.cs b/backend/src/TeleWave.Application/Streaming/GetIptvGuide/IptvDtos.cs index 2e9c146..3f30b2c 100644 --- a/backend/src/TeleWave.Application/Streaming/GetIptvGuide/IptvDtos.cs +++ b/backend/src/TeleWave.Application/Streaming/GetIptvGuide/IptvDtos.cs @@ -5,7 +5,7 @@ public sealed record IptvChannelDto( string Slug, string Name, int? Number, - Guid? LogoImageId, + Guid? IconImageId, IReadOnlyList Programmes ); diff --git a/backend/src/TeleWave.Domain/Broadcast/Channel.cs b/backend/src/TeleWave.Domain/Broadcast/Channel.cs index 1c26d3e..9ea86f1 100644 --- a/backend/src/TeleWave.Domain/Broadcast/Channel.cs +++ b/backend/src/TeleWave.Domain/Broadcast/Channel.cs @@ -46,6 +46,14 @@ public class Channel /// Ассет-заглушка на случай пустого расписания (аварийная подстраховка). public Guid? FillerAssetId { get; private set; } + /// + /// Иконка канала для внешних IPTV-плееров (tvg-logo в M3U и <icon> в XMLTV): ссылка + /// на реестр изображений или null. Отдельная от экранного логотипа-оверлея + /// (): тот — полупрозрачный водяной знак в углу видео, а здесь нужна + /// нормальная квадратная иконка канала. Null — плеер получит заглушку (фавикон сайта). + /// + public Guid? IconImageId { get; private set; } + // ── Зрительская часть (см. 6.8). Всё рисуется на клиенте поверх