Add IPTV playlist functionality and enhance streaming endpoints
Implemented a new endpoint for downloading the IPTV playlist, allowing users to access a comprehensive M3U file containing all channels and program guides. Updated the StreamingEndpoints to support token validation for both cookie and query parameters, ensuring secure access for external IPTV players. Enhanced the AirPage component to include a download button for the IPTV playlist, with appropriate user feedback on success or failure. Updated localization strings to reflect new features and instructions for users.
This commit is contained in:
@@ -117,7 +117,9 @@ public static class StreamingEndpoints
|
||||
// Плейлист hls.js перезагружает регулярно — здесь дёшево (1 запрос на перезагрузку) сверить,
|
||||
// что зритель из токена ещё существует и не заблокирован. Так блокировка отражается почти сразу,
|
||||
// не дожидаясь истечения короткого TTL cookie; сегменты этой проверки не делают (слишком часто).
|
||||
if (tokens.Validate(request.Cookies[StreamCookieName]) is not { } userId)
|
||||
// Для IPTV это единственная точка, где блокировка вообще может сработать: у внешнего плеера
|
||||
// пропуск долгий и перевыпуску не подлежит.
|
||||
if (Viewer(request, tokens) is not { } userId)
|
||||
return Results.Unauthorized();
|
||||
var profile = await identity.GetProfileAsync(userId, cancellationToken);
|
||||
if (profile is null || profile.IsBlocked)
|
||||
@@ -132,8 +134,15 @@ public static class StreamingEndpoints
|
||||
if (result.Value.Segments.Count == 0)
|
||||
return Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||
|
||||
// Токен из ссылки тянется дальше в адреса сегментов: плеер разрешает их относительно
|
||||
// плейлиста и query при этом теряет, а cookie у него нет — без этого сегменты дадут 401.
|
||||
var token = request.Query["token"].ToString();
|
||||
var suffix = string.IsNullOrEmpty(token)
|
||||
? string.Empty
|
||||
: $"?token={Uri.EscapeDataString(token)}";
|
||||
|
||||
response.Headers.CacheControl = "no-cache";
|
||||
return Results.Text(Render(result.Value), "application/vnd.apple.mpegurl");
|
||||
return Results.Text(Render(result.Value, suffix), "application/vnd.apple.mpegurl");
|
||||
}
|
||||
|
||||
private static IResult Segment(
|
||||
@@ -145,7 +154,7 @@ public static class StreamingEndpoints
|
||||
MediaPathResolver paths
|
||||
)
|
||||
{
|
||||
if (tokens.Validate(request.Cookies[StreamCookieName]) is null)
|
||||
if (Viewer(request, tokens) is null)
|
||||
return Results.Unauthorized();
|
||||
if (!SegmentFiles.IsSegmentName(file))
|
||||
return Results.NotFound();
|
||||
@@ -157,7 +166,15 @@ public static class StreamingEndpoints
|
||||
return Results.File(path, "video/mp2t", enableRangeProcessing: true);
|
||||
}
|
||||
|
||||
private static string Render(LivePlaylistDto playlist)
|
||||
/// <summary>
|
||||
/// Кто смотрит: cookie — для браузера, токен в ссылке — для внешнего IPTV-плеера, который
|
||||
/// cookie не носит. Токен один и тот же по формату, различается только сроком выдачи.
|
||||
/// </summary>
|
||||
private static Guid? Viewer(HttpRequest request, StreamTokenService tokens) =>
|
||||
tokens.Validate(request.Cookies[StreamCookieName])
|
||||
?? tokens.Validate(request.Query["token"]);
|
||||
|
||||
private static string Render(LivePlaylistDto playlist, string segmentSuffix)
|
||||
{
|
||||
var extinf = playlist.TargetDuration.ToString("F6", CultureInfo.InvariantCulture);
|
||||
var sb = new StringBuilder();
|
||||
@@ -179,7 +196,7 @@ public static class StreamingEndpoints
|
||||
sb.Append(CultureInfo.InvariantCulture, $"#EXTINF:{extinf},\n");
|
||||
sb.Append(
|
||||
CultureInfo.InvariantCulture,
|
||||
$"/api/stream/{segment.AssetId:N}/seg{segment.LocalIndex:D5}.ts\n"
|
||||
$"/api/stream/{segment.AssetId:N}/seg{segment.LocalIndex:D5}.ts{segmentSuffix}\n"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user