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
|
|
@ -122,6 +122,22 @@ public sealed class PodcastBroadcasterTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Prepared_broadcast_starts_the_existing_emission_instead_of_creating_another()
|
||||
{
|
||||
await using var transport = new Transport();
|
||||
var client = new PodcastApiClient(transport, () => "https://api.example/");
|
||||
FakeCapture? capture = null;
|
||||
await using var broadcaster = await PodcastBroadcaster.StartAsync(client, "Prepared",
|
||||
(directory, _) => Task.FromResult<IPodcastCapture>(capture = new FakeCapture(directory)),
|
||||
TestContext.Current.CancellationToken, transport.PodcastId);
|
||||
Assert.Equal(transport.PodcastId, broadcaster.Podcast.Id);
|
||||
Assert.Equal(transport.PodcastId, transport.StartedId);
|
||||
Assert.Equal(0, transport.CreateCount);
|
||||
await capture!.StopAsync();
|
||||
await broadcaster.Completion.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Native_stop_flushes_final_segment_before_completing_server_replay()
|
||||
{
|
||||
|
|
@ -217,10 +233,14 @@ public sealed class PodcastBroadcasterTests
|
|||
public List<string> Events { get; } = [];
|
||||
public bool FailUpload { get; init; }
|
||||
public int CreateCount { get; private set; }
|
||||
public Guid PodcastId => _podcast.Id;
|
||||
public Guid? StartedId { get; private set; }
|
||||
public TaskCompletionSource FirstUpload { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public Task<T> CallAsync<T>(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
||||
{
|
||||
if (path.EndsWith("/complete", StringComparison.Ordinal)) Events.Add("complete");
|
||||
else if (path.EndsWith("/start", StringComparison.Ordinal))
|
||||
StartedId = Guid.Parse(new Uri(path).Segments[^2].TrimEnd('/'));
|
||||
else CreateCount++;
|
||||
return Task.FromResult((T)(object)_podcast);
|
||||
}
|
||||
|
|
|
|||
88
src/PostIt/PostIt.Tests/PodcastScheduleTests.cs
Normal file
88
src/PostIt/PostIt.Tests/PodcastScheduleTests.cs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
using Avalonia.Headless.XUnit;
|
||||
using PostIt.Services;
|
||||
using PostIt.ViewModels;
|
||||
using Yavsc.Abstract.Identity;
|
||||
using Yavsc.Abstract.Streaming;
|
||||
using Yavsc.Api.Client;
|
||||
|
||||
namespace PostIt.Tests;
|
||||
|
||||
public sealed class PodcastScheduleTests
|
||||
{
|
||||
[AvaloniaFact]
|
||||
public async Task Preparation_and_editing_send_schedule_and_optional_announcement_without_capture()
|
||||
{
|
||||
var tokenPath = Path.Combine(Path.GetTempPath(), $"postit-plan-{Guid.NewGuid():N}.json");
|
||||
var store = new TokenStore(tokenPath);
|
||||
store.Save(new RefreshTokenRecord("test-access-token", "test-refresh-token", DateTimeOffset.UtcNow.AddHours(1), null));
|
||||
try
|
||||
{
|
||||
await using var api = new ScheduleApi(store);
|
||||
var session = new SessionStatusViewModel { Api = api };
|
||||
session.Refresh();
|
||||
var vm = new PodcastsPageViewModel(new PodcastApiClient(api, () => "https://api.example/"), api, session)
|
||||
{
|
||||
Title = "Concert", ScheduledDate = DateTimeOffset.Now.AddDays(7),
|
||||
ScheduledTime = new TimeSpan(14, 15, 0), PlannedDurationMinutes = 90
|
||||
};
|
||||
Assert.True(vm.PrepareCommand.CanExecute(null));
|
||||
await vm.PrepareCommand.ExecuteAsync(null);
|
||||
Assert.Equal(HttpMethod.Post, api.SavedMethod);
|
||||
Assert.EndsWith("/podcasts/planned", api.SavedPath);
|
||||
Assert.Equal(90, api.Saved?.PlannedDurationMinutes);
|
||||
Assert.Null(api.Saved?.PublicAnnouncement);
|
||||
Assert.Equal(14, api.Saved?.ScheduledStartAt.Hour);
|
||||
Assert.Equal(PodcastState.Planned, vm.SelectedPodcast?.State);
|
||||
Assert.False(vm.FinishCommand.CanExecute(null));
|
||||
Assert.True(vm.DeleteCommand.CanExecute(null));
|
||||
Assert.True(vm.UpdatePlanCommand.CanExecute(null));
|
||||
vm.Title = "Nouveau titre";
|
||||
vm.PublishAnnouncement = true;
|
||||
vm.Announcement = "Venez au concert";
|
||||
await vm.UpdatePlanCommand.ExecuteAsync(null);
|
||||
Assert.Equal(HttpMethod.Put, api.SavedMethod);
|
||||
Assert.EndsWith($"/podcasts/{api.Plan.Id}/planned", api.SavedPath);
|
||||
Assert.Equal("Venez au concert", api.Saved?.PublicAnnouncement);
|
||||
Assert.Equal("Nouveau titre", vm.SelectedPodcast?.Title);
|
||||
vm.PublishAnnouncement = false;
|
||||
await vm.UpdatePlanCommand.ExecuteAsync(null);
|
||||
Assert.Null(api.Saved?.PublicAnnouncement);
|
||||
vm.PlannedDurationMinutes = 0;
|
||||
api.Saved = null;
|
||||
await vm.UpdatePlanCommand.ExecuteAsync(null);
|
||||
Assert.Null(api.Saved);
|
||||
Assert.Contains("Preparation impossible", vm.Status);
|
||||
Assert.False(vm.IsBroadcasting);
|
||||
}
|
||||
finally { File.Delete(tokenPath); }
|
||||
}
|
||||
|
||||
private sealed class ScheduleApi(TokenStore store) : YavscApiClient(new Settings(), store)
|
||||
{
|
||||
public PodcastResponse Plan { get; private set; } = new(Guid.NewGuid(), "Concert", "owner", "Owner",
|
||||
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow, PodcastState.Planned, 0, 0, 0);
|
||||
public PreparePodcastRequest? Saved { get; set; }
|
||||
public string? SavedPath { get; private set; }
|
||||
public HttpMethod? SavedMethod { get; private set; }
|
||||
|
||||
public override Task<T> CallAsync<T>(HttpMethod method, string path, object? body = null, CancellationToken ct = default)
|
||||
{
|
||||
object response;
|
||||
if (path.EndsWith("/account/me", StringComparison.Ordinal)) response = new Me { UserId = "owner" };
|
||||
else if (method == HttpMethod.Get) response = new List<PodcastResponse> { Plan };
|
||||
else
|
||||
{
|
||||
Saved = Assert.IsType<PreparePodcastRequest>(body);
|
||||
SavedPath = path;
|
||||
SavedMethod = method;
|
||||
Plan = Plan with
|
||||
{
|
||||
Title = Saved.Title, ScheduledStartAt = Saved.ScheduledStartAt,
|
||||
PlannedDurationMinutes = Saved.PlannedDurationMinutes, PublicAnnouncement = Saved.PublicAnnouncement
|
||||
};
|
||||
response = Plan;
|
||||
}
|
||||
return Task.FromResult((T)response);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue