const { app, BrowserWindow, ipcMain, dialog, clipboard } = 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 { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater'); let mainWindow; const configStore = new ConfigStore(app); let uploadManager = null; const HEALTH_CHECK_TIMEOUT = 25000; 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 getLogFilePath() { // Next to the EXE when packaged, next to project root when dev const baseDir = app.isPackaged ? path.dirname(process.execPath) : path.join(__dirname); return path.join(baseDir, 'fileuploader.log'); } function appendUploadLog(hoster, link, fileName) { try { 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(getLogFilePath(), line, 'utf-8'); } catch {} } 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 runHosterHealthCheck(config, requestedHosters) { const allowed = ['doodstream.com', 'vidmoly.me']; 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 }; } 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: '#0f0f1a', 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 --- 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; }); ipcMain.handle('start-upload', (_event, payload) => { const config = configStore.load(); const { files, hosters } = payload; // Build tasks with credentials const tasks = []; for (const file of files) { for (const hoster of hosters) { const hosterConfig = config.hosters[hoster]; if (!hosterConfig) continue; if (hoster === 'vidmoly.me') { // Vidmoly uses username/password login if (!hosterConfig.username || !hosterConfig.password) continue; tasks.push({ file, hoster, username: hosterConfig.username, password: hosterConfig.password }); } else { // Other hosters use API key if (!hosterConfig.apiKey) continue; tasks.push({ file, hoster, apiKey: hosterConfig.apiKey }); } } } 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) => { 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) => { 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.startBatch(tasks); 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', async () => { try { installUpdate((progress) => { if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('app:update-progress', progress); } }); return { started: true }; } catch (err) { return { error: err.message }; } }); 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); } });