VODs disappear from Twitch after 7-60 days depending on the channel
partnership tier. Anyone serious about archiving needs to capture
streams while they are still live, not after. The downloader is now
a recorder too.
End-user surface:
- Each streamer in the sidebar has a small red "REC" pill next to
the remove-x. Click it -> server checks Helix (or public GQL when
no client_id is configured) for live status. If the channel is
online a new queue item is added with isLive: true, status:
pending; the existing queue scheduler picks it up. Toast feedback
for offline / already-recording / generic-failure cases.
- Live items render with a pulsing red REC badge in the queue title
row and skip the bulk-select checkbox + the merge-group selector
(they don't make sense for an open-ended capture).
- Output goes to {download_path}/{streamer}/live/
{streamer}_LIVE_{YYYY-MM-DD}_{HH-mm-ss}.mp4 — timestamped so back-
to-back recordings of the same channel never collide.
- Streamlink runs without --hls-start-offset / --hls-duration so it
records until the stream actually ends or the user hits cancel /
remove. The existing per-item filename claim, integrity check on
close, and downloaded_vod_ids tracking apply unchanged (live
recordings are not added to downloaded_vod_ids since they have
no Twitch VOD ID).
Server plumbing:
- New getLiveStreamInfo(login) helper. Helix /streams when an app
token is available (better metadata: title + game), public GQL
fallback otherwise so users in public-mode still get live status.
- New IPC start-live-recording(streamerName) does the live check,
refuses with ALREADY_RECORDING if a live item for the same
channel is already pending or downloading.
- downloadVOD branches into a small downloadLiveStream helper when
item.isLive — computes the timestamped filename, ensures the
per-streamer/live folder exists, hands off to downloadVODPart
with null start/end times.
- sanitizeQueueItem preserves the isLive flag across queue file
reload so a recording in progress survives an app restart in
state (though streamlink itself dies on app exit and the user
has to re-trigger).
DE + EN locale strings for every toast + tooltip + the queue badge.
CSS animation for the pulsing badge so it visually distinguishes
live recordings from regular VOD downloads at a glance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
75 lines
2.1 KiB
TypeScript
75 lines
2.1 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;
|
|
}
|
|
|
|
export interface DownloadProgress {
|
|
id: string;
|
|
progress: number;
|
|
speed: string;
|
|
speedBytesPerSec?: number;
|
|
eta: string;
|
|
status: string;
|
|
currentPart?: number;
|
|
totalParts?: number;
|
|
downloadedBytes?: number;
|
|
totalBytes?: number;
|
|
}
|
|
|
|
export interface DownloadResult {
|
|
success: boolean;
|
|
error?: string;
|
|
outputFiles?: string[];
|
|
}
|