754 lines
23 KiB
JavaScript
754 lines
23 KiB
JavaScript
const { app, BrowserWindow, ipcMain, dialog, clipboard, powerSaveBlocker } = require('electron');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const ConfigStore = require('./lib/config-store');
|
|
const UploadManager = require('./lib/upload-manager');
|
|
const { HOSTER_CONFIGS } = require('./lib/hosters');
|
|
const VidmolyUploader = require('./lib/vidmoly-upload');
|
|
const VoeUploader = require('./lib/voe-upload');
|
|
const DoodstreamUploader = require('./lib/doodstream-upload');
|
|
const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater');
|
|
|
|
let mainWindow;
|
|
const configStore = new ConfigStore(app);
|
|
let uploadManager = null;
|
|
const HEALTH_CHECK_TIMEOUT = 25000;
|
|
|
|
// --- Debug logging (writes to upload-debug.log next to the app) ---
|
|
function getDebugLogPath() {
|
|
const baseDir = app.isPackaged
|
|
? path.dirname(process.execPath)
|
|
: __dirname;
|
|
return path.join(baseDir, 'upload-debug.log');
|
|
}
|
|
|
|
function debugLog(msg) {
|
|
try {
|
|
const ts = new Date().toISOString();
|
|
fs.appendFileSync(getDebugLogPath(), `[${ts}] ${msg}\n`, 'utf-8');
|
|
} catch {}
|
|
}
|
|
|
|
// Catch unhandled rejections from fire-and-forget async calls
|
|
process.on('unhandledRejection', (reason) => {
|
|
debugLog(`UNHANDLED REJECTION: ${reason && reason.stack ? reason.stack : reason}`);
|
|
});
|
|
|
|
function withTimeout(promise, timeoutMs, label) {
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
reject(new Error(`${label} Timeout`));
|
|
}, timeoutMs);
|
|
|
|
promise
|
|
.then((result) => {
|
|
clearTimeout(timer);
|
|
resolve(result);
|
|
})
|
|
.catch((err) => {
|
|
clearTimeout(timer);
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
|
|
function normalizeApiError(payload, fallback) {
|
|
if (!payload || typeof payload !== 'object') return fallback;
|
|
const msg = String(payload.msg || payload.message || '').trim();
|
|
if (msg) return msg;
|
|
if (payload.status) return `API Status ${payload.status}`;
|
|
return fallback;
|
|
}
|
|
|
|
function getDefaultLogFilePath() {
|
|
const baseDir = app.isPackaged
|
|
? path.dirname(process.execPath)
|
|
: path.join(__dirname);
|
|
return path.join(baseDir, 'fileuploader.log');
|
|
}
|
|
|
|
function getLogFilePath() {
|
|
const config = configStore.load();
|
|
const customPath = config && config.globalSettings
|
|
? String(config.globalSettings.logFilePath || '').trim()
|
|
: '';
|
|
return customPath || getDefaultLogFilePath();
|
|
}
|
|
|
|
function appendUploadLog(hoster, link, fileName) {
|
|
try {
|
|
const logPath = getLogFilePath();
|
|
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
const now = new Date();
|
|
const pad = (n) => String(n).padStart(2, '0');
|
|
const dateStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
const line = `${dateStr}|${hoster}|${link}||${fileName}|\n`;
|
|
fs.appendFileSync(logPath, line, 'utf-8');
|
|
} catch {}
|
|
}
|
|
|
|
function buildUploadTasks(config, files, hosters) {
|
|
const tasks = [];
|
|
|
|
for (const file of files) {
|
|
for (const hoster of hosters) {
|
|
const hosterConfig = config.hosters[hoster];
|
|
if (!hosterConfig) {
|
|
debugLog(` skip ${hoster}: no config`);
|
|
continue;
|
|
}
|
|
|
|
if (hoster === 'vidmoly.me') {
|
|
if (!hosterConfig.username || !hosterConfig.password) {
|
|
debugLog(` skip ${hoster}: missing username/password`);
|
|
continue;
|
|
}
|
|
tasks.push({ file, hoster, username: hosterConfig.username, password: hosterConfig.password });
|
|
} else if (hoster === 'voe.sx' && hosterConfig.username && hosterConfig.password) {
|
|
// VOE login-based upload (preferred over API)
|
|
tasks.push({ file, hoster, username: hosterConfig.username, password: hosterConfig.password });
|
|
debugLog(` task: ${hoster} login=${hosterConfig.username.slice(0, 6)}...`);
|
|
} else if (hoster === 'doodstream.com' && hosterConfig.username && hosterConfig.password) {
|
|
// Doodstream login-based upload (preferred over API)
|
|
tasks.push({ file, hoster, username: hosterConfig.username, password: hosterConfig.password });
|
|
debugLog(` task: ${hoster} login=${hosterConfig.username.slice(0, 6)}...`);
|
|
} else {
|
|
if (!hosterConfig.apiKey) {
|
|
debugLog(` skip ${hoster}: missing apiKey`);
|
|
continue;
|
|
}
|
|
tasks.push({ file, hoster, apiKey: hosterConfig.apiKey });
|
|
debugLog(` task: ${hoster} key=${hosterConfig.apiKey.slice(0, 6)}...`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return tasks;
|
|
}
|
|
|
|
function buildUploadTasksFromJobs(config, jobs) {
|
|
if (!Array.isArray(jobs)) return [];
|
|
|
|
return jobs.flatMap((job) => {
|
|
if (!job || !job.file || !job.hoster) return [];
|
|
const hosterConfig = config.hosters[job.hoster];
|
|
if (!hosterConfig) {
|
|
debugLog(` skip ${job.hoster}: no config for queued job`);
|
|
return [];
|
|
}
|
|
|
|
const baseTask = {
|
|
jobId: job.id || job.jobId || null,
|
|
file: job.file,
|
|
hoster: job.hoster
|
|
};
|
|
|
|
if (job.hoster === 'vidmoly.me') {
|
|
if (!hosterConfig.username || !hosterConfig.password) {
|
|
debugLog(` skip ${job.hoster}: missing username/password`);
|
|
return [];
|
|
}
|
|
return [{ ...baseTask, username: hosterConfig.username, password: hosterConfig.password }];
|
|
}
|
|
|
|
if ((job.hoster === 'voe.sx' || job.hoster === 'doodstream.com') && hosterConfig.username && hosterConfig.password) {
|
|
debugLog(` task: ${job.hoster} queued login=${hosterConfig.username.slice(0, 6)}...`);
|
|
return [{ ...baseTask, username: hosterConfig.username, password: hosterConfig.password, apiKey: hosterConfig.apiKey || '' }];
|
|
}
|
|
|
|
if (!hosterConfig.apiKey) {
|
|
debugLog(` skip ${job.hoster}: missing apiKey`);
|
|
return [];
|
|
}
|
|
|
|
debugLog(` task: ${job.hoster} queued key=${hosterConfig.apiKey.slice(0, 6)}...`);
|
|
return [{ ...baseTask, apiKey: hosterConfig.apiKey }];
|
|
});
|
|
}
|
|
|
|
async function checkDoodstreamHealth(hosterConfig) {
|
|
const username = hosterConfig && hosterConfig.username
|
|
? String(hosterConfig.username).trim()
|
|
: '';
|
|
const password = hosterConfig && hosterConfig.password
|
|
? String(hosterConfig.password).trim()
|
|
: '';
|
|
|
|
// Login-based check (preferred)
|
|
if (username && password) {
|
|
const uploader = new DoodstreamUploader();
|
|
await uploader.login(username, password);
|
|
return { status: 'ok', message: 'Login ok, Upload-Seite bereit' };
|
|
}
|
|
|
|
// Fall back to API key check
|
|
const apiKey = hosterConfig && hosterConfig.apiKey
|
|
? String(hosterConfig.apiKey).trim()
|
|
: '';
|
|
|
|
if (!apiKey) {
|
|
return { status: 'error', message: 'Login oder API Key fehlt' };
|
|
}
|
|
|
|
const apiBase = HOSTER_CONFIGS['doodstream.com'].apiBase;
|
|
|
|
const accountRes = await fetch(`${apiBase}/api/account/info?key=${encodeURIComponent(apiKey)}`, {
|
|
method: 'GET',
|
|
redirect: 'follow'
|
|
});
|
|
const accountPayload = await accountRes.json().catch(() => null);
|
|
if (!accountPayload || typeof accountPayload !== 'object') {
|
|
return { status: 'error', message: 'Account-Check lieferte kein gültiges JSON' };
|
|
}
|
|
|
|
if (Number(accountPayload.status || 0) !== 200) {
|
|
return {
|
|
status: 'error',
|
|
message: normalizeApiError(accountPayload, 'Account-Check fehlgeschlagen')
|
|
};
|
|
}
|
|
|
|
const serverRes = await fetch(`${apiBase}/api/upload/server?key=${encodeURIComponent(apiKey)}`, {
|
|
method: 'GET',
|
|
redirect: 'follow'
|
|
});
|
|
const serverPayload = await serverRes.json().catch(() => null);
|
|
if (!serverPayload || typeof serverPayload !== 'object') {
|
|
return { status: 'warn', message: 'Upload-Server-Check lieferte kein gültiges JSON' };
|
|
}
|
|
|
|
const serverResult = serverPayload.result;
|
|
if (typeof serverResult === 'string' && /^https?:\/\//i.test(serverResult.trim())) {
|
|
return { status: 'ok', message: 'API Key gültig, Upload-Server verfügbar' };
|
|
}
|
|
|
|
const serverMsg = String(serverPayload.msg || serverPayload.message || '').trim();
|
|
if (/no servers available/i.test(serverMsg)) {
|
|
return {
|
|
status: 'warn',
|
|
message: 'API Key gültig, aktuell kein Server von API (Uploader nutzt Fallback)'
|
|
};
|
|
}
|
|
|
|
return {
|
|
status: 'warn',
|
|
message: serverMsg || 'API Key gültig, Upload-Server aktuell nicht geliefert'
|
|
};
|
|
}
|
|
|
|
async function checkVidmolyHealth(hosterConfig) {
|
|
const username = hosterConfig && hosterConfig.username
|
|
? String(hosterConfig.username).trim()
|
|
: '';
|
|
const password = hosterConfig && hosterConfig.password
|
|
? String(hosterConfig.password).trim()
|
|
: '';
|
|
|
|
if (!username || !password) {
|
|
return { status: 'error', message: 'Username oder Passwort fehlt' };
|
|
}
|
|
|
|
const uploader = new VidmolyUploader();
|
|
await uploader.login(username, password);
|
|
const { uploadUrl, fileFieldName } = await uploader.getUploadParams();
|
|
|
|
if (!uploadUrl || !/^https?:\/\//i.test(uploadUrl)) {
|
|
return { status: 'error', message: 'Upload-URL wurde nicht erkannt' };
|
|
}
|
|
|
|
return {
|
|
status: 'ok',
|
|
message: `Login ok, Upload-Form bereit (Dateifeld: ${fileFieldName || 'file'})`
|
|
};
|
|
}
|
|
|
|
async function checkVoeHealth(hosterConfig) {
|
|
const username = hosterConfig && hosterConfig.username
|
|
? String(hosterConfig.username).trim()
|
|
: '';
|
|
const password = hosterConfig && hosterConfig.password
|
|
? String(hosterConfig.password).trim()
|
|
: '';
|
|
|
|
if (!username || !password) {
|
|
// Fall back to API key check if no login
|
|
const apiKey = hosterConfig && hosterConfig.apiKey
|
|
? String(hosterConfig.apiKey).trim()
|
|
: '';
|
|
if (!apiKey) {
|
|
return { status: 'error', message: 'Login oder API Key fehlt' };
|
|
}
|
|
// Quick API check
|
|
const res = await fetch(`https://voe.sx/api/upload/server?key=${encodeURIComponent(apiKey)}`, { method: 'GET' });
|
|
const data = await res.json().catch(() => null);
|
|
if (data && data.result && typeof data.result === 'string' && /^https?:\/\//i.test(data.result.trim())) {
|
|
return { status: 'ok', message: 'API Key gültig, Upload-Server verfügbar' };
|
|
}
|
|
const msg = data && (data.msg || data.message) ? String(data.msg || data.message).trim() : '';
|
|
if (/no servers/i.test(msg)) {
|
|
return { status: 'warn', message: 'API Key gültig, aktuell kein Server verfügbar' };
|
|
}
|
|
return { status: 'error', message: msg || 'API Key ungültig oder Server nicht erreichbar' };
|
|
}
|
|
|
|
const uploader = new VoeUploader();
|
|
await uploader.login(username, password);
|
|
const { csrfToken } = await uploader._getUploadParams();
|
|
|
|
if (!csrfToken) {
|
|
return { status: 'error', message: 'Login ok, aber Upload-Seite liefert kein CSRF-Token' };
|
|
}
|
|
|
|
return {
|
|
status: 'ok',
|
|
message: 'Login ok, Upload-Seite bereit'
|
|
};
|
|
}
|
|
|
|
async function checkByseHealth(hosterConfig) {
|
|
const apiKey = hosterConfig && hosterConfig.apiKey
|
|
? String(hosterConfig.apiKey).trim()
|
|
: '';
|
|
|
|
if (!apiKey) {
|
|
return { status: 'error', message: 'API Key fehlt' };
|
|
}
|
|
|
|
const apiBase = 'https://api.byse.sx';
|
|
|
|
const serverRes = await fetch(`${apiBase}/upload/server?key=${encodeURIComponent(apiKey)}`, {
|
|
method: 'GET',
|
|
redirect: 'follow'
|
|
});
|
|
const serverPayload = await serverRes.json().catch(() => null);
|
|
|
|
if (!serverPayload || typeof serverPayload !== 'object') {
|
|
return { status: 'error', message: 'API lieferte kein gültiges JSON' };
|
|
}
|
|
|
|
const serverResult = serverPayload.result;
|
|
if (typeof serverResult === 'string' && /^https?:\/\//i.test(serverResult.trim())) {
|
|
return { status: 'ok', message: 'API Key gültig, Upload-Server verfügbar' };
|
|
}
|
|
|
|
const msg = String(serverPayload.msg || serverPayload.message || '').trim();
|
|
if (msg) {
|
|
return { status: 'error', message: msg };
|
|
}
|
|
|
|
return { status: 'error', message: 'API Key ungültig oder Server nicht erreichbar' };
|
|
}
|
|
|
|
async function runHosterHealthCheck(config, requestedHosters) {
|
|
const allowed = ['doodstream.com', 'vidmoly.me', 'voe.sx', 'byse.sx'];
|
|
const source = Array.isArray(requestedHosters) && requestedHosters.length > 0
|
|
? requestedHosters
|
|
: allowed;
|
|
|
|
const hosters = source
|
|
.map((name) => String(name || '').trim())
|
|
.filter((name, index, arr) => name && arr.indexOf(name) === index);
|
|
|
|
const checks = hosters.map(async (hoster) => {
|
|
if (!allowed.includes(hoster)) {
|
|
return { hoster, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
|
|
}
|
|
|
|
const hosterConfig = config && config.hosters ? config.hosters[hoster] : null;
|
|
|
|
try {
|
|
if (hoster === 'doodstream.com') {
|
|
const result = await withTimeout(
|
|
checkDoodstreamHealth(hosterConfig),
|
|
HEALTH_CHECK_TIMEOUT,
|
|
'Doodstream-Check'
|
|
);
|
|
return { hoster, ...result };
|
|
}
|
|
|
|
if (hoster === 'vidmoly.me') {
|
|
const result = await withTimeout(
|
|
checkVidmolyHealth(hosterConfig),
|
|
HEALTH_CHECK_TIMEOUT,
|
|
'Vidmoly-Check'
|
|
);
|
|
return { hoster, ...result };
|
|
}
|
|
|
|
if (hoster === 'voe.sx') {
|
|
const result = await withTimeout(
|
|
checkVoeHealth(hosterConfig),
|
|
HEALTH_CHECK_TIMEOUT,
|
|
'VOE-Check'
|
|
);
|
|
return { hoster, ...result };
|
|
}
|
|
|
|
if (hoster === 'byse.sx') {
|
|
const result = await withTimeout(
|
|
checkByseHealth(hosterConfig),
|
|
HEALTH_CHECK_TIMEOUT,
|
|
'Byse-Check'
|
|
);
|
|
return { hoster, ...result };
|
|
}
|
|
|
|
return { hoster, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
|
|
} catch (err) {
|
|
return {
|
|
hoster,
|
|
status: 'error',
|
|
message: err && err.message ? err.message : 'Health-Check fehlgeschlagen'
|
|
};
|
|
}
|
|
});
|
|
|
|
const results = await Promise.all(checks);
|
|
return {
|
|
checkedAt: new Date().toISOString(),
|
|
results
|
|
};
|
|
}
|
|
|
|
function createWindow() {
|
|
mainWindow = new BrowserWindow({
|
|
width: 1100,
|
|
height: 750,
|
|
minWidth: 800,
|
|
minHeight: 550,
|
|
backgroundColor: '#16181c',
|
|
autoHideMenuBar: true,
|
|
webPreferences: {
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
preload: path.join(__dirname, 'preload.js')
|
|
}
|
|
});
|
|
|
|
mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
|
|
}
|
|
|
|
app.whenReady().then(() => {
|
|
createWindow();
|
|
|
|
// Auto-check for updates after 3 seconds
|
|
setTimeout(async () => {
|
|
try {
|
|
const result = await checkForUpdate();
|
|
if (result && result.available && mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send('app:update-available', result);
|
|
}
|
|
} catch {}
|
|
}, 3000);
|
|
});
|
|
|
|
app.on('window-all-closed', () => {
|
|
app.quit();
|
|
});
|
|
|
|
// --- IPC Handlers ---
|
|
|
|
// Debug log from renderer
|
|
ipcMain.handle('debug-log', (_event, msg) => {
|
|
debugLog(`[RENDERER] ${msg}`);
|
|
return true;
|
|
});
|
|
|
|
ipcMain.handle('get-config', () => {
|
|
return configStore.load();
|
|
});
|
|
|
|
ipcMain.handle('save-config', async (_event, config) => {
|
|
await configStore.save(config);
|
|
return true;
|
|
});
|
|
|
|
ipcMain.handle('get-history', () => {
|
|
return configStore.loadHistory();
|
|
});
|
|
|
|
ipcMain.handle('run-health-check', async (_event, payload) => {
|
|
const config = configStore.load();
|
|
const hosters = payload && Array.isArray(payload.hosters) ? payload.hosters : [];
|
|
return runHosterHealthCheck(config, hosters);
|
|
});
|
|
|
|
ipcMain.handle('select-files', async () => {
|
|
const result = await dialog.showOpenDialog(mainWindow, {
|
|
properties: ['openFile', 'multiSelections'],
|
|
filters: [
|
|
{ name: 'Alle Dateien', extensions: ['*'] },
|
|
{ name: 'Videos', extensions: ['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm'] }
|
|
]
|
|
});
|
|
return result.canceled ? null : result.filePaths;
|
|
});
|
|
|
|
// Debug self-test: runs a minimal upload in the main process to verify events work
|
|
ipcMain.handle('debug-test-upload', async () => {
|
|
const testFile = path.join(__dirname, 'test-self-check.txt');
|
|
try {
|
|
fs.writeFileSync(testFile, 'selftest ' + Date.now(), 'utf-8');
|
|
const mgr = new UploadManager({ 'voe.sx': { retries: 0, parallelCount: 1, maxSpeedKbs: 0, restartBelowKbs: 0, timeIntervalSec: 0, maxSizeMb: 0 } });
|
|
const events = [];
|
|
return new Promise((resolve) => {
|
|
mgr.on('progress', (data) => { events.push({ s: data.status, e: data.error || null }); });
|
|
mgr.on('batch-done', (summary) => {
|
|
try { fs.unlinkSync(testFile); } catch {}
|
|
resolve({ ok: true, events, summary: { ok: summary.succeeded, fail: summary.failed } });
|
|
});
|
|
mgr.startBatch([{ file: testFile, hoster: 'voe.sx', apiKey: 'invalid-test-key' }]);
|
|
setTimeout(() => {
|
|
try { fs.unlinkSync(testFile); } catch {}
|
|
resolve({ ok: false, events, timeout: true });
|
|
}, 20000);
|
|
});
|
|
} catch (err) {
|
|
try { fs.unlinkSync(testFile); } catch {}
|
|
return { ok: false, error: err.message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('select-folder', async () => {
|
|
const result = await dialog.showOpenDialog(mainWindow, {
|
|
properties: ['openDirectory']
|
|
});
|
|
return result.canceled ? null : result.filePaths;
|
|
});
|
|
|
|
ipcMain.handle('start-upload', (_event, payload) => {
|
|
const config = configStore.load();
|
|
const files = payload && Array.isArray(payload.files) ? payload.files : [];
|
|
const hosters = payload && Array.isArray(payload.hosters) ? payload.hosters : [];
|
|
const jobs = payload && Array.isArray(payload.jobs) ? payload.jobs : [];
|
|
|
|
debugLog(`start-upload: files=${JSON.stringify(files)}, hosters=${JSON.stringify(hosters)}, jobs=${jobs.length}`);
|
|
|
|
const tasks = jobs.length > 0
|
|
? buildUploadTasksFromJobs(config, jobs)
|
|
: buildUploadTasks(config, files, hosters);
|
|
|
|
debugLog(` tasks built: ${tasks.length}`);
|
|
|
|
if (tasks.length === 0) return { error: 'Keine gültigen Zugangsdaten für die gewählten Hoster.' };
|
|
|
|
// Pass hoster settings to the upload manager
|
|
uploadManager = new UploadManager(config.hosterSettings || {}, config.globalSettings || {});
|
|
|
|
uploadManager.on('progress', (data) => {
|
|
// Only log state changes, not continuous progress updates
|
|
if (data.status !== 'uploading') {
|
|
debugLog(`progress: ${data.fileName} ${data.hoster} ${data.status} ${data.error || ''}`);
|
|
}
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send('upload-progress', data);
|
|
}
|
|
});
|
|
|
|
uploadManager.on('stats', (data) => {
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send('upload-stats', data);
|
|
}
|
|
});
|
|
|
|
uploadManager.on('batch-done', async (summary) => {
|
|
debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`);
|
|
await configStore.appendHistory(summary);
|
|
// Write successful uploads to fileuploader.log
|
|
for (const file of summary.files || []) {
|
|
for (const result of file.results || []) {
|
|
if (result.status === 'done' && (result.download_url || result.embed_url)) {
|
|
appendUploadLog(
|
|
result.hoster || '',
|
|
result.download_url || result.embed_url || '',
|
|
file.name || ''
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send('upload-batch-done', summary);
|
|
}
|
|
|
|
// Shutdown after finish
|
|
handleShutdownAfterFinish();
|
|
uploadManager = null;
|
|
});
|
|
|
|
// Defer startBatch to next tick so the IPC response is sent first.
|
|
// This ensures webContents.send() calls from upload events
|
|
// are not interleaved with the handle() response.
|
|
process.nextTick(() => {
|
|
debugLog('nextTick: calling startBatch now');
|
|
uploadManager.startBatch(tasks).catch((err) => {
|
|
debugLog(`startBatch REJECTED: ${err && err.stack ? err.stack : err}`);
|
|
// Forward error to renderer as batch-done with failure
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send('upload-batch-done', {
|
|
id: 'error',
|
|
timestamp: new Date().toISOString(),
|
|
total: tasks.length,
|
|
succeeded: 0,
|
|
failed: tasks.length,
|
|
files: [],
|
|
error: err ? err.message : 'Unbekannter Fehler'
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
debugLog(`start-upload returning started=true (startBatch deferred to nextTick)`);
|
|
return { started: true, taskCount: tasks.length };
|
|
});
|
|
|
|
ipcMain.handle('cancel-upload', () => {
|
|
if (uploadManager) {
|
|
uploadManager.cancel();
|
|
}
|
|
return true;
|
|
});
|
|
|
|
ipcMain.handle('cancel-selected-jobs', (_event, jobIds) => {
|
|
if (uploadManager) {
|
|
uploadManager.cancelJobs(Array.isArray(jobIds) ? jobIds : []);
|
|
}
|
|
return true;
|
|
});
|
|
|
|
ipcMain.handle('finish-after-active', () => {
|
|
if (uploadManager) {
|
|
uploadManager.finishAfterActive();
|
|
}
|
|
return true;
|
|
});
|
|
|
|
ipcMain.handle('clear-history', () => {
|
|
configStore.clearHistory();
|
|
return true;
|
|
});
|
|
|
|
ipcMain.handle('copy-to-clipboard', (_event, text) => {
|
|
clipboard.writeText(text);
|
|
return true;
|
|
});
|
|
|
|
ipcMain.handle('app:check-updates', async () => {
|
|
try {
|
|
return await checkForUpdate();
|
|
} catch (err) {
|
|
return { available: false, error: err.message };
|
|
}
|
|
});
|
|
|
|
ipcMain.handle('app:install-update', () => {
|
|
installUpdate((progress) => {
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send('app:update-progress', progress);
|
|
}
|
|
}).catch((err) => {
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send('app:update-progress', { stage: 'error', error: err.message });
|
|
}
|
|
});
|
|
return { started: true };
|
|
});
|
|
|
|
ipcMain.handle('app:abort-update', () => {
|
|
abortUpdate();
|
|
return true;
|
|
});
|
|
|
|
ipcMain.handle('app:get-version', () => {
|
|
return app.getVersion();
|
|
});
|
|
|
|
// --- Hoster settings ---
|
|
ipcMain.handle('get-hoster-settings', () => {
|
|
const config = configStore.load();
|
|
return config.hosterSettings || {};
|
|
});
|
|
|
|
ipcMain.handle('save-hoster-settings', async (_event, hosterSettings) => {
|
|
await configStore.save({ hosterSettings });
|
|
return true;
|
|
});
|
|
|
|
// --- Global settings ---
|
|
ipcMain.handle('get-global-settings', () => {
|
|
const config = configStore.load();
|
|
return config.globalSettings || {};
|
|
});
|
|
|
|
ipcMain.handle('save-global-settings', async (_event, globalSettings) => {
|
|
await configStore.save({ globalSettings });
|
|
return true;
|
|
});
|
|
|
|
// --- Always on top ---
|
|
ipcMain.handle('set-always-on-top', async (_event, value) => {
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.setAlwaysOnTop(!!value);
|
|
}
|
|
await configStore.save({ globalSettings: { ...configStore.load().globalSettings, alwaysOnTop: !!value } });
|
|
return true;
|
|
});
|
|
|
|
ipcMain.handle('get-always-on-top', () => {
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
return mainWindow.isAlwaysOnTop();
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// --- Shutdown after finish ---
|
|
let shutdownMode = 'nothing';
|
|
let shutdownTimer = null;
|
|
|
|
ipcMain.handle('set-shutdown-after-finish', (_event, mode) => {
|
|
shutdownMode = mode || 'nothing';
|
|
return true;
|
|
});
|
|
|
|
ipcMain.handle('get-shutdown-after-finish', () => {
|
|
return shutdownMode;
|
|
});
|
|
|
|
ipcMain.handle('cancel-shutdown', () => {
|
|
if (shutdownTimer) {
|
|
clearTimeout(shutdownTimer);
|
|
shutdownTimer = null;
|
|
}
|
|
shutdownMode = 'nothing';
|
|
return true;
|
|
});
|
|
|
|
function handleShutdownAfterFinish() {
|
|
if (shutdownMode === 'nothing') return;
|
|
|
|
const { exec } = require('child_process');
|
|
const mode = shutdownMode;
|
|
|
|
// Notify renderer
|
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
mainWindow.webContents.send('shutdown-countdown', { mode, seconds: 60 });
|
|
}
|
|
|
|
shutdownTimer = setTimeout(() => {
|
|
if (mode === 'shutdown') {
|
|
exec('shutdown /s /t 0');
|
|
} else if (mode === 'restart') {
|
|
exec('shutdown /r /t 0');
|
|
} else if (mode === 'sleep') {
|
|
exec('rundll32.exe powrprof.dll,SetSuspendState 0,1,0');
|
|
}
|
|
}, 60000);
|
|
}
|
|
|
|
// Restore always-on-top from config on window creation
|
|
app.on('browser-window-created', () => {
|
|
const config = configStore.load();
|
|
if (config.globalSettings && config.globalSettings.alwaysOnTop && mainWindow) {
|
|
mainWindow.setAlwaysOnTop(true);
|
|
}
|
|
});
|