Improve live download status and add debug logging (v3.7.8)
Keep queue visibility synced while downloads run, show richer per-item progress/error states in the sidebar, and write backend downloader diagnostics to ProgramData debug.log so instant start/stop failures can be traced on user systems.
This commit is contained in:
parent
7f208cf369
commit
78378b9812
@ -54,3 +54,11 @@ Release must include all of:
|
||||
- `Twitch-VOD-Manager-Setup-<version>.exe.blockmap`
|
||||
|
||||
Tag version must match app version (example: `v3.7.6`).
|
||||
|
||||
## Debug log for failed downloads
|
||||
|
||||
From `v3.7.8`, detailed downloader logs are written to:
|
||||
|
||||
`C:\ProgramData\Twitch_VOD_Manager\debug.log`
|
||||
|
||||
If a download instantly switches from `Stoppen` back to `Start`, check the latest lines in that file for streamlink exit reasons.
|
||||
|
||||
4
typescript-version/package-lock.json
generated
4
typescript-version/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "3.7.7",
|
||||
"version": "3.7.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "3.7.7",
|
||||
"version": "3.7.8",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twitch-vod-manager",
|
||||
"version": "3.7.7",
|
||||
"version": "3.7.8",
|
||||
"description": "Twitch VOD Manager - Download Twitch VODs easily",
|
||||
"main": "dist/main.js",
|
||||
"author": "xRangerDE",
|
||||
|
||||
@ -335,7 +335,7 @@
|
||||
|
||||
<div class="settings-card">
|
||||
<h3>Updates</h3>
|
||||
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.7.7</p>
|
||||
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v3.7.8</p>
|
||||
<button class="btn-secondary" onclick="checkUpdate()">Nach Updates suchen</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -346,7 +346,7 @@
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span id="statusText">Nicht verbunden</span>
|
||||
</div>
|
||||
<span id="versionText">v3.7.7</span>
|
||||
<span id="versionText">v3.7.8</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -8,13 +8,14 @@ import { autoUpdater } from 'electron-updater';
|
||||
// ==========================================
|
||||
// CONFIG & CONSTANTS
|
||||
// ==========================================
|
||||
const APP_VERSION = '3.7.7';
|
||||
const APP_VERSION = '3.7.8';
|
||||
const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
|
||||
|
||||
// Paths
|
||||
const APPDATA_DIR = path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'Twitch_VOD_Manager');
|
||||
const CONFIG_FILE = path.join(APPDATA_DIR, 'config.json');
|
||||
const QUEUE_FILE = path.join(APPDATA_DIR, 'download_queue.json');
|
||||
const DEBUG_LOG_FILE = path.join(APPDATA_DIR, 'debug.log');
|
||||
const DEFAULT_DOWNLOAD_PATH = path.join(app.getPath('desktop'), 'Twitch_VODs');
|
||||
|
||||
// Timeouts
|
||||
@ -74,9 +75,15 @@ interface QueueItem {
|
||||
eta?: string;
|
||||
downloadedBytes?: number;
|
||||
totalBytes?: number;
|
||||
last_error?: string;
|
||||
customClip?: CustomClip;
|
||||
}
|
||||
|
||||
interface DownloadResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface DownloadProgress {
|
||||
id: string;
|
||||
progress: number;
|
||||
@ -225,6 +232,18 @@ function getFFprobePath(): string {
|
||||
return path.join(path.dirname(ffmpegPath), ffprobeExe);
|
||||
}
|
||||
|
||||
function appendDebugLog(message: string, details?: unknown): void {
|
||||
try {
|
||||
const ts = new Date().toISOString();
|
||||
const payload = details === undefined
|
||||
? ''
|
||||
: ` | ${typeof details === 'string' ? details : JSON.stringify(details)}`;
|
||||
fs.appendFileSync(DEBUG_LOG_FILE, `[${ts}] ${message}${payload}\n`);
|
||||
} catch {
|
||||
// ignore debug log errors
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// DURATION HELPERS
|
||||
// ==========================================
|
||||
@ -756,10 +775,11 @@ function downloadVODPart(
|
||||
itemId: string,
|
||||
partNum: number,
|
||||
totalParts: number
|
||||
): Promise<boolean> {
|
||||
): Promise<DownloadResult> {
|
||||
return new Promise((resolve) => {
|
||||
const streamlinkPath = getStreamlinkPath();
|
||||
const args = [url, 'best', '-o', filename, '--force'];
|
||||
let lastErrorLine = '';
|
||||
|
||||
if (startTime) {
|
||||
args.push('--hls-start-offset', startTime);
|
||||
@ -769,6 +789,7 @@ function downloadVODPart(
|
||||
}
|
||||
|
||||
console.log('Starting download:', streamlinkPath, args);
|
||||
appendDebugLog('download-part-start', { itemId, streamlinkPath, filename, args });
|
||||
|
||||
const proc = spawn(streamlinkPath, args, { windowsHide: true });
|
||||
currentProcess = proc;
|
||||
@ -828,7 +849,12 @@ function downloadVODPart(
|
||||
});
|
||||
|
||||
proc.stderr?.on('data', (data: Buffer) => {
|
||||
console.error('Streamlink error:', data.toString());
|
||||
const message = data.toString().trim();
|
||||
if (message) {
|
||||
lastErrorLine = message.split('\n').pop() || message;
|
||||
appendDebugLog('download-part-stderr', { itemId, message: lastErrorLine });
|
||||
console.error('Streamlink error:', message);
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
@ -836,26 +862,36 @@ function downloadVODPart(
|
||||
currentProcess = null;
|
||||
|
||||
if (currentDownloadCancelled) {
|
||||
resolve(false);
|
||||
appendDebugLog('download-part-cancelled', { itemId, filename });
|
||||
resolve({ success: false, error: 'Download wurde abgebrochen.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (code === 0 && fs.existsSync(filename)) {
|
||||
const stats = fs.statSync(filename);
|
||||
if (stats.size > 1024 * 1024) {
|
||||
resolve(true);
|
||||
appendDebugLog('download-part-success', { itemId, filename, bytes: stats.size });
|
||||
resolve({ success: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const tooSmall = `Datei zu klein (${stats.size} Bytes)`;
|
||||
appendDebugLog('download-part-failed-small-file', { itemId, filename, bytes: stats.size });
|
||||
resolve({ success: false, error: tooSmall });
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(false);
|
||||
const genericError = lastErrorLine || `Streamlink Exit-Code ${code ?? -1}`;
|
||||
appendDebugLog('download-part-failed', { itemId, filename, code, error: genericError });
|
||||
resolve({ success: false, error: genericError });
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
clearInterval(progressInterval);
|
||||
console.error('Process error:', err);
|
||||
currentProcess = null;
|
||||
resolve(false);
|
||||
appendDebugLog('download-part-process-error', { itemId, error: String(err) });
|
||||
resolve({ success: false, error: String(err) });
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -863,7 +899,7 @@ function downloadVODPart(
|
||||
async function downloadVOD(
|
||||
item: QueueItem,
|
||||
onProgress: (progress: DownloadProgress) => void
|
||||
): Promise<boolean> {
|
||||
): Promise<DownloadResult> {
|
||||
const streamer = item.streamer.replace(/[^a-zA-Z0-9_-]/g, '');
|
||||
const date = new Date(item.date);
|
||||
const dateStr = `${date.getDate().toString().padStart(2, '0')}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getFullYear()}`;
|
||||
@ -907,7 +943,7 @@ async function downloadVOD(
|
||||
|
||||
const partFilename = makeClipFilename(partNum, startOffset);
|
||||
|
||||
const success = await downloadVODPart(
|
||||
const result = await downloadVODPart(
|
||||
item.url,
|
||||
partFilename,
|
||||
formatDuration(startOffset),
|
||||
@ -918,11 +954,14 @@ async function downloadVOD(
|
||||
numParts
|
||||
);
|
||||
|
||||
if (!success) return false;
|
||||
if (!result.success) return result;
|
||||
downloadedFiles.push(partFilename);
|
||||
}
|
||||
|
||||
return downloadedFiles.length === numParts;
|
||||
return {
|
||||
success: downloadedFiles.length === numParts,
|
||||
error: downloadedFiles.length === numParts ? undefined : 'Nicht alle Clip-Teile konnten heruntergeladen werden.'
|
||||
};
|
||||
} else {
|
||||
// Single clip file
|
||||
const filename = makeClipFilename(clip.startPart, clip.startSec);
|
||||
@ -959,7 +998,7 @@ async function downloadVOD(
|
||||
|
||||
const partFilename = path.join(folder, `${dateStr}_Part${(i + 1).toString().padStart(2, '0')}.mp4`);
|
||||
|
||||
const success = await downloadVODPart(
|
||||
const result = await downloadVODPart(
|
||||
item.url,
|
||||
partFilename,
|
||||
formatDuration(startSec),
|
||||
@ -970,20 +1009,24 @@ async function downloadVOD(
|
||||
numParts
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
return false;
|
||||
if (!result.success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
downloadedFiles.push(partFilename);
|
||||
}
|
||||
|
||||
return downloadedFiles.length === numParts;
|
||||
return {
|
||||
success: downloadedFiles.length === numParts,
|
||||
error: downloadedFiles.length === numParts ? undefined : 'Nicht alle Teile konnten heruntergeladen werden.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function processQueue(): Promise<void> {
|
||||
if (isDownloading || downloadQueue.length === 0) return;
|
||||
|
||||
appendDebugLog('queue-start', { items: downloadQueue.length });
|
||||
isDownloading = true;
|
||||
mainWindow?.webContents.send('download-started');
|
||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||
@ -992,17 +1035,27 @@ async function processQueue(): Promise<void> {
|
||||
if (!isDownloading) break;
|
||||
if (item.status === 'completed') continue;
|
||||
|
||||
appendDebugLog('queue-item-start', { itemId: item.id, title: item.title, url: item.url });
|
||||
|
||||
currentDownloadCancelled = false;
|
||||
item.status = 'downloading';
|
||||
saveQueue(downloadQueue);
|
||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||
|
||||
const success = await downloadVOD(item, (progress) => {
|
||||
item.last_error = '';
|
||||
|
||||
const result = await downloadVOD(item, (progress) => {
|
||||
mainWindow?.webContents.send('download-progress', progress);
|
||||
});
|
||||
|
||||
item.status = success ? 'completed' : 'error';
|
||||
item.progress = success ? 100 : 0;
|
||||
item.status = result.success ? 'completed' : 'error';
|
||||
item.progress = result.success ? 100 : 0;
|
||||
item.last_error = result.success ? '' : (result.error || 'Unbekannter Fehler beim Download');
|
||||
appendDebugLog('queue-item-finished', {
|
||||
itemId: item.id,
|
||||
status: item.status,
|
||||
error: item.last_error
|
||||
});
|
||||
saveQueue(downloadQueue);
|
||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||
}
|
||||
@ -1011,6 +1064,7 @@ async function processQueue(): Promise<void> {
|
||||
saveQueue(downloadQueue);
|
||||
mainWindow?.webContents.send('queue-updated', downloadQueue);
|
||||
mainWindow?.webContents.send('download-finished');
|
||||
appendDebugLog('queue-finished', { items: downloadQueue.length });
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
|
||||
2
typescript-version/src/renderer-globals.d.ts
vendored
2
typescript-version/src/renderer-globals.d.ts
vendored
@ -42,6 +42,8 @@ interface QueueItem {
|
||||
eta?: string;
|
||||
downloadedBytes?: number;
|
||||
totalBytes?: number;
|
||||
progressStatus?: string;
|
||||
last_error?: string;
|
||||
customClip?: CustomClip;
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,59 @@ async function clearCompleted(): Promise<void> {
|
||||
renderQueue();
|
||||
}
|
||||
|
||||
function getQueueStatusLabel(item: QueueItem): string {
|
||||
if (item.status === 'completed') return 'Abgeschlossen';
|
||||
if (item.status === 'error') return 'Fehlgeschlagen';
|
||||
if (item.status === 'downloading') return 'Lauft';
|
||||
return 'Wartet';
|
||||
}
|
||||
|
||||
function getQueueProgressText(item: QueueItem): string {
|
||||
if (item.status === 'completed') return '100%';
|
||||
if (item.status === 'error') return 'Fehler';
|
||||
if (item.status === 'pending') return 'Bereit';
|
||||
if (item.progress > 0) return `${Math.max(0, Math.min(100, item.progress)).toFixed(1)}%`;
|
||||
return item.progressStatus || 'Lade...';
|
||||
}
|
||||
|
||||
function getQueueMetaText(item: QueueItem): string {
|
||||
if (item.status === 'error' && item.last_error) {
|
||||
return item.last_error;
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (item.currentPart && item.totalParts) {
|
||||
parts.push(`Teil ${item.currentPart}/${item.totalParts}`);
|
||||
}
|
||||
|
||||
if (item.speed) {
|
||||
parts.push(item.speed);
|
||||
}
|
||||
|
||||
if (item.eta) {
|
||||
parts.push(`ETA ${item.eta}`);
|
||||
}
|
||||
|
||||
if (!parts.length && item.status === 'pending') {
|
||||
parts.push('Bereit zum Download');
|
||||
}
|
||||
|
||||
if (!parts.length && item.status === 'downloading') {
|
||||
parts.push(item.progressStatus || 'Download gestartet');
|
||||
}
|
||||
|
||||
if (!parts.length && item.status === 'completed') {
|
||||
parts.push('Fertig');
|
||||
}
|
||||
|
||||
if (!parts.length && item.status === 'error') {
|
||||
parts.push('Download fehlgeschlagen');
|
||||
}
|
||||
|
||||
return parts.join(' | ');
|
||||
}
|
||||
|
||||
function renderQueue(): void {
|
||||
if (!Array.isArray(queue)) {
|
||||
queue = [];
|
||||
@ -34,11 +87,30 @@ function renderQueue(): void {
|
||||
|
||||
list.innerHTML = queue.map((item: QueueItem) => {
|
||||
const safeTitle = escapeHtml(item.title || 'Untitled');
|
||||
const safeStatusLabel = escapeHtml(getQueueStatusLabel(item));
|
||||
const safeProgressText = escapeHtml(getQueueProgressText(item));
|
||||
const safeMeta = escapeHtml(getQueueMetaText(item));
|
||||
const isClip = item.customClip ? '* ' : '';
|
||||
const hasDeterminateProgress = item.progress > 0 && item.progress <= 100;
|
||||
const progressValue = item.status === 'completed'
|
||||
? 100
|
||||
: (hasDeterminateProgress ? Math.max(0, Math.min(100, item.progress)) : 0);
|
||||
const progressClass = item.status === 'downloading' && !hasDeterminateProgress ? ' indeterminate' : '';
|
||||
|
||||
return `
|
||||
<div class="queue-item">
|
||||
<div class="status ${item.status}"></div>
|
||||
<div class="queue-main">
|
||||
<div class="queue-title-row">
|
||||
<div class="title" title="${safeTitle}">${isClip}${safeTitle}</div>
|
||||
<div class="queue-status-label">${safeStatusLabel}</div>
|
||||
</div>
|
||||
<div class="queue-meta">${safeMeta}</div>
|
||||
<div class="queue-progress-wrap">
|
||||
<div class="queue-progress-bar${progressClass}" style="width: ${progressValue}%;"></div>
|
||||
</div>
|
||||
<div class="queue-progress-text">${safeProgressText}</div>
|
||||
</div>
|
||||
<span class="remove" onclick="removeFromQueue('${item.id}')">x</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -31,7 +31,15 @@ async function init(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
item.status = 'downloading';
|
||||
item.progress = progress.progress;
|
||||
item.speed = progress.speed;
|
||||
item.eta = progress.eta;
|
||||
item.currentPart = progress.currentPart;
|
||||
item.totalParts = progress.totalParts;
|
||||
item.downloadedBytes = progress.downloadedBytes;
|
||||
item.totalBytes = progress.totalBytes;
|
||||
item.progressStatus = progress.status;
|
||||
renderQueue();
|
||||
});
|
||||
|
||||
|
||||
@ -195,13 +195,13 @@ body {
|
||||
}
|
||||
|
||||
.queue-list {
|
||||
max-height: 150px;
|
||||
max-height: 210px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.queue-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 8px;
|
||||
background: var(--bg-card);
|
||||
@ -234,6 +234,63 @@ body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.queue-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.queue-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.queue-status-label {
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.queue-meta {
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 2px;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.queue-progress-wrap {
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
background: rgba(255,255,255,0.12);
|
||||
}
|
||||
|
||||
.queue-progress-bar {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: var(--accent);
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.queue-progress-bar.indeterminate {
|
||||
width: 35% !important;
|
||||
animation: queue-indeterminate 1.3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.queue-progress-text {
|
||||
margin-top: 3px;
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
@keyframes queue-indeterminate {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(280%); }
|
||||
}
|
||||
|
||||
.queue-item .remove {
|
||||
cursor: pointer;
|
||||
color: var(--error);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user