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');
});