Smooth auto-updater flow with background downloads and status events (v4.1.9)

This commit is contained in:
xRangerDE 2026-02-21 00:16:47 +01:00
parent 4ade0a46ac
commit 3695c096ba
9 changed files with 250 additions and 40 deletions

View File

@ -1,12 +1,12 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.1.8", "version": "4.1.9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.1.8", "version": "4.1.9",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.6.0", "axios": "^1.6.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "twitch-vod-manager", "name": "twitch-vod-manager",
"version": "4.1.8", "version": "4.1.9",
"description": "Twitch VOD Manager - Download Twitch VODs easily", "description": "Twitch VOD Manager - Download Twitch VODs easily",
"main": "dist/main.js", "main": "dist/main.js",
"author": "xRangerDE", "author": "xRangerDE",

View File

@ -457,7 +457,7 @@
<div class="settings-card"> <div class="settings-card">
<h3 id="updateTitle">Updates</h3> <h3 id="updateTitle">Updates</h3>
<p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.1.8</p> <p id="versionInfo" style="margin-bottom: 10px; color: var(--text-secondary);">Version: v4.1.9</p>
<button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button> <button class="btn-secondary" id="checkUpdateBtn" onclick="checkUpdate()">Nach Updates suchen</button>
</div> </div>
@ -502,7 +502,7 @@
<div class="status-dot" id="statusDot"></div> <div class="status-dot" id="statusDot"></div>
<span id="statusText">Nicht verbunden</span> <span id="statusText">Nicht verbunden</span>
</div> </div>
<span id="versionText">v4.1.8</span> <span id="versionText">v4.1.9</span>
</div> </div>
</main> </main>
</div> </div>

View File

@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater';
// ========================================== // ==========================================
// CONFIG & CONSTANTS // CONFIG & CONSTANTS
// ========================================== // ==========================================
const APP_VERSION = '4.1.8'; const APP_VERSION = '4.1.9';
const UPDATE_CHECK_URL = 'http://24-music.de/version.json'; const UPDATE_CHECK_URL = 'http://24-music.de/version.json';
// Paths // Paths
@ -33,6 +33,7 @@ const DEBUG_LOG_BUFFER_FLUSH_LINES = 48;
const AUTO_UPDATE_CHECK_INTERVAL_MS = 10 * 60 * 1000; const AUTO_UPDATE_CHECK_INTERVAL_MS = 10 * 60 * 1000;
const AUTO_UPDATE_STARTUP_CHECK_DELAY_MS = 5000; const AUTO_UPDATE_STARTUP_CHECK_DELAY_MS = 5000;
const AUTO_UPDATE_MIN_CHECK_GAP_MS = 45 * 1000; const AUTO_UPDATE_MIN_CHECK_GAP_MS = 45 * 1000;
const AUTO_UPDATE_AUTO_DOWNLOAD = true;
const CACHE_CLEANUP_INTERVAL_MS = 60 * 1000; const CACHE_CLEANUP_INTERVAL_MS = 60 * 1000;
const MAX_LOGIN_TO_USER_ID_CACHE_ENTRIES = 4096; const MAX_LOGIN_TO_USER_ID_CACHE_ENTRIES = 4096;
const MAX_VOD_LIST_CACHE_ENTRIES = 512; const MAX_VOD_LIST_CACHE_ENTRIES = 512;
@ -47,6 +48,7 @@ const TWITCH_WEB_CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko';
type PerformanceMode = 'stability' | 'balanced' | 'speed'; type PerformanceMode = 'stability' | 'balanced' | 'speed';
type RetryErrorClass = 'network' | 'rate_limit' | 'auth' | 'tooling' | 'integrity' | 'io' | 'validation' | 'unknown'; type RetryErrorClass = 'network' | 'rate_limit' | 'auth' | 'tooling' | 'integrity' | 'io' | 'validation' | 'unknown';
type UpdateCheckSource = 'startup' | 'interval' | 'manual'; type UpdateCheckSource = 'startup' | 'interval' | 'manual';
type UpdateDownloadSource = 'auto' | 'manual';
// Ensure directories exist // Ensure directories exist
if (!fs.existsSync(APPDATA_DIR)) { if (!fs.existsSync(APPDATA_DIR)) {
@ -395,6 +397,7 @@ let autoUpdateCheckTimer: NodeJS.Timeout | null = null;
let autoUpdateStartupTimer: NodeJS.Timeout | null = null; let autoUpdateStartupTimer: NodeJS.Timeout | null = null;
let autoUpdateCheckInProgress = false; let autoUpdateCheckInProgress = false;
let autoUpdateReadyToInstall = false; let autoUpdateReadyToInstall = false;
let autoUpdateDownloadInProgress = false;
let lastAutoUpdateCheckAt = 0; let lastAutoUpdateCheckAt = 0;
let twitchLoginInFlight: Promise<boolean> | null = null; let twitchLoginInFlight: Promise<boolean> | 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 { function stopAutoUpdatePolling(): void {
if (autoUpdateCheckTimer) { if (autoUpdateCheckTimer) {
clearInterval(autoUpdateCheckTimer); clearInterval(autoUpdateCheckTimer);
@ -2936,21 +2963,28 @@ function setupAutoUpdater() {
autoUpdater.on('checking-for-update', () => { autoUpdater.on('checking-for-update', () => {
console.log('Checking for updates...'); console.log('Checking for updates...');
mainWindow?.webContents.send('update-checking');
}); });
autoUpdater.on('update-available', (info) => { autoUpdater.on('update-available', (info) => {
console.log('Update available:', info.version); console.log('Update available:', info.version);
autoUpdateReadyToInstall = false; autoUpdateReadyToInstall = false;
autoUpdateDownloadInProgress = false;
if (mainWindow) { if (mainWindow) {
mainWindow.webContents.send('update-available', { mainWindow.webContents.send('update-available', {
version: info.version, version: info.version,
releaseDate: info.releaseDate releaseDate: info.releaseDate
}); });
} }
if (AUTO_UPDATE_AUTO_DOWNLOAD) {
void requestUpdateDownload('auto');
}
}); });
autoUpdater.on('update-not-available', () => { autoUpdater.on('update-not-available', () => {
console.log('No updates available'); console.log('No updates available');
mainWindow?.webContents.send('update-not-available');
}); });
autoUpdater.on('download-progress', (progress) => { autoUpdater.on('download-progress', (progress) => {
@ -2968,6 +3002,7 @@ function setupAutoUpdater() {
autoUpdater.on('update-downloaded', (info) => { autoUpdater.on('update-downloaded', (info) => {
console.log('Update downloaded:', info.version); console.log('Update downloaded:', info.version);
autoUpdateReadyToInstall = true; autoUpdateReadyToInstall = true;
autoUpdateDownloadInProgress = false;
if (mainWindow) { if (mainWindow) {
mainWindow.webContents.send('update-downloaded', { mainWindow.webContents.send('update-downloaded', {
version: info.version version: info.version
@ -2977,6 +3012,10 @@ function setupAutoUpdater() {
autoUpdater.on('error', (err) => { autoUpdater.on('error', (err) => {
autoUpdateCheckInProgress = false; 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); console.error('Auto-updater error:', err);
}); });
@ -3194,9 +3233,15 @@ ipcMain.handle('check-update', async () => {
ipcMain.handle('download-update', async () => { ipcMain.handle('download-update', async () => {
try { try {
autoUpdateReadyToInstall = false; setupAutoUpdater();
await autoUpdater.downloadUpdate(); const result = await requestUpdateDownload('manual');
return { downloading: true }; if (result.reason === 'error') {
return { error: true };
}
return result.started
? { downloading: true }
: { downloading: true, skipped: result.reason };
} catch (err) { } catch (err) {
console.error('Download failed:', err); console.error('Download failed:', err);
return { error: true }; return { error: true };

View File

@ -165,13 +165,22 @@ contextBridge.exposeInMainWorld('api', {
}, },
// Auto-Update Events // Auto-Update Events
onUpdateChecking: (callback: () => void) => {
ipcRenderer.on('update-checking', () => callback());
},
onUpdateAvailable: (callback: (info: { version: string; releaseDate?: string }) => void) => { onUpdateAvailable: (callback: (info: { version: string; releaseDate?: string }) => void) => {
ipcRenderer.on('update-available', (_, info) => callback(info)); 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) => { onUpdateDownloadProgress: (callback: (progress: { percent: number; bytesPerSecond: number; transferred: number; total: number }) => void) => {
ipcRenderer.on('update-download-progress', (_, progress) => callback(progress)); ipcRenderer.on('update-download-progress', (_, progress) => callback(progress));
}, },
onUpdateDownloaded: (callback: (info: { version: string }) => void) => { onUpdateDownloaded: (callback: (info: { version: string }) => void) => {
ipcRenderer.on('update-downloaded', (_, info) => callback(info)); ipcRenderer.on('update-downloaded', (_, info) => callback(info));
},
onUpdateError: (callback: (payload: { message: string }) => void) => {
ipcRenderer.on('update-error', (_, payload) => callback(payload));
} }
}); });

View File

@ -179,7 +179,7 @@ interface ApiBridge {
mergeVideos(inputFiles: string[], outputFile: string): Promise<{ success: boolean; outputFile: string | null }>; mergeVideos(inputFiles: string[], outputFile: string): Promise<{ success: boolean; outputFile: string | null }>;
getVersion(): Promise<string>; getVersion(): Promise<string>;
checkUpdate(): Promise<{ checking?: boolean; error?: boolean; skipped?: 'ready-to-install' | 'in-progress' | 'throttled' | 'error' | string }>; 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<void>; installUpdate(): Promise<void>;
openExternal(url: string): Promise<void>; openExternal(url: string): Promise<void>;
runPreflight(autoFix: boolean): Promise<PreflightResult>; runPreflight(autoFix: boolean): Promise<PreflightResult>;
@ -193,9 +193,12 @@ interface ApiBridge {
onDownloadFinished(callback: () => void): void; onDownloadFinished(callback: () => void): void;
onCutProgress(callback: (percent: number) => void): void; onCutProgress(callback: (percent: number) => void): void;
onMergeProgress(callback: (percent: number) => void): void; onMergeProgress(callback: (percent: number) => void): void;
onUpdateChecking(callback: () => void): void;
onUpdateAvailable(callback: (info: UpdateInfo) => void): void; onUpdateAvailable(callback: (info: UpdateInfo) => void): void;
onUpdateNotAvailable(callback: () => void): void;
onUpdateDownloadProgress(callback: (progress: UpdateDownloadProgress) => void): void; onUpdateDownloadProgress(callback: (progress: UpdateDownloadProgress) => void): void;
onUpdateDownloaded(callback: (info: UpdateInfo) => void): void; onUpdateDownloaded(callback: (info: UpdateInfo) => void): void;
onUpdateError(callback: (payload: { message: string }) => void): void;
} }
interface Window { interface Window {

View File

@ -197,7 +197,13 @@ const UI_TEXT_DE = {
updates: { updates: {
bannerDefault: 'Neue Version verfugbar!', bannerDefault: 'Neue Version verfugbar!',
latest: 'Du hast die neueste Version!', 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...', downloading: 'Wird heruntergeladen...',
downloadInProgress: 'Update-Download lauft bereits.',
downloadFailed: 'Update-Download fehlgeschlagen.',
available: 'verfugbar!', available: 'verfugbar!',
downloadNow: 'Jetzt herunterladen', downloadNow: 'Jetzt herunterladen',
downloadLabel: 'Download', downloadLabel: 'Download',

View File

@ -197,7 +197,13 @@ const UI_TEXT_EN = {
updates: { updates: {
bannerDefault: 'New version available!', bannerDefault: 'New version available!',
latest: 'You are on the latest version!', 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...', downloading: 'Downloading...',
downloadInProgress: 'Update download is already running.',
downloadFailed: 'Update download failed.',
available: 'available!', available: 'available!',
downloadNow: 'Download now', downloadNow: 'Download now',
downloadLabel: 'Download', downloadLabel: 'Download',

View File

@ -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<HTMLButtonElement>('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<HTMLButtonElement>('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<HTMLButtonElement>('updateButton');
button.textContent = UI_TEXT.updates.installNow;
button.disabled = false;
}
async function checkUpdateSilent(): Promise<void> { async function checkUpdateSilent(): Promise<void> {
try {
await window.api.checkUpdate(); await window.api.checkUpdate();
} catch {
// ignore silent updater errors
}
} }
async function checkUpdate(): Promise<void> { async function checkUpdate(): Promise<void> {
manualUpdateCheckPending = true;
setCheckButtonCheckingState(true);
try {
const result = await window.api.checkUpdate(); const result = await window.api.checkUpdate();
if (result?.error) { if (result?.error) {
manualUpdateCheckPending = false;
updateCheckInProgress = false;
setCheckButtonCheckingState(false);
notifyUpdate(UI_TEXT.updates.checkFailed, 'warn');
return; return;
} }
const skippedReason = result?.skipped; const skippedReason = result?.skipped;
if (skippedReason === 'in-progress' || skippedReason === 'ready-to-install' || skippedReason === 'throttled') { if (skippedReason === 'ready-to-install') {
manualUpdateCheckPending = false;
updateCheckInProgress = false;
setCheckButtonCheckingState(false);
notifyUpdate(UI_TEXT.updates.readyToInstall, 'info');
return; return;
} }
setTimeout(() => { if (skippedReason === 'in-progress' || skippedReason === 'throttled') {
if (byId('updateBanner').style.display !== 'flex') { manualUpdateCheckPending = false;
alert(UI_TEXT.updates.latest); updateCheckInProgress = false;
setCheckButtonCheckingState(false);
notifyUpdate(UI_TEXT.updates.checkInProgress, 'info');
return;
}
} catch {
manualUpdateCheckPending = false;
updateCheckInProgress = false;
setCheckButtonCheckingState(false);
notifyUpdate(UI_TEXT.updates.checkFailed, 'warn');
} }
}, 2000);
} }
function downloadUpdate(): void { function downloadUpdate(): void {
@ -27,20 +102,77 @@ function downloadUpdate(): void {
return; return;
} }
byId('updateButton').textContent = UI_TEXT.updates.downloading; if (updateDownloadInProgress) {
byId('updateButton').disabled = true; notifyUpdate(UI_TEXT.updates.downloadInProgress, 'info');
byId('updateProgress').style.display = 'block'; return;
byId('updateProgressBar').classList.add('downloading'); }
void window.api.downloadUpdate();
updateDownloadInProgress = true;
setDownloadPendingUi();
void window.api.downloadUpdate().then((result) => {
if (result?.error) {
updateDownloadInProgress = false;
const button = byId<HTMLButtonElement>('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<HTMLButtonElement>('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) => { 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('updateText').textContent = `Version ${info.version} ${UI_TEXT.updates.available}`;
byId('updateButton').textContent = UI_TEXT.updates.downloadNow; byId('updateButton').textContent = UI_TEXT.updates.downloading;
byId<HTMLButtonElement>('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) => { window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => {
updateDownloadInProgress = true;
const bar = byId('updateProgressBar'); const bar = byId('updateProgressBar');
bar.classList.remove('downloading'); bar.classList.remove('downloading');
bar.style.width = progress.percent + '%'; bar.style.width = progress.percent + '%';
@ -51,13 +183,22 @@ window.api.onUpdateDownloadProgress((progress: UpdateDownloadProgress) => {
}); });
window.api.onUpdateDownloaded((info: UpdateInfo) => { window.api.onUpdateDownloaded((info: UpdateInfo) => {
updateReady = true; setDownloadReadyUi(info.version);
});
const bar = byId('updateProgressBar');
bar.classList.remove('downloading'); window.api.onUpdateError(() => {
bar.style.width = '100%'; updateCheckInProgress = false;
const wasDownloading = updateDownloadInProgress;
byId('updateText').textContent = `Version ${info.version} ${UI_TEXT.updates.ready}`; updateDownloadInProgress = false;
byId('updateButton').textContent = UI_TEXT.updates.installNow; manualUpdateCheckPending = false;
byId('updateButton').disabled = false; setCheckButtonCheckingState(false);
const button = byId<HTMLButtonElement>('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');
}); });