Smooth auto-updater flow with background downloads and status events (v4.1.9)
This commit is contained in:
parent
4ade0a46ac
commit
3695c096ba
4
typescript-version/package-lock.json
generated
4
typescript-version/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -457,7 +457,7 @@
|
||||
|
||||
<div class="settings-card">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -502,7 +502,7 @@
|
||||
<div class="status-dot" id="statusDot"></div>
|
||||
<span id="statusText">Nicht verbunden</span>
|
||||
</div>
|
||||
<span id="versionText">v4.1.8</span>
|
||||
<span id="versionText">v4.1.9</span>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -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<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 {
|
||||
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 };
|
||||
|
||||
@ -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));
|
||||
}
|
||||
});
|
||||
|
||||
5
typescript-version/src/renderer-globals.d.ts
vendored
5
typescript-version/src/renderer-globals.d.ts
vendored
@ -179,7 +179,7 @@ interface ApiBridge {
|
||||
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 }>;
|
||||
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>;
|
||||
@ -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 {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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> {
|
||||
try {
|
||||
await window.api.checkUpdate();
|
||||
} catch {
|
||||
// ignore silent updater errors
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUpdate(): Promise<void> {
|
||||
manualUpdateCheckPending = true;
|
||||
setCheckButtonCheckingState(true);
|
||||
|
||||
try {
|
||||
const result = await window.api.checkUpdate();
|
||||
|
||||
if (result?.error) {
|
||||
manualUpdateCheckPending = false;
|
||||
updateCheckInProgress = false;
|
||||
setCheckButtonCheckingState(false);
|
||||
notifyUpdate(UI_TEXT.updates.checkFailed, 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (byId('updateBanner').style.display !== 'flex') {
|
||||
alert(UI_TEXT.updates.latest);
|
||||
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');
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
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<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) => {
|
||||
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<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) => {
|
||||
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<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');
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user