Private broadcasts end like recordings: Enter keeps, Escape deletes (KeepBroadcastDialog). Privacy read from YouTube; delete = videos.delete (fallback liveBroadcasts.delete on 404). Refs: https://developers.google.com/youtube/v3/docs/videos/delete https://developers.google.com/youtube/v3/live/docs/liveBroadcasts/delete (Unit: private broadcast close-out)

This commit is contained in:
2026-10-09 13:32:38 -07:00
parent b140024ebf
commit 87f05ba2ce
8 changed files with 255 additions and 2 deletions
@@ -360,9 +360,10 @@ public partial class MainViewModel : ViewModelBase
// finalizes the VOD immediately instead of leaving viewers on a frozen
// "stream offline" until enableAutoStop fires (~1 min). Log-only failure:
// the backstop still ends the broadcast; a stop never fails over cosmetics.
if (wasLive && _currentBroadcastId != null)
var endedBroadcastId = wasLive ? _currentBroadcastId : null;
if (endedBroadcastId != null)
{
var closeOutError = await _youtubeStream.EndBroadcastAsync(_currentBroadcastId);
var closeOutError = await _youtubeStream.EndBroadcastAsync(endedBroadcastId);
if (closeOutError != null)
AppLog.Write($"End close-out: {closeOutError} — enableAutoStop will finish the broadcast");
}
@@ -370,6 +371,11 @@ public partial class MainViewModel : ViewModelBase
_currentBroadcastId = null;
OnPropertyChanged(nameof(CurrentBroadcastId));
// Creator ruling 2026-10-09: a PRIVATE broadcast ends like a recording — Enter keeps
// it on YouTube, Escape deletes it.
if (endedBroadcastId != null)
await OfferToKeepPrivateBroadcastAsync(endedBroadcastId);
if (wasRecording) await FinalizeRecordingAsync();
// Explicit sign-out only (TASK 18): stopping the stream does NOT clear
@@ -519,4 +525,49 @@ public partial class MainViewModel : ViewModelBase
_recDotPulse = _recDotPulse > 0.5 ? 0.35 : 1.0;
OnPropertyChanged(nameof(RecDotOpacity));
}
/// <summary>What happened to a finished broadcast at the keep-or-delete prompt.</summary>
internal enum PrivateBroadcastOutcome { NotPrivate, Kept, Deleted, DeleteFailed }
/// <summary>Test seam: answers the keep-or-delete prompt (title → keep?) instead of showing
/// <see cref="KeepBroadcastDialog"/>. Mirrors the other static overrides.</summary>
internal static Func<string, bool>? KeepBroadcastPromptOverride { get; set; }
private async Task OfferToKeepPrivateBroadcastAsync(string broadcastId)
{
var title = BroadcastForm.Title;
var (outcome, error) = await ClosePrivateBroadcastAsync(_youtubeStream, broadcastId,
() => KeepBroadcastPromptOverride?.Invoke(title) ?? new KeepBroadcastDialog(title).ShowDialog() == true);
switch (outcome)
{
case PrivateBroadcastOutcome.Kept:
AppLog.Write($"Private broadcast kept: {broadcastId}");
_notifications.Info("Broadcast kept", "It's on YouTube as a private video.");
break;
case PrivateBroadcastOutcome.Deleted:
AppLog.Write($"Private broadcast deleted by creator: {broadcastId}");
_notifications.Info("Broadcast deleted", "The broadcast was removed from YouTube.");
break;
case PrivateBroadcastOutcome.DeleteFailed:
AppLog.Write($"Private broadcast delete failed: {broadcastId}: {error}");
_notifications.Error("Couldn't delete the broadcast",
$"{error}. It's still on YouTube as a private video — you can delete it in YouTube Studio.");
break;
}
}
/// <summary>The keep-or-delete decision for a finished broadcast. Asks ONLY when YouTube
/// itself reports the broadcast as private (visibility can change mid-stream, so local state
/// isn't trusted); an unreadable status asks nothing and deletes nothing. Keep = leave it;
/// otherwise delete the video from YouTube.</summary>
internal static async Task<(PrivateBroadcastOutcome Outcome, string? Error)> ClosePrivateBroadcastAsync(
YouTubeStreamService youtube, string broadcastId, Func<bool> askKeep)
{
var privacy = await youtube.GetBroadcastPrivacyAsync(broadcastId);
if (!string.Equals(privacy, "private", StringComparison.OrdinalIgnoreCase))
return (PrivateBroadcastOutcome.NotPrivate, null);
if (askKeep()) return (PrivateBroadcastOutcome.Kept, null);
var error = await youtube.DeleteFinishedBroadcastAsync(broadcastId);
return error == null ? (PrivateBroadcastOutcome.Deleted, null) : (PrivateBroadcastOutcome.DeleteFailed, error);
}
}