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 { 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 (!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; } async function checkDoodstreamHealth(hosterConfig) { const apiKey = hosterConfig && hosterConfig.apiKey ? String(hosterConfig.apiKey).trim() : ''; if (!apiKey) { return { status: 'error', message: '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 gueltiges 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 gueltiges JSON' }; } const serverResult = serverPayload.result; if (typeof serverResult === 'string' && /^https?:\/\//i.test(serverResult.trim())) { return { status: 'ok', message: 'API Key gueltig, Upload-Server verfuegbar' }; } const serverMsg = String(serverPayload.msg || serverPayload.message || '').trim(); if (/no servers available/i.test(serverMsg)) { return { status: 'warn', message: 'API Key gueltig, aktuell kein Server von API (Uploader nutzt Fallback)' }; } return { status: 'warn', message: serverMsg || 'API Key gueltig, 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 gueltig, Upload-Server verfuegbar' }; } 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 gueltig, aktuell kein Server verfuegbar' }; } return { status: 'error', message: msg || 'API Key ungueltig 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 gueltiges JSON' }; } const serverResult = serverPayload.result; if (typeof serverResult === 'string' && /^https?:\/\//i.test(serverResult.trim())) { return { status: 'ok', message: 'API Key gueltig, Upload-Server verfuegbar' }; } const msg = String(serverPayload.msg || serverPayload.message || '').trim(); if (msg) { return { status: 'error', message: msg }; } return { status: 'error', message: 'API Key ungueltig 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', (_event, config) => { 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, hosters } = payload; debugLog(`start-upload: files=${JSON.stringify(files)}, hosters=${JSON.stringify(hosters)}`); const tasks = buildUploadTasks(config, files, hosters); debugLog(` tasks built: ${tasks.length}`); if (tasks.length === 0) return { error: 'Keine gueltigen Zugangsdaten fuer die gewaehlten Hoster.' }; // Pass hoster settings to the upload manager uploadManager = new UploadManager(config.hosterSettings || {}); 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', (summary) => { debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`); 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(); }); // 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(); uploadManager = null; } 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', (_event, hosterSettings) => { configStore.save({ hosterSettings }); return true; }); // --- Global settings --- ipcMain.handle('get-global-settings', () => { const config = configStore.load(); return config.globalSettings || {}; }); ipcMain.handle('save-global-settings', (_event, globalSettings) => { configStore.save({ globalSettings }); return true; }); // --- Always on top --- ipcMain.handle('set-always-on-top', (_event, value) => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.setAlwaysOnTop(!!value); } 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); } });