Twitch-VOD-Manager/src/renderer-globals.d.ts
xRangerDE 3129c9b5be feat: in-app chat replay viewer — read .chat.json/.chat.jsonl without leaving the app
Up to now, the app saved chat data (4.6.2 VOD replay, 4.6.3 live
capture) but had no way to view it — users had to open the file in
Notepad or write a custom parser. New in-app modal closes that loop:
queue items with a sibling .chat.json or .chat.jsonl get a "View
chat" button next to Open file / Show in folder; click pops a modal
with a scrollable, filterable, formatted message list.

Server:
- New ipcMain.handle("read-chat-file") parses both formats. JSON
  Lines (.jsonl) is split per line, header row skipped, malformed
  lines silently dropped — that way a partial / killed live capture
  still renders. JSON object (.json) is the VOD replay shape with
  messages array. Hard-capped at 50k messages so a multi-day archive
  can't kill the renderer; truncation is reported via {truncated,
  total} in the result.

Renderer:
- New chatViewerModal in index.html — full-height list with a filter
  input + status line.
- openChatViewer(filePath, title) loads the file via IPC, normalises
  the message shape (supports both .chat.json and .chat.jsonl
  fields), renders in 500-message chunks via setTimeout(0) so the
  main thread stays responsive on a 30k-message archive.
- Each row: time marker (offset for replays, wall-clock for live),
  user (in their stored color), message text. Non-msg event types
  (subs, raids, clears) get a faint italic [type] tag.
- Filter substring-matches user OR text, case-insensitive, instant.
- Esc + outside-click + the close-x dismiss; Esc handler in
  closeTopmostOpenModal lists the chat viewer first so a user
  with multiple modals open closes the foreground one.

Queue UI:
- renderQueueItemFileActions detects sibling chat files (regex
  /\.chat\.json(l)?$/) in item.outputFiles and surfaces the View
  chat button. The button is shown for both 4.6.2-style replays
  and 4.6.3-style live captures because both formats parse.

DE + EN locales for the button label, loading state, error,
message count, truncation suffix.

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

296 lines
9.6 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;
download_chat_replay?: boolean;
capture_live_chat?: boolean;
discord_webhook_url?: string;
discord_notify_live_start?: boolean;
discord_notify_live_end?: boolean;
discord_notify_vod_complete?: boolean;
auto_cleanup_enabled?: boolean;
auto_cleanup_days?: number;
auto_cleanup_target?: 'live_only' | 'all';
auto_cleanup_action?: 'delete' | 'archive';
log_stream_events?: boolean;
[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 StreamerStorageEntry {
name: string;
fileCount: number;
totalBytes: number;
liveBytes: number;
chatBytes: number;
folderPath: string;
}
interface CleanupReport {
enabled: boolean;
dryRun: boolean;
cutoffDays: number;
target: 'live_only' | 'all';
action: 'delete' | 'archive';
scannedAt: string;
candidates: number;
processed: number;
failed: number;
bytesFreed: number;
failures: Array<{ path: string; error: string }>;
}
interface StorageStatsResult {
downloadPath: string;
rootExists: boolean;
freeBytes: number | null;
totalFiles: number;
totalBytes: number;
streamers: StreamerStorageEntry[];
extras: StreamerStorageEntry[];
scannedAt: 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>;
getStorageStats(): Promise<StorageStatsResult>;
runStorageCleanup(options?: { dryRun?: boolean }): Promise<CleanupReport>;
readChatFile(filePath: string): Promise<{ success: boolean; error?: string; format?: 'replay' | 'live'; messages?: Array<Record<string, unknown>>; truncated?: boolean; total?: number }>;
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;
}