fix(60): declare disabled-channel and no-playout as Unavailable preview causes
GetPreview keyed only on StreamingMode + JWT, so a disabled channel or one with no playout was declared Available and then failed confusingly (404 from IptvController, or an indefinitely-blocking manifest request). Extend it to take isEnabled + playoutCount and check JWT, then disabled, then no-playout, before falling back to the existing mode-based rules; order is documented in a comment. Also corrects a stale comment in ChannelPreviewResponseModel.cs that claimed a C# string generates a TypeScript union (it does not).
This commit is contained in:
@@ -114,7 +114,7 @@ internal static class Mapper
|
||||
channel.ShowInEpg,
|
||||
playoutCount,
|
||||
GetLogoUrl(channel),
|
||||
GetPreview(channel.StreamingMode, channel.Number, iptvJwtEnabled));
|
||||
GetPreview(channel.StreamingMode, channel.Number, iptvJwtEnabled, channel.IsEnabled, playoutCount));
|
||||
|
||||
internal static ResolutionViewModel ProjectToViewModel(Resolution resolution) =>
|
||||
new(resolution.Height, resolution.Width);
|
||||
@@ -183,8 +183,17 @@ internal static class Mapper
|
||||
internal static ChannelPreviewResponseModel GetPreview(
|
||||
StreamingMode streamingMode,
|
||||
string channelNumber,
|
||||
bool iptvJwtEnabled)
|
||||
bool iptvJwtEnabled,
|
||||
bool isEnabled,
|
||||
int playoutCount)
|
||||
{
|
||||
// Precedence among the three Unavailable causes (checked in this order; the first match wins):
|
||||
// 1. JWT enabled — a host-wide auth configuration; nothing the operator can fix per-channel.
|
||||
// 2. channel disabled — an explicit operator choice; IptvController 404s a disabled channel, so
|
||||
// preview must not even try.
|
||||
// 3. no playout — the channel could theoretically play once scheduled, but a manifest
|
||||
// request against it blocks indefinitely today; catch it before that happens.
|
||||
//
|
||||
// /iptv/* is gated by ConditionalIptvAuthorizeFilter only when JWT is configured, and the
|
||||
// "jwt" scheme does not accept the SPA's ctv-session cookie. Nothing mints a JWT for the
|
||||
// SPA today, so preview cannot run at all in that configuration.
|
||||
@@ -196,6 +205,22 @@ internal static class Mapper
|
||||
"IPTV JWT authentication is enabled");
|
||||
}
|
||||
|
||||
if (!isEnabled)
|
||||
{
|
||||
return new ChannelPreviewResponseModel(
|
||||
ChannelPreviewAvailability.Unavailable,
|
||||
null,
|
||||
"Channel is disabled");
|
||||
}
|
||||
|
||||
if (playoutCount == 0)
|
||||
{
|
||||
return new ChannelPreviewResponseModel(
|
||||
ChannelPreviewAvailability.Unavailable,
|
||||
null,
|
||||
"Channel has no playout");
|
||||
}
|
||||
|
||||
return streamingMode switch
|
||||
{
|
||||
StreamingMode.HttpLiveStreamingSegmenter or StreamingMode.HttpLiveStreamingDirect =>
|
||||
|
||||
@@ -9,8 +9,10 @@ namespace ErsatzTV.Core.Api.Channels;
|
||||
/// SPA would mean keying behavior off the human-readable StreamingMode label.
|
||||
/// </remarks>
|
||||
public record ChannelPreviewResponseModel(
|
||||
// One of ChannelPreviewAvailability's values. A plain string (not an enum) so the generated
|
||||
// TypeScript is a string union the SPA can switch on directly.
|
||||
// One of ChannelPreviewAvailability's values. A plain string (not a C# enum), which keeps the
|
||||
// OpenAPI schema simple, but the tradeoff is that the generated TypeScript types this as a bare
|
||||
// `string`, not a literal union — the SPA hand-maintains its own `ChannelPreviewAvailability`
|
||||
// union (web/src/api/channels.ts) and narrows against it, rather than getting one for free.
|
||||
string Availability,
|
||||
// Rooted, directly-usable HLS manifest URL; null when Availability is Unavailable.
|
||||
string? ManifestUrl,
|
||||
|
||||
@@ -13,7 +13,8 @@ public class ChannelPreviewTests
|
||||
[TestCase(StreamingMode.HttpLiveStreamingDirect)]
|
||||
public void HlsModes_Are_Available_With_Plain_Manifest_Url(StreamingMode mode)
|
||||
{
|
||||
ChannelPreviewResponseModel result = Mapper.GetPreview(mode, "12.1", iptvJwtEnabled: false);
|
||||
ChannelPreviewResponseModel result =
|
||||
Mapper.GetPreview(mode, "12.1", iptvJwtEnabled: false, isEnabled: true, playoutCount: 1);
|
||||
|
||||
result.Availability.ShouldBe("Available");
|
||||
result.ManifestUrl.ShouldBe("/iptv/channel/12.1.m3u8");
|
||||
@@ -24,7 +25,8 @@ public class ChannelPreviewTests
|
||||
[TestCase(StreamingMode.TransportStreamHybrid)]
|
||||
public void TransportStream_Modes_Are_ForcedHlsOnly(StreamingMode mode)
|
||||
{
|
||||
ChannelPreviewResponseModel result = Mapper.GetPreview(mode, "12.1", iptvJwtEnabled: false);
|
||||
ChannelPreviewResponseModel result =
|
||||
Mapper.GetPreview(mode, "12.1", iptvJwtEnabled: false, isEnabled: true, playoutCount: 1);
|
||||
|
||||
result.Availability.ShouldBe("ForcedHlsOnly");
|
||||
result.ManifestUrl.ShouldBe("/iptv/channel/12.1.m3u8?mode=segmenter");
|
||||
@@ -37,18 +39,53 @@ public class ChannelPreviewTests
|
||||
[TestCase(StreamingMode.TransportStreamHybrid)]
|
||||
public void Jwt_Enabled_Makes_Every_Mode_Unavailable(StreamingMode mode)
|
||||
{
|
||||
ChannelPreviewResponseModel result = Mapper.GetPreview(mode, "12.1", iptvJwtEnabled: true);
|
||||
ChannelPreviewResponseModel result =
|
||||
Mapper.GetPreview(mode, "12.1", iptvJwtEnabled: true, isEnabled: true, playoutCount: 1);
|
||||
|
||||
result.Availability.ShouldBe("Unavailable");
|
||||
result.ManifestUrl.ShouldBeNull();
|
||||
result.UnavailableReason.ShouldBe("IPTV JWT authentication is enabled");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Disabled_Channel_Is_Unavailable()
|
||||
{
|
||||
ChannelPreviewResponseModel result = Mapper.GetPreview(
|
||||
StreamingMode.HttpLiveStreamingSegmenter,
|
||||
"12.1",
|
||||
iptvJwtEnabled: false,
|
||||
isEnabled: false,
|
||||
playoutCount: 1);
|
||||
|
||||
result.Availability.ShouldBe("Unavailable");
|
||||
result.ManifestUrl.ShouldBeNull();
|
||||
result.UnavailableReason.ShouldBe("Channel is disabled");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Zero_Playout_Count_Is_Unavailable()
|
||||
{
|
||||
ChannelPreviewResponseModel result = Mapper.GetPreview(
|
||||
StreamingMode.HttpLiveStreamingSegmenter,
|
||||
"12.1",
|
||||
iptvJwtEnabled: false,
|
||||
isEnabled: true,
|
||||
playoutCount: 0);
|
||||
|
||||
result.Availability.ShouldBe("Unavailable");
|
||||
result.ManifestUrl.ShouldBeNull();
|
||||
result.UnavailableReason.ShouldBe("Channel has no playout");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Channel_Number_Is_Used_Verbatim_In_The_Manifest_Url()
|
||||
{
|
||||
ChannelPreviewResponseModel result =
|
||||
Mapper.GetPreview(StreamingMode.HttpLiveStreamingSegmenter, "7", iptvJwtEnabled: false);
|
||||
ChannelPreviewResponseModel result = Mapper.GetPreview(
|
||||
StreamingMode.HttpLiveStreamingSegmenter,
|
||||
"7",
|
||||
iptvJwtEnabled: false,
|
||||
isEnabled: true,
|
||||
playoutCount: 1);
|
||||
|
||||
result.ManifestUrl.ShouldBe("/iptv/channel/7.m3u8");
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ public class OpenApiSerializerContractTests
|
||||
true,
|
||||
2,
|
||||
"/iptv/logos/logo.png",
|
||||
Mapper.GetPreview(StreamingMode.HttpLiveStreamingSegmenter, "1", false));
|
||||
Mapper.GetPreview(StreamingMode.HttpLiveStreamingSegmenter, "1", false, true, 2));
|
||||
|
||||
private static string FindOpenApiDocument()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user