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
+60
View File
@@ -174,6 +174,66 @@ public class YouTubeStreamService
return $"YouTube rejected the end transition ({(int)response.StatusCode})";
}
/// <summary>The broadcast's current <c>status.privacyStatus</c> ("private", "unlisted",
/// "public"), read from YouTube rather than trusted from local state — the creator can
/// change visibility mid-stream from the broadcast form. Null when signed out or the
/// read fails (the caller then does nothing destructive).</summary>
public async Task<string?> GetBroadcastPrivacyAsync(string broadcastId)
{
if (!await EnsureToken()) return null;
_http.DefaultRequestHeaders.Authorization = new("Bearer", _auth.CurrentChannel!.AccessToken);
try
{
var response = await _http.GetAsync(
$"{ApiBase}/liveBroadcasts?part=status&id={Uri.EscapeDataString(broadcastId)}");
if (!response.IsSuccessStatusCode) return null;
var json = JsonSerializer.Deserialize<JsonElement>(await response.Content.ReadAsStringAsync());
if (!json.TryGetProperty("items", out var items) || items.GetArrayLength() == 0) return null;
if (!items[0].TryGetProperty("status", out var status)) return null;
return status.TryGetProperty("privacyStatus", out var privacy) ? privacy.GetString() : null;
}
catch (Exception ex)
{
AppLog.Write($"Broadcast privacy read failed: {ex.Message}");
return null;
}
}
/// <summary>Deletes a finished broadcast's recording from YouTube (the creator pressed
/// Escape / Delete on the keep-or-delete prompt). A live broadcast's id IS its video's
/// id, so this is <c>videos.delete</c> (Data API, 204 on success); if YouTube hasn't
/// created the video yet (404) it falls back to <c>liveBroadcasts.delete</c>. Returns
/// null on success, or an error string — never throws.
/// Docs: https://developers.google.com/youtube/v3/docs/videos/delete ,
/// https://developers.google.com/youtube/v3/live/docs/liveBroadcasts/delete</summary>
public async Task<string?> DeleteFinishedBroadcastAsync(string broadcastId)
{
if (!await EnsureToken()) return "not signed in";
_http.DefaultRequestHeaders.Authorization = new("Bearer", _auth.CurrentChannel!.AccessToken);
try
{
var id = Uri.EscapeDataString(broadcastId);
var video = await _http.DeleteAsync($"{ApiBase}/videos?id={id}");
if (video.IsSuccessStatusCode) return null;
if (video.StatusCode == System.Net.HttpStatusCode.NotFound)
{
var broadcast = await _http.DeleteAsync($"{ApiBase}/liveBroadcasts?id={id}");
if (broadcast.IsSuccessStatusCode) return null;
var bBody = await broadcast.Content.ReadAsStringAsync();
AppLog.Write($"liveBroadcasts.delete failed ({(int)broadcast.StatusCode}): {bBody}");
return $"YouTube refused to delete the broadcast ({(int)broadcast.StatusCode})";
}
var body = await video.Content.ReadAsStringAsync();
AppLog.Write($"videos.delete failed ({(int)video.StatusCode}): {body}");
return $"YouTube refused to delete the video ({(int)video.StatusCode})";
}
catch (Exception ex)
{
AppLog.Write($"Broadcast delete failed: {ex.Message}");
return ex.Message;
}
}
/// <summary>One liveBroadcasts.list (part=status) read of the broadcast's own
/// lifeCycleStatus — the only reliable "can we complete?" signal. Null when the
/// list fails or the status is missing (caller degrades gracefully).</summary>