From 3695c096ba640f574f5e78cfbb92699801abc679 Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Sat, 21 Feb 2026 00:16:47 +0100 Subject: [PATCH] Smooth auto-updater flow with background downloads and status events (v4.1.9) --- typescript-version/package-lock.json | 4 +- typescript-version/package.json | 2 +- typescript-version/src/index.html | 4 +- typescript-version/src/main.ts | 53 ++++- typescript-version/src/preload.ts | 9 + typescript-version/src/renderer-globals.d.ts | 5 +- typescript-version/src/renderer-locale-de.ts | 6 + typescript-version/src/renderer-locale-en.ts | 6 + typescript-version/src/renderer-updates.ts | 201 ++++++++++++++++--- 9 files changed, 250 insertions(+), 40 deletions(-) diff --git a/typescript-version/package-lock.json b/typescript-version/package-lock.json index e4b6f2e..2fa9959 100644 --- a/typescript-version/package-lock.json +++ b/typescript-version/package-lock.json @@ -1,12 +1,12 @@ { "name": "twitch-vod-manager", - "version": "4.1.8", + "version": "4.1.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twitch-vod-manager", - "version": "4.1.8", + "version": "4.1.9", "license": "MIT", "dependencies": { "axios": "^1.6.0", diff --git a/typescript-version/package.json b/typescript-version/package.json index 3ffe8d6..bb39ab3 100644 --- a/typescript-version/package.json +++ b/typescript-version/package.json @@ -1,6 +1,6 @@ { "name": "twitch-vod-manager", - "version": "4.1.8", + "version": "4.1.9", "description": "Twitch VOD Manager - Download Twitch VODs easily", "main": "dist/main.js", "author": "xRangerDE", diff --git a/typescript-version/src/index.html b/typescript-version/src/index.html index a50e466..1a34d13 100644 --- a/typescript-version/src/index.html +++ b/typescript-version/src/index.html @@ -457,7 +457,7 @@

Updates

-

Version: v4.1.8

+

Version: v4.1.9

@@ -502,7 +502,7 @@
Nicht verbunden - v4.1.8 + v4.1.9 diff --git a/typescript-version/src/main.ts b/typescript-version/src/main.ts index cd38aea..e26810c 100644 --- a/typescript-version/src/main.ts +++ b/typescript-version/src/main.ts @@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater'; // ========================================== // CONFIG & CONSTANTS // ========================================== -const APP_VERSION = '4.1.8'; +const APP_VERSION = '4.1.9'; const UPDATE_CHECK_URL = 'http://24-music.de/version.json'; // Paths @@ -33,6 +33,7 @@ const DEBUG_LOG_BUFFER_FLUSH_LINES = 48; const AUTO_UPDATE_CHECK_INTERVAL_MS = 10 * 60 * 1000; const AUTO_UPDATE_STARTUP_CHECK_DELAY_MS = 5000; const AUTO_UPDATE_MIN_CHECK_GAP_MS = 45 * 1000; +const AUTO_UPDATE_AUTO_DOWNLOAD = true; const CACHE_CLEANUP_INTERVAL_MS = 60 * 1000; const MAX_LOGIN_TO_USER_ID_CACHE_ENTRIES = 4096; const MAX_VOD_LIST_CACHE_ENTRIES = 512; @@ -47,6 +48,7 @@ const TWITCH_WEB_CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko'; type PerformanceMode = 'stability' | 'balanced' | 'speed'; type RetryErrorClass = 'network' | 'rate_limit' | 'auth' | 'tooling' | 'integrity' | 'io' | 'validation' | 'unknown'; type UpdateCheckSource = 'startup' | 'interval' | 'manual'; +type UpdateDownloadSource = 'auto' | 'manual'; // Ensure directories exist if (!fs.existsSync(APPDATA_DIR)) { @@ -395,6 +397,7 @@ let autoUpdateCheckTimer: NodeJS.Timeout | null = null; let autoUpdateStartupTimer: NodeJS.Timeout | null = null; let autoUpdateCheckInProgress = false; let autoUpdateReadyToInstall = false; +let autoUpdateDownloadInProgress = false; let lastAutoUpdateCheckAt = 0; let twitchLoginInFlight: Promise | null = null; @@ -2892,6 +2895,30 @@ async function requestUpdateCheck(source: UpdateCheckSource, force = false): Pro } } +async function requestUpdateDownload(source: UpdateDownloadSource): Promise<{ started: boolean; reason?: string }> { + if (autoUpdateReadyToInstall) { + return { started: false, reason: 'ready-to-install' }; + } + + if (autoUpdateDownloadInProgress) { + return { started: false, reason: 'in-progress' }; + } + + autoUpdateDownloadInProgress = true; + appendDebugLog('update-download-start', { source }); + + try { + await autoUpdater.downloadUpdate(); + return { started: true }; + } catch (err) { + appendDebugLog('update-download-failed', { source, error: String(err) }); + console.error('Download failed:', err); + return { started: false, reason: 'error' }; + } finally { + autoUpdateDownloadInProgress = false; + } +} + function stopAutoUpdatePolling(): void { if (autoUpdateCheckTimer) { clearInterval(autoUpdateCheckTimer); @@ -2936,21 +2963,28 @@ function setupAutoUpdater() { autoUpdater.on('checking-for-update', () => { console.log('Checking for updates...'); + mainWindow?.webContents.send('update-checking'); }); autoUpdater.on('update-available', (info) => { console.log('Update available:', info.version); autoUpdateReadyToInstall = false; + autoUpdateDownloadInProgress = false; if (mainWindow) { mainWindow.webContents.send('update-available', { version: info.version, releaseDate: info.releaseDate }); } + + if (AUTO_UPDATE_AUTO_DOWNLOAD) { + void requestUpdateDownload('auto'); + } }); autoUpdater.on('update-not-available', () => { console.log('No updates available'); + mainWindow?.webContents.send('update-not-available'); }); autoUpdater.on('download-progress', (progress) => { @@ -2968,6 +3002,7 @@ function setupAutoUpdater() { autoUpdater.on('update-downloaded', (info) => { console.log('Update downloaded:', info.version); autoUpdateReadyToInstall = true; + autoUpdateDownloadInProgress = false; if (mainWindow) { mainWindow.webContents.send('update-downloaded', { version: info.version @@ -2977,6 +3012,10 @@ function setupAutoUpdater() { autoUpdater.on('error', (err) => { autoUpdateCheckInProgress = false; + autoUpdateDownloadInProgress = false; + const message = String(err); + appendDebugLog('auto-updater-error', message); + mainWindow?.webContents.send('update-error', { message }); console.error('Auto-updater error:', err); }); @@ -3194,9 +3233,15 @@ ipcMain.handle('check-update', async () => { ipcMain.handle('download-update', async () => { try { - autoUpdateReadyToInstall = false; - await autoUpdater.downloadUpdate(); - return { downloading: true }; + setupAutoUpdater(); + const result = await requestUpdateDownload('manual'); + if (result.reason === 'error') { + return { error: true }; + } + + return result.started + ? { downloading: true } + : { downloading: true, skipped: result.reason }; } catch (err) { console.error('Download failed:', err); return { error: true }; diff --git a/typescript-version/src/preload.ts b/typescript-version/src/preload.ts index d263ab6..33548ed 100644 --- a/typescript-version/src/preload.ts +++ b/typescript-version/src/preload.ts @@ -165,13 +165,22 @@ contextBridge.exposeInMainWorld('api', { }, // Auto-Update Events + onUpdateChecking: (callback: () => void) => { + ipcRenderer.on('update-checking', () => callback()); + }, onUpdateAvailable: (callback: (info: { version: string; releaseDate?: string }) => void) => { ipcRenderer.on('update-available', (_, info) => callback(info)); }, + onUpdateNotAvailable: (callback: () => void) => { + ipcRenderer.on('update-not-available', () => callback()); + }, onUpdateDownloadProgress: (callback: (progress: { percent: number; bytesPerSecond: number; transferred: number; total: number }) => void) => { ipcRenderer.on('update-download-progress', (_, progress) => callback(progress)); }, onUpdateDownloaded: (callback: (info: { version: string }) => void) => { ipcRenderer.on('update-downloaded', (_, info) => callback(info)); + }, + onUpdateError: (callback: (payload: { message: string }) => void) => { + ipcRenderer.on('update-error', (_, payload) => callback(payload)); } }); diff --git a/typescript-version/src/renderer-globals.d.ts b/typescript-version/src/renderer-globals.d.ts index 23e9cf6..5604cf4 100644 --- a/typescript-version/src/renderer-globals.d.ts +++ b/typescript-version/src/renderer-globals.d.ts @@ -179,7 +179,7 @@ interface ApiBridge { mergeVideos(inputFiles: string[], outputFile: string): Promise<{ success: boolean; outputFile: string | null }>; getVersion(): Promise; checkUpdate(): Promise<{ checking?: boolean; error?: boolean; skipped?: 'ready-to-install' | 'in-progress' | 'throttled' | 'error' | string }>; - downloadUpdate(): Promise<{ downloading?: boolean; error?: boolean }>; + downloadUpdate(): Promise<{ downloading?: boolean; error?: boolean; skipped?: 'ready-to-install' | 'in-progress' | 'error' | string }>; installUpdate(): Promise; openExternal(url: string): Promise; runPreflight(autoFix: boolean): Promise; @@ -193,9 +193,12 @@ interface ApiBridge { 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 { diff --git a/typescript-version/src/renderer-locale-de.ts b/typescript-version/src/renderer-locale-de.ts index ffee6e6..53378cd 100644 --- a/typescript-version/src/renderer-locale-de.ts +++ b/typescript-version/src/renderer-locale-de.ts @@ -197,7 +197,13 @@ const UI_TEXT_DE = { updates: { bannerDefault: 'Neue Version verfugbar!', latest: 'Du hast die neueste Version!', + checking: 'Suche nach Updates...', + checkInProgress: 'Update-Prufung lauft bereits.', + readyToInstall: 'Update ist bereit zur Installation.', + checkFailed: 'Update-Prufung fehlgeschlagen.', downloading: 'Wird heruntergeladen...', + downloadInProgress: 'Update-Download lauft bereits.', + downloadFailed: 'Update-Download fehlgeschlagen.', available: 'verfugbar!', downloadNow: 'Jetzt herunterladen', downloadLabel: 'Download', diff --git a/typescript-version/src/renderer-locale-en.ts b/typescript-version/src/renderer-locale-en.ts index 85d436d..d8ee96c 100644 --- a/typescript-version/src/renderer-locale-en.ts +++ b/typescript-version/src/renderer-locale-en.ts @@ -197,7 +197,13 @@ const UI_TEXT_EN = { updates: { bannerDefault: 'New version available!', latest: 'You are on the latest version!', + checking: 'Checking for updates...', + checkInProgress: 'Update check is already running.', + readyToInstall: 'Update is ready to install.', + checkFailed: 'Update check failed.', downloading: 'Downloading...', + downloadInProgress: 'Update download is already running.', + downloadFailed: 'Update download failed.', available: 'available!', downloadNow: 'Download now', downloadLabel: 'Download', diff --git a/typescript-version/src/renderer-updates.ts b/typescript-version/src/renderer-updates.ts index 375bec4..9016bdb 100644 --- a/typescript-version/src/renderer-updates.ts +++ b/typescript-version/src/renderer-updates.ts @@ -1,24 +1,99 @@ +let updateCheckInProgress = false; +let updateDownloadInProgress = false; +let manualUpdateCheckPending = false; +let latestUpdateVersion = ''; + +function notifyUpdate(message: string, type: 'info' | 'warn' = 'info'): void { + const toastFn = (window as unknown as { showAppToast?: (msg: string, kind?: 'info' | 'warn') => void }).showAppToast; + if (typeof toastFn === 'function') { + toastFn(message, type); + } else if (type === 'warn') { + alert(message); + } +} + +function setCheckButtonCheckingState(enabled: boolean): void { + const btn = byId('checkUpdateBtn'); + btn.disabled = enabled; + btn.textContent = enabled ? UI_TEXT.updates.checking : UI_TEXT.static.checkUpdates; +} + +function showUpdateBanner(): void { + byId('updateBanner').style.display = 'flex'; +} + +function setDownloadPendingUi(): void { + showUpdateBanner(); + const button = byId('updateButton'); + button.textContent = UI_TEXT.updates.downloading; + button.disabled = true; + byId('updateProgress').style.display = 'block'; + const bar = byId('updateProgressBar'); + bar.classList.add('downloading'); + bar.style.width = '30%'; +} + +function setDownloadReadyUi(version: string): void { + showUpdateBanner(); + updateReady = true; + updateDownloadInProgress = false; + latestUpdateVersion = version || latestUpdateVersion; + + const bar = byId('updateProgressBar'); + bar.classList.remove('downloading'); + bar.style.width = '100%'; + + byId('updateText').textContent = `Version ${latestUpdateVersion || '?'} ${UI_TEXT.updates.ready}`; + const button = byId('updateButton'); + button.textContent = UI_TEXT.updates.installNow; + button.disabled = false; +} + async function checkUpdateSilent(): Promise { - await window.api.checkUpdate(); + try { + await window.api.checkUpdate(); + } catch { + // ignore silent updater errors + } } async function checkUpdate(): Promise { - const result = await window.api.checkUpdate(); + manualUpdateCheckPending = true; + setCheckButtonCheckingState(true); - if (result?.error) { - return; - } + try { + const result = await window.api.checkUpdate(); - const skippedReason = result?.skipped; - if (skippedReason === 'in-progress' || skippedReason === 'ready-to-install' || skippedReason === 'throttled') { - return; - } - - setTimeout(() => { - if (byId('updateBanner').style.display !== 'flex') { - alert(UI_TEXT.updates.latest); + if (result?.error) { + manualUpdateCheckPending = false; + updateCheckInProgress = false; + setCheckButtonCheckingState(false); + notifyUpdate(UI_TEXT.updates.checkFailed, 'warn'); + return; } - }, 2000); + + const skippedReason = result?.skipped; + if (skippedReason === 'ready-to-install') { + manualUpdateCheckPending = false; + updateCheckInProgress = false; + setCheckButtonCheckingState(false); + notifyUpdate(UI_TEXT.updates.readyToInstall, 'info'); + return; + } + + if (skippedReason === 'in-progress' || skippedReason === 'throttled') { + manualUpdateCheckPending = false; + updateCheckInProgress = false; + setCheckButtonCheckingState(false); + notifyUpdate(UI_TEXT.updates.checkInProgress, 'info'); + return; + } + } catch { + manualUpdateCheckPending = false; + updateCheckInProgress = false; + setCheckButtonCheckingState(false); + notifyUpdate(UI_TEXT.updates.checkFailed, 'warn'); + } } function downloadUpdate(): void { @@ -27,20 +102,77 @@ function downloadUpdate(): void { return; } - byId('updateButton').textContent = UI_TEXT.updates.downloading; - byId('updateButton').disabled = true; - byId('updateProgress').style.display = 'block'; - byId('updateProgressBar').classList.add('downloading'); - void window.api.downloadUpdate(); + if (updateDownloadInProgress) { + notifyUpdate(UI_TEXT.updates.downloadInProgress, 'info'); + return; + } + + updateDownloadInProgress = true; + setDownloadPendingUi(); + + void window.api.downloadUpdate().then((result) => { + if (result?.error) { + updateDownloadInProgress = false; + const button = byId('updateButton'); + button.textContent = UI_TEXT.updates.downloadNow; + button.disabled = false; + byId('updateProgressBar').classList.remove('downloading'); + notifyUpdate(UI_TEXT.updates.downloadFailed, 'warn'); + return; + } + + if (result?.skipped === 'ready-to-install') { + setDownloadReadyUi(latestUpdateVersion); + return; + } + + if (result?.skipped === 'in-progress') { + notifyUpdate(UI_TEXT.updates.downloadInProgress, 'info'); + } + }).catch(() => { + updateDownloadInProgress = false; + const button = byId('updateButton'); + button.textContent = UI_TEXT.updates.downloadNow; + button.disabled = false; + byId('updateProgressBar').classList.remove('downloading'); + notifyUpdate(UI_TEXT.updates.downloadFailed, 'warn'); + }); } +window.api.onUpdateChecking(() => { + updateCheckInProgress = true; + setCheckButtonCheckingState(true); +}); + window.api.onUpdateAvailable((info: UpdateInfo) => { - byId('updateBanner').style.display = 'flex'; + updateCheckInProgress = false; + updateReady = false; + updateDownloadInProgress = true; + manualUpdateCheckPending = false; + latestUpdateVersion = info.version; + setCheckButtonCheckingState(false); + + showUpdateBanner(); byId('updateText').textContent = `Version ${info.version} ${UI_TEXT.updates.available}`; - byId('updateButton').textContent = UI_TEXT.updates.downloadNow; + byId('updateButton').textContent = UI_TEXT.updates.downloading; + byId('updateButton').disabled = true; + byId('updateProgress').style.display = 'block'; + byId('updateProgressBar').classList.add('downloading'); +}); + +window.api.onUpdateNotAvailable(() => { + updateCheckInProgress = false; + setCheckButtonCheckingState(false); + + if (manualUpdateCheckPending) { + notifyUpdate(UI_TEXT.updates.latest, 'info'); + } + + manualUpdateCheckPending = false; }); window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => { + updateDownloadInProgress = true; const bar = byId('updateProgressBar'); bar.classList.remove('downloading'); bar.style.width = progress.percent + '%'; @@ -51,13 +183,22 @@ window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => { }); window.api.onUpdateDownloaded((info: UpdateInfo) => { - updateReady = true; - - const bar = byId('updateProgressBar'); - bar.classList.remove('downloading'); - bar.style.width = '100%'; - - byId('updateText').textContent = `Version ${info.version} ${UI_TEXT.updates.ready}`; - byId('updateButton').textContent = UI_TEXT.updates.installNow; - byId('updateButton').disabled = false; + setDownloadReadyUi(info.version); +}); + +window.api.onUpdateError(() => { + updateCheckInProgress = false; + const wasDownloading = updateDownloadInProgress; + updateDownloadInProgress = false; + manualUpdateCheckPending = false; + setCheckButtonCheckingState(false); + + const button = byId('updateButton'); + if (!updateReady) { + button.textContent = UI_TEXT.updates.downloadNow; + button.disabled = false; + byId('updateProgressBar').classList.remove('downloading'); + } + + notifyUpdate(wasDownloading ? UI_TEXT.updates.downloadFailed : UI_TEXT.updates.checkFailed, 'warn'); });