Twitch-VOD-Manager/src/types.ts
xRangerDE 2c40bbf66e feat: live recording health indicator (green/amber dot per item)
In-flight live recordings now show a small coloured dot before the
title indicating whether bytes are still flowing.

The health state is derived from byte-progress liveness: each time
the byte counter advances, we stamp lastBytesAdvancedAt; if more
than 30s pass without an advance we flip the badge to amber to tell
the user the streamlink subprocess has gone quiet (dropped segments,
network blip, or the stream just ended). Until the first segment
arrives we report "unknown" so we don't claim health prematurely on
a streamlink that's still negotiating playlists.

Critical wrinkle: streamlink emits progress events on byte boundaries,
so a hung process emits NO events at all. A pure event-driven badge
would never update from "ok" to "stale" — it'd stay frozen at the
last known good state. To avoid that, downloadLiveStream now runs a
10s health-tick interval that re-emits the most recent progress
event with a fresh health computation. The interval is killed in a
finally block so process termination doesn't leak it.

DownloadProgress + QueueItem in both src/types.ts and the renderer
declaration shadow get the new optional recordingHealth field. The
renderer queue handler copies it onto the item; the queue render
function shows a coloured dot before the title for in-flight live
items only (status === 'downloading' && isLive). Three states:
green pulsing (ok), amber flashing (stale), grey static (unknown).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:04:53 +02:00

82 lines
2.5 KiB
TypeScript

export interface CustomClip {
startSec: number;
durationSec: number;
startPart: number;
filenameFormat: 'simple' | 'timestamp' | 'template' | 'parts';
filenameTemplate?: string;
}
export interface MergeGroupItem {
url: string;
title: string;
date: string;
streamer: string;
duration_str: string;
}
export interface MergeGroup {
items: MergeGroupItem[];
mergePhase: 'downloading' | 'merging' | 'splitting' | 'cleanup' | 'done';
currentItemIndex: number;
downloadedFiles: Record<number, string>;
mergedFile?: string;
splitFiles?: string[];
totalDurationSec?: number;
}
export interface QueueItem {
id: string;
title: string;
url: string;
date: string;
streamer: string;
duration_str: string;
status: 'pending' | 'downloading' | 'paused' | 'completed' | 'error';
progress: number;
currentPart?: number;
totalParts?: number;
speed?: string;
eta?: string;
downloadedBytes?: number;
totalBytes?: number;
last_error?: string;
customClip?: CustomClip;
mergeGroup?: MergeGroup;
// File paths produced by the download (single file for VOD/clip, multiple
// for parts/merge-group splits). Persisted with the queue so completed
// items keep their "Open file" / "Show in folder" actions across restarts.
outputFiles?: string[];
// Live stream recording — when true, item.url is the channel URL
// (https://twitch.tv/{streamer}) and streamlink runs until the stream
// ends instead of using --hls-start-offset / --hls-duration. The output
// filename includes a timestamp so consecutive live recordings of the
// same streamer don't collide.
isLive?: boolean;
// Live recording health snapshot. 'ok' means bytes are flowing within
// the freshness window, 'stale' means the streamlink subprocess hasn't
// pushed bytes recently (dropped segments, network blip, or stream just
// ended), 'unknown' until the first progress event arrives. Only set
// for in-flight live recordings; cleared when the recording finishes.
recordingHealth?: 'ok' | 'stale' | 'unknown';
}
export interface DownloadProgress {
id: string;
progress: number;
speed: string;
speedBytesPerSec?: number;
eta: string;
status: string;
currentPart?: number;
totalParts?: number;
downloadedBytes?: number;
totalBytes?: number;
recordingHealth?: 'ok' | 'stale' | 'unknown';
}
export interface DownloadResult {
success: boolean;
error?: string;
outputFiles?: string[];
}