Twitch-VOD-Manager/src/renderer-globals.d.ts
xRangerDE 029b2bd407 feat: auto-record polling — set-and-forget live archival
Building on the manual REC button from 4.6.0: each streamer now also
has an AUTO toggle. When enabled, a background poller in the main
process checks the streamers live status every 90s (configurable
30-1800s via config.auto_record_poll_seconds). On an offline -> live
transition, a live recording is queued automatically without the
user having to be at the keyboard.

Server:
- config.auto_record_streamers: string[] holds the watched logins
  (deduped + normalized via normalizeAutoRecordList). Empty list
  stops the poller entirely so users who don't use the feature pay
  zero CPU.
- runAutoRecordPoll iterates the list, hits getLiveStreamInfo
  (existing helper from 4.6.0 — Helix when authed, public GQL
  otherwise), tracks per-streamer last-known live state in
  autoRecordLastLiveState, and only triggers on the offline->live
  edge. If a live item already exists for that streamer (manual
  REC click + auto-poll racing), the auto-trigger backs off.
- restartAutoRecordPoller is wired into save-config so toggling AUTO
  on/off or changing the interval takes effect without a restart;
  state for de-watched streamers is dropped so re-enabling them
  later doesn't suppress an immediate first-poll trigger.
- Wired into app.whenReady (start) and shutdownCleanup (stop).
- Initial poll fires ~1.5s after restart so a streamer that's
  already live when the user enables AUTO gets picked up
  immediately instead of after a full interval.

Renderer:
- AUTO pill next to REC. Off = grey outline, on = green outline +
  green text + faint green background. Click toggles via saveConfig
  with the updated auto_record_streamers array; toast confirms.
- Per-streamer state survives reload (it's in the config file).

DE + EN locale strings for the toggle title + on/off toasts.

Why this matters: VODs vanish from Twitch within 7-60 days. Manual
REC requires the user to be present when the stream starts. AUTO
closes that gap — the app watches in the background and captures
without supervision.

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

250 lines
8.1 KiB
TypeScript

interface AppConfig {
client_id?: string;
client_secret?: string;
download_path?: string;
streamers?: string[];
theme?: string;
download_mode?: 'parts' | 'full';
part_minutes?: number;
language?: 'de' | 'en';
filename_template_vod?: string;
filename_template_parts?: string;
filename_template_clip?: string;
smart_queue_scheduler?: boolean;
performance_mode?: 'stability' | 'balanced' | 'speed';
prevent_duplicate_downloads?: boolean;
persist_queue_on_restart?: boolean;
metadata_cache_minutes?: number;
parallel_downloads?: number;
auto_resume_queue_on_startup?: boolean;
downloaded_vod_ids?: string[];
streamlink_quality?: string;
notify_on_each_completion?: boolean;
streamlink_disable_ads?: boolean;
auto_record_streamers?: string[];
auto_record_poll_seconds?: number;
[key: string]: unknown;
}
interface VOD {
id: string;
title: string;
created_at: string;
duration: string;
thumbnail_url: string;
url: string;
view_count: number;
stream_id?: string;
}
interface CustomClip {
startSec: number;
durationSec: number;
startPart: number;
filenameFormat: 'simple' | 'timestamp' | 'template' | 'parts';
filenameTemplate?: string;
}
interface MergeGroupItem {
url: string;
title: string;
date: string;
streamer: string;
duration_str: string;
}
interface MergeGroup {
items: MergeGroupItem[];
mergePhase: 'downloading' | 'merging' | 'splitting' | 'cleanup' | 'done';
currentItemIndex: number;
downloadedFiles: Record<number, string>;
mergedFile?: string;
splitFiles?: string[];
totalDurationSec?: number;
}
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;
progressStatus?: string;
last_error?: string;
customClip?: CustomClip;
mergeGroup?: MergeGroup;
outputFiles?: string[];
isLive?: boolean;
}
interface DownloadProgress {
id: string;
progress: number;
speed: string;
speedBytesPerSec?: number;
eta: string;
status: string;
currentPart?: number;
totalParts?: number;
downloadedBytes?: number;
totalBytes?: number;
}
interface RuntimeMetricsSnapshot {
cacheHits: number;
cacheMisses: number;
duplicateSkips: number;
retriesScheduled: number;
retriesExhausted: number;
integrityFailures: number;
downloadsStarted: number;
downloadsCompleted: number;
downloadsFailed: number;
downloadedBytesTotal: number;
lastSpeedBytesPerSec: number;
avgSpeedBytesPerSec: number;
activeItemId: string | null;
activeItemTitle: string | null;
lastErrorClass: string | null;
lastRetryDelaySeconds: number;
timestamp: string;
queue: {
pending: number;
downloading: number;
paused: number;
completed: number;
error: number;
total: number;
};
caches: {
loginToUserId: number;
vodList: number;
clipInfo: number;
};
config: {
performanceMode: 'stability' | 'balanced' | 'speed';
smartScheduler: boolean;
metadataCacheMinutes: number;
duplicatePrevention: boolean;
};
}
interface VideoInfo {
duration: number;
width: number;
height: number;
fps: number;
}
interface ClipDialogData {
url: string;
title: string;
date: string;
streamer: string;
duration: string;
}
interface UpdateInfo {
version: string;
releaseDate?: string;
releaseName?: string;
releaseNotes?: string;
}
interface UpdateDownloadProgress {
percent: number;
bytesPerSecond: number;
transferred: number;
total: number;
}
interface PreflightChecks {
internet: boolean;
streamlink: boolean;
ffmpeg: boolean;
ffprobe: boolean;
downloadPathWritable: boolean;
}
interface PreflightResult {
ok: boolean;
autoFixApplied: boolean;
checks: PreflightChecks;
messages: string[];
timestamp: string;
}
interface ApiBridge {
getConfig(): Promise<AppConfig>;
saveConfig(config: Partial<AppConfig>): Promise<AppConfig>;
login(): Promise<boolean>;
getUserId(username: string): Promise<string | null>;
getVODs(userId: string, forceRefresh?: boolean): Promise<VOD[]>;
getQueue(): Promise<QueueItem[]>;
addToQueue(item: Omit<QueueItem, 'id' | 'status' | 'progress'>): Promise<QueueItem[]>;
startLiveRecording(streamerName: string): Promise<{ success: boolean; error?: string; streamer?: string; title?: string }>;
removeFromQueue(id: string): Promise<QueueItem[]>;
reorderQueue(orderIds: string[]): Promise<QueueItem[]>;
clearCompleted(): Promise<QueueItem[]>;
retryFailedDownloads(): Promise<QueueItem[]>;
retryQueueItem(id: string): Promise<QueueItem[]>;
createMergeGroup(itemIds: string[]): Promise<QueueItem[]>;
startDownload(): Promise<boolean>;
pauseDownload(): Promise<boolean>;
cancelDownload(): Promise<boolean>;
isDownloading(): Promise<boolean>;
downloadClip(url: string): Promise<{ success: boolean; error?: string }>;
selectFolder(): Promise<string | null>;
selectVideoFile(): Promise<string | null>;
selectMultipleVideos(): Promise<string[] | null>;
saveVideoDialog(defaultName: string): Promise<string | null>;
openFolder(path: string): Promise<void>;
openFile(path: string): Promise<boolean>;
showInFolder(path: string): Promise<boolean>;
openDebugLogFile(): Promise<boolean>;
checkFolderWritable(path: string): Promise<boolean>;
getVideoInfo(filePath: string): Promise<VideoInfo | null>;
extractFrame(filePath: string, timeSeconds: number): Promise<string | null>;
cutVideo(inputFile: string, startTime: number, endTime: number): Promise<{ success: boolean; outputFile: string | null }>;
mergeVideos(inputFiles: string[], outputFile: string): Promise<{ success: boolean; outputFile: string | null }>;
getVersion(): Promise<string>;
checkUpdate(): Promise<{ checking?: boolean; error?: boolean; skipped?: 'ready-to-install' | 'in-progress' | 'throttled' | 'error' | string }>;
downloadUpdate(): Promise<{ downloading?: boolean; error?: boolean; skipped?: 'ready-to-install' | 'in-progress' | 'error' | string }>;
installUpdate(): Promise<void>;
openExternal(url: string): Promise<void>;
runPreflight(autoFix: boolean): Promise<PreflightResult>;
getDebugLog(lines: number): Promise<string>;
getRuntimeMetrics(): Promise<RuntimeMetricsSnapshot>;
exportRuntimeMetrics(): Promise<{ success: boolean; cancelled?: boolean; error?: string; filePath?: string }>;
resetDownloadedVodIds(): Promise<{ success: boolean; removedCount: number }>;
markVodDownloaded(vodId: string, mark: boolean): Promise<{ success: boolean }>;
exportConfig(): Promise<{ success: boolean; cancelled?: boolean; error?: string; filePath?: string }>;
importConfig(): Promise<{ success: boolean; cancelled?: boolean; error?: string; filePath?: string }>;
onDownloadProgress(callback: (progress: DownloadProgress) => void): void;
onQueueUpdated(callback: (queue: QueueItem[]) => void): void;
onQueueDuplicateSkipped(callback: (payload: { title: string; streamer: string; url: string }) => void): void;
onDownloadStarted(callback: () => void): void;
onDownloadFinished(callback: () => void): void;
onCutProgress(callback: (percent: number) => void): void;
onMergeProgress(callback: (percent: number) => void): void;
onUpdateChecking(callback: () => void): void;
onUpdateAvailable(callback: (info: UpdateInfo) => void): void;
onUpdateNotAvailable(callback: () => void): void;
onUpdateDownloadProgress(callback: (progress: UpdateDownloadProgress) => void): void;
onUpdateDownloaded(callback: (info: UpdateInfo) => void): void;
onUpdateError(callback: (payload: { message: string }) => void): void;
}
interface Window {
api: ApiBridge;
}