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.
210 lines
8.3 KiB
C#
210 lines
8.3 KiB
C#
using TeleWave.Application.Streaming.GetIptvGuide;
|
|
using TeleWave.Application.Tests.Support;
|
|
using TeleWave.Domain.Broadcast;
|
|
using TeleWave.Domain.Library;
|
|
using TeleWave.Domain.Media;
|
|
using Xunit;
|
|
|
|
namespace TeleWave.Application.Tests.Streaming;
|
|
|
|
/// <summary>
|
|
/// Телегид для IPTV: программа всех включённых каналов одной выгрузкой. Проверяем то, чем он
|
|
/// отличается от списка записей ленты — склейку серий в блок, отбор окна и подстановку описаний.
|
|
/// </summary>
|
|
public class IptvGuideTests
|
|
{
|
|
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 12, 0, 0, TimeSpan.Zero);
|
|
|
|
[Fact]
|
|
public async Task Guide_MergesConsecutiveEpisodesOfTheSameShow()
|
|
{
|
|
var fixture = new TestDb();
|
|
var channel = Enabled("Первый", "one");
|
|
var show = Show.Create("Футурама", ShowKind.Series);
|
|
var first = MediaAsset.Register("s01e01.mkv", ".mkv", MediaSource.Upload);
|
|
var second = MediaAsset.Register("s01e02.mkv", ".mkv", MediaSource.Upload);
|
|
show.AddEpisode(first.Id);
|
|
show.AddEpisode(second.Id);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Channels.Add(channel);
|
|
seed.Shows.Add(show);
|
|
seed.MediaAssets.AddRange(first, second);
|
|
seed.ScheduleEntries.AddRange(
|
|
Program(channel.Id, first.Id, show.Id, T0, 25),
|
|
Program(channel.Id, second.Id, show.Id, T0.AddMinutes(25), 25)
|
|
);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var channels = await GuideAsync(fixture, T0.AddHours(-1), T0.AddHours(3));
|
|
|
|
// Две серии встык — одна передача в глазах зрителя: гид показывает программу, а не файлы.
|
|
var programme = Assert.Single(Assert.Single(channels).Programmes);
|
|
Assert.Equal("Футурама", programme.Title);
|
|
Assert.Equal(T0, programme.StartsAtUtc);
|
|
Assert.Equal(T0.AddMinutes(50), programme.EndsAtUtc);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Guide_KeepsBlocksApartWhenSomethingRanBetween()
|
|
{
|
|
var fixture = new TestDb();
|
|
var channel = Enabled("Первый", "one");
|
|
var show = Show.Create("Футурама", ShowKind.Series);
|
|
var first = MediaAsset.Register("s01e01.mkv", ".mkv", MediaSource.Upload);
|
|
var second = MediaAsset.Register("s01e02.mkv", ".mkv", MediaSource.Upload);
|
|
show.AddEpisode(first.Id);
|
|
show.AddEpisode(second.Id);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Channels.Add(channel);
|
|
seed.Shows.Add(show);
|
|
seed.MediaAssets.AddRange(first, second);
|
|
seed.ScheduleEntries.AddRange(
|
|
Program(channel.Id, first.Id, show.Id, T0, 25),
|
|
// Разрыв в пять минут — между сериями шла реклама, и одной передачей это не назвать.
|
|
Program(channel.Id, second.Id, show.Id, T0.AddMinutes(30), 25)
|
|
);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var channels = await GuideAsync(fixture, T0.AddHours(-1), T0.AddHours(3));
|
|
|
|
var programmes = Assert.Single(channels).Programmes;
|
|
Assert.Equal(2, programmes.Count);
|
|
Assert.Equal(T0.AddMinutes(25), programmes[0].EndsAtUtc);
|
|
Assert.Equal(T0.AddMinutes(30), programmes[1].StartsAtUtc);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Guide_TakesTitleAndCoverFromEpisodeThenFallsBackToShow()
|
|
{
|
|
var fixture = new TestDb();
|
|
var channel = Enabled("Первый", "one");
|
|
var stillId = Guid.NewGuid();
|
|
var posterId = Guid.NewGuid();
|
|
|
|
var titled = Show.Create("Футурама", ShowKind.Series);
|
|
titled.SetPosterImage(posterId);
|
|
var withMetadata = MediaAsset.Register("s01e01.mkv", ".mkv", MediaSource.Upload);
|
|
var bare = MediaAsset.Register("s01e02.mkv", ".mkv", MediaSource.Upload);
|
|
titled
|
|
.AddEpisode(withMetadata.Id)
|
|
.ApplyMetadata("Космический пилот", "Фрай", stillId, null);
|
|
titled.AddEpisode(bare.Id);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Channels.Add(channel);
|
|
seed.Shows.Add(titled);
|
|
seed.MediaAssets.AddRange(withMetadata, bare);
|
|
seed.ScheduleEntries.AddRange(
|
|
Program(channel.Id, withMetadata.Id, titled.Id, T0, 25),
|
|
Program(channel.Id, bare.Id, titled.Id, T0.AddMinutes(30), 25)
|
|
);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var channels = await GuideAsync(fixture, T0.AddHours(-1), T0.AddHours(3));
|
|
|
|
var programmes = Assert.Single(channels).Programmes;
|
|
Assert.Equal("Космический пилот", programmes[0].SubTitle);
|
|
Assert.Equal("Фрай", programmes[0].Description);
|
|
Assert.Equal(stillId, programmes[0].ImageId);
|
|
// Серия без метаданных показывается обложкой шоу — пустая карточка в плеере хуже общей.
|
|
Assert.Null(programmes[1].SubTitle);
|
|
Assert.Equal(posterId, programmes[1].ImageId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Guide_TakesOnlyEnabledChannelsAndTheRequestedWindow()
|
|
{
|
|
var fixture = new TestDb();
|
|
var visible = Enabled("Первый", "one");
|
|
var hidden = Channel.Create("Выключенный", "off", T0);
|
|
hidden.UpdateSettings("Выключенный", isEnabled: false, null, null);
|
|
|
|
var show = Show.Create("Ворон", ShowKind.Single);
|
|
var asset = MediaAsset.Register("crow.mkv", ".mkv", MediaSource.Upload);
|
|
show.AddEpisode(asset.Id);
|
|
|
|
await using (var seed = fixture.New())
|
|
{
|
|
seed.Channels.AddRange(visible, hidden);
|
|
seed.Shows.Add(show);
|
|
seed.MediaAssets.Add(asset);
|
|
seed.ScheduleEntries.AddRange(
|
|
Program(visible.Id, asset.Id, show.Id, T0, 60),
|
|
// За окном — не в выгрузке; выключенный канал не показываем вовсе.
|
|
Program(visible.Id, asset.Id, show.Id, T0.AddHours(10), 60),
|
|
Program(hidden.Id, asset.Id, show.Id, T0, 60),
|
|
// Заставки и врезки в гид не идут: у них нет шоу, а значит и передачи.
|
|
ScheduleEntry.Bumper(
|
|
visible.Id,
|
|
asset.Id,
|
|
T0.AddHours(2),
|
|
T0.AddHours(2).AddSeconds(10),
|
|
null,
|
|
null
|
|
)
|
|
);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
}
|
|
|
|
var channels = await GuideAsync(fixture, T0.AddHours(-1), T0.AddHours(3));
|
|
|
|
var dto = Assert.Single(channels);
|
|
Assert.Equal("one", dto.Slug);
|
|
var programme = Assert.Single(dto.Programmes);
|
|
Assert.Equal("Ворон", programme.Title);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Guide_WithoutChannels_ReturnsEmpty()
|
|
{
|
|
var fixture = new TestDb();
|
|
var channels = await GuideAsync(fixture, T0, T0.AddHours(3));
|
|
|
|
Assert.Empty(channels);
|
|
}
|
|
|
|
private static async Task<IReadOnlyList<IptvChannelDto>> GuideAsync(
|
|
TestDb fixture,
|
|
DateTimeOffset from,
|
|
DateTimeOffset to
|
|
)
|
|
{
|
|
await using var db = fixture.New();
|
|
return await new GetIptvGuideQueryHandler(db).Handle(
|
|
new GetIptvGuideQuery(from, to),
|
|
CancellationToken.None
|
|
);
|
|
}
|
|
|
|
private static Channel Enabled(string name, string slug)
|
|
{
|
|
var channel = Channel.Create(name, slug, T0);
|
|
channel.UpdateSettings(name, isEnabled: true, null, null);
|
|
return channel;
|
|
}
|
|
|
|
private static ScheduleEntry Program(
|
|
Guid channelId,
|
|
Guid assetId,
|
|
Guid showId,
|
|
DateTimeOffset startsAt,
|
|
int minutes
|
|
) =>
|
|
ScheduleEntry.Program(
|
|
channelId,
|
|
assetId,
|
|
startsAt,
|
|
startsAt.AddMinutes(minutes),
|
|
showId,
|
|
0
|
|
);
|
|
}
|