The last resilience slice. When a drop ends the broadcast, YouTube cannot resume it, so the only way
back is a new one — and that is what the app now offers, in one click.
- `Services/BroadcastTitle.cs` — the resume marker is applied to the title the show was using, and any
existing marker is REPLACED rather than stacked ("Show (resumed)" → "Show (resumed 2)"), with the base
truncated so the whole title fits YouTube's 100-character limit. Otherwise a channel grows two
identically named videos and the creator has to explain it.
- `MainViewModel.BackOnAirCommand` — reuses the ordinary go-live path with the saved details and the
cached stream: new broadcast, new VOD, no dialog. It appears only after a drop ended the session and
the ordinary start button steps aside, so there is exactly one way forward.
- The running line goes red OFF AIR (not amber) in that state; the button reads "● Back on air".
Tests: `BackOnAirTests` — first re-air marks the title, a second drop counts instead of stacking, the
100-character limit holds, an empty title still works, and the post-drop state exposes exactly one
start affordance. FAST gate: 552 passed, 0 failed, 0 warnings.
TASK 32 is now complete: fragmented MP4, blip retry, poll-driven RECONNECTING, ingest pre-flight, Back
on air.
66 lines
2.3 KiB
C#
66 lines
2.3 KiB
C#
using System;
|
|
using System.IO;
|
|
using Microsoft.Data.Sqlite;
|
|
using Xunit;
|
|
using ytLive.Services;
|
|
using ytLive.ViewModels;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// TASK 32·3 — Back on air. Once YouTube completes a broadcast it cannot be resumed, so the only honest
|
|
/// route back is a NEW broadcast: one click, saved details, no dialog, and a title that says it is a
|
|
/// resume (otherwise the channel grows two identically named videos).
|
|
/// </summary>
|
|
public class BackOnAirTests
|
|
{
|
|
[Fact]
|
|
public void ResumeTitle_MarksTheFirstReAir()
|
|
=> Assert.Equal("Friday Night Show (resumed)", BroadcastTitle.ForResume("Friday Night Show", 1));
|
|
|
|
[Fact]
|
|
public void ResumeTitle_CountsASecondDrop_InsteadOfStackingMarkers()
|
|
=> Assert.Equal("Friday Night Show (resumed 2)", BroadcastTitle.ForResume("Friday Night Show (resumed)", 2));
|
|
|
|
[Fact]
|
|
public void ResumeTitle_FitsYouTubesHundredCharacterLimit()
|
|
{
|
|
var longTitle = new string('x', 140);
|
|
var resumed = BroadcastTitle.ForResume(longTitle, 1);
|
|
|
|
Assert.Equal(BroadcastTitle.MaxLength, resumed.Length);
|
|
Assert.EndsWith(" (resumed)", resumed);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResumeTitle_HandlesAnEmptyTitle()
|
|
=> Assert.Equal("LlamaCasty (resumed)", BroadcastTitle.ForResume(" ", 1));
|
|
|
|
[Fact]
|
|
public void AfterADrop_TheCreatorIsOfferedOneClickBackOnAir()
|
|
{
|
|
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-backonair-{Guid.NewGuid():N}.db");
|
|
MainViewModel.LayoutPathOverride = tempDb;
|
|
var vm = new MainViewModel();
|
|
try
|
|
{
|
|
Assert.False(vm.IsOffAirAfterDrop);
|
|
Assert.False(vm.BackOnAirCommand.CanExecute(null));
|
|
|
|
vm.MarkOffAirAfterDropForTest("Friday Night Show");
|
|
|
|
Assert.True(vm.IsOffAirAfterDrop);
|
|
Assert.True(vm.BackOnAirCommand.CanExecute(null));
|
|
Assert.Equal("● Back on air", vm.BackOnAirLabel);
|
|
// The ordinary start button steps aside so there is exactly one way forward.
|
|
Assert.False(vm.ShowPrimaryStartButton);
|
|
}
|
|
finally
|
|
{
|
|
MainViewModel.LayoutPathOverride = null;
|
|
SqliteConnection.ClearAllPools();
|
|
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
|
}
|
|
}
|
|
}
|