Prepare podcast emissions using LiveFlow
Persist scheduled emissions, expose owner-only planning controls in PostIt and publish optional presentation-only announcements on Yavsc.Org. Preserve manual broadcasting and authenticated playback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
e3b4414ab1
commit
4a5a3c8844
28 changed files with 5960 additions and 29 deletions
|
|
@ -23,6 +23,15 @@ public sealed class PodcastController(
|
|||
ApplicationDbContext db,
|
||||
ILogger<PodcastController> logger) : Controller
|
||||
{
|
||||
[AllowAnonymous, HttpGet]
|
||||
public async Task<IActionResult> Announcements(CancellationToken ct)
|
||||
{
|
||||
using var response = await api.SendAsync(HttpMethod.Get, "announcements", null, ct, publicAnnouncement: true);
|
||||
var announcements = await response.Content.ReadFromJsonAsync<List<PodcastAnnouncementResponse>>(ct)
|
||||
?? throw new JsonException("Empty podcast announcements response.");
|
||||
return View(announcements);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Index(CancellationToken ct)
|
||||
{
|
||||
|
|
|
|||
5078
src/Yavsc.Org/Migrations/20261102122758_PodcastScheduling.Designer.cs
generated
Normal file
5078
src/Yavsc.Org/Migrations/20261102122758_PodcastScheduling.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
101
src/Yavsc.Org/Migrations/20261102122758_PodcastScheduling.cs
Normal file
101
src/Yavsc.Org/Migrations/20261102122758_PodcastScheduling.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Yavsc.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class PodcastScheduling : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "PlannedDurationMinutes",
|
||||
table: "LiveFlow",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "PodcastDeleted",
|
||||
table: "LiveFlow",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "PodcastId",
|
||||
table: "LiveFlow",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "PreparedAt",
|
||||
table: "LiveFlow",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PublicAnnouncement",
|
||||
table: "LiveFlow",
|
||||
type: "character varying(1023)",
|
||||
maxLength: 1023,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "ScheduledStartAt",
|
||||
table: "LiveFlow",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "StartedAt",
|
||||
table: "LiveFlow",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_LiveFlow_PodcastId",
|
||||
table: "LiveFlow",
|
||||
column: "PodcastId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_LiveFlow_PodcastId",
|
||||
table: "LiveFlow");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PlannedDurationMinutes",
|
||||
table: "LiveFlow");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PodcastDeleted",
|
||||
table: "LiveFlow");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PodcastId",
|
||||
table: "LiveFlow");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PreparedAt",
|
||||
table: "LiveFlow");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PublicAnnouncement",
|
||||
table: "LiveFlow");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ScheduledStartAt",
|
||||
table: "LiveFlow");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "StartedAt",
|
||||
table: "LiveFlow");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3082,9 +3082,31 @@ namespace Yavsc.Migrations
|
|||
b.Property<string>("Pitch")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int?>("PlannedDurationMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<bool>("PodcastDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid?>("PodcastId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("PreparedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("PublicAnnouncement")
|
||||
.HasMaxLength(1023)
|
||||
.HasColumnType("character varying(1023)");
|
||||
|
||||
b.Property<DateTimeOffset?>("ScheduledStartAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("SequenceNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("text");
|
||||
|
||||
|
|
@ -3092,6 +3114,9 @@ namespace Yavsc.Migrations
|
|||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.HasIndex("PodcastId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("LiveFlow");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -24,14 +24,16 @@ public sealed class PodcastWebClient(HttpClient http, IOptions<SiteSettings> opt
|
|||
}
|
||||
|
||||
public async Task<HttpResponseMessage> SendAsync(HttpMethod method, string path,
|
||||
RangeHeaderValue? range, CancellationToken ct)
|
||||
RangeHeaderValue? range, CancellationToken ct, bool publicAnnouncement = false)
|
||||
{
|
||||
var settings = options.Value;
|
||||
if (!Uri.TryCreate(settings.ApiUrl, UriKind.Absolute, out var address)
|
||||
|| address.Scheme != Uri.UriSchemeHttps || !string.IsNullOrEmpty(address.UserInfo))
|
||||
throw new PodcastException(503, "La connexion aux podcasts n'est pas configuree.");
|
||||
|
||||
if (_accessToken == null)
|
||||
if (publicAnnouncement && (method != HttpMethod.Get || path != "announcements"))
|
||||
throw new InvalidOperationException("Only podcast announcements may be requested anonymously.");
|
||||
if (!publicAnnouncement && _accessToken == null)
|
||||
{
|
||||
var user = contextAccessor.HttpContext?.User;
|
||||
var userId = user?.GetUserId();
|
||||
|
|
@ -52,7 +54,8 @@ public sealed class PodcastWebClient(HttpClient http, IOptions<SiteSettings> opt
|
|||
if (!address.AbsolutePath.EndsWith("/" + Constants.APIPrefix + "/", StringComparison.Ordinal))
|
||||
address = new Uri(address, Constants.APIPrefix + "/");
|
||||
using var request = new HttpRequestMessage(method, new Uri(address, "podcasts/" + path));
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
|
||||
if (!publicAnnouncement)
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _accessToken);
|
||||
request.Headers.Range = range;
|
||||
var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
|
||||
if (!response.IsSuccessStatusCode && response.StatusCode != HttpStatusCode.RequestedRangeNotSatisfiable)
|
||||
|
|
|
|||
21
src/Yavsc.Org/Views/Podcast/Announcements.cshtml
Normal file
21
src/Yavsc.Org/Views/Podcast/Announcements.cshtml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
@model IReadOnlyList<Yavsc.Abstract.Streaming.PodcastAnnouncementResponse>
|
||||
@{ ViewBag.Title = Localizer["Emissions annoncees"]; }
|
||||
<h1>@Localizer["Emissions annoncees"]</h1>
|
||||
<p>@Localizer["Horaires previsionnels. La lecture des directs et replays necessite une connexion."]</p>
|
||||
<a asp-action="Index" class="btn btn-primary">@Localizer["Se connecter et ecouter les podcasts"]</a>
|
||||
@if (Model.Count == 0)
|
||||
{
|
||||
<p>@Localizer["Aucune emission annoncee pour le moment."]</p>
|
||||
}
|
||||
@foreach (var announcement in Model)
|
||||
{
|
||||
<article class="card my-3">
|
||||
<div class="card-body">
|
||||
<h2 class="h4">@announcement.Title</h2>
|
||||
<p>@announcement.OwnerName</p>
|
||||
<p><time datetime="@announcement.ScheduledStartAt.ToString("O")">@announcement.ScheduledStartAt.ToString("u")</time>
|
||||
· @Localizer["Duree prevue : {0} min", announcement.PlannedDurationMinutes]</p>
|
||||
<p style="white-space: pre-wrap">@announcement.Announcement</p>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a asp-action="Index" class="btn btn-outline-primary">@Localizer["Actualiser"]</a>
|
||||
<a asp-action="Announcements" class="btn btn-outline-primary">@Localizer["Emissions annoncees"]</a>
|
||||
<a asp-controller="Donation" asp-action="Settings" class="btn btn-outline-success">@Localizer["Configurer mes dons PayPal"]</a>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -34,6 +35,7 @@ else
|
|||
{
|
||||
PodcastState.Live => Localizer["En direct"],
|
||||
PodcastState.Completed => Localizer["Replay"],
|
||||
PodcastState.Planned => Localizer["Emission preparee"],
|
||||
_ => Localizer["Direct interrompu"]
|
||||
};
|
||||
<article class="col-12 col-lg-6">
|
||||
|
|
@ -47,6 +49,10 @@ else
|
|||
@podcast.OwnerName · <time datetime="@podcast.CreatedAt.ToString("O")">@podcast.CreatedAt.ToLocalTime().ToString("g")</time>
|
||||
</p>
|
||||
<p>@Localizer["Durée : {0} s", podcast.DurationSeconds.ToString("0.##")]</p>
|
||||
@if (podcast.ScheduledStartAt.HasValue)
|
||||
{
|
||||
<p>@Localizer["Debut prevu : {0} ; duree prevue : {1} min", podcast.ScheduledStartAt.Value.ToString("u"), podcast.PlannedDurationMinutes]</p>
|
||||
}
|
||||
@if (podcast.SegmentCount > 0)
|
||||
{
|
||||
<video controls playsinline preload="none" class="w-100"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ background-attachment: fixed;">
|
|||
<li class="nav-item"><a class="nav-link @PageHelpers.ActivePage(ViewContext, "")" href="/">@Localizer["Home"]</a></li>
|
||||
<li class="nav-item"><a asp-controller="Blogspot" asp-action="Index" class="nav-link @PageHelpers.ActivePage(ViewContext, "Blogspot")">@Localizer["Blogs"]</a></li>
|
||||
<li class="nav-item"><a asp-controller="Podcast" asp-action="Index" class="nav-link @PageHelpers.ActivePage(ViewContext, "Podcast")">@Localizer["Podcasts"]</a></li>
|
||||
<li class="nav-item"><a asp-controller="Podcast" asp-action="Announcements" class="nav-link">@Localizer["Emissions annoncees"]</a></li>
|
||||
<li class="nav-item"><a asp-controller="Home" asp-action="Contact" class="nav-link @PageHelpers.ActivePage(ViewContext, "Home", "Contact")">@Localizer["Contact"]</a></li>
|
||||
<li class="nav-item"><a asp-controller="Home" asp-action="About" class="nav-link @PageHelpers.ActivePage(ViewContext, "Home", "About")">@Localizer["About"]</a></li>
|
||||
<partial name="_LoginPartial" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue