const fs = require('fs'); const path = require('path'); const HOSTER_SETTINGS_DEFAULTS = { retries: 3, maxSpeedKbs: 0, // 0 = unlimited parallelCount: 2, // 1-100 restartBelowKbs: 0, // 0 = off timeIntervalSec: 0, // delay between jobs maxSizeMb: 0 // 0 = unlimited }; const DEFAULTS = { hosters: { 'doodstream.com': { enabled: true, apiKey: '', username: '', password: '' }, 'voe.sx': { enabled: true, apiKey: '' }, 'vidmoly.me': { enabled: true, authType: 'login', username: '', password: '' }, 'byse.sx': { enabled: true, apiKey: '' } }, hosterSettings: { 'doodstream.com': { ...HOSTER_SETTINGS_DEFAULTS }, 'voe.sx': { ...HOSTER_SETTINGS_DEFAULTS }, 'vidmoly.me': { ...HOSTER_SETTINGS_DEFAULTS }, 'byse.sx': { ...HOSTER_SETTINGS_DEFAULTS } }, globalSettings: { alwaysOnTop: false, shutdownAfterFinish: 'nothing', // nothing | sleep | shutdown | restart logFilePath: '', sessionLog: false, resumeQueueOnLaunch: true, parallelUploadCount: 0, // 0 = use per-hoster limits only scaleParallelUploads: false, removeFromQueueOnDone: false, globalMaxSpeedKbs: 0, // 0 = unlimited global speed pendingQueue: null, scramble: { active: false, prefix: '', suffix: '', chars: 'both', // 'letters' | 'numbers' | 'both' length: 0 // 0 = same as original basename length }, folderMonitor: { enabled: false, folderPath: '', recursive: false, filterMode: 'include', // 'include' | 'exclude' extensions: '', // comma-separated: 'mp4,mkv,avi' skipDuplicates: true, delaySec: 3, autoStart: true } }, history: [] }; const MAX_HISTORY = 100; class ConfigStore { constructor(app) { const dir = app && app.isPackaged ? app.getPath('userData') : path.join(__dirname, '..'); this.filePath = path.join(dir, 'electron-config.json'); // Migrate config from old location if current doesn't exist if (!fs.existsSync(this.filePath) && app && app.isPackaged) { this._migrateFromOldPath(app); } } _migrateFromOldPath(app) { try { const appDataDir = path.dirname(app.getPath('userData')); // Check alternate folder names that may have been used const candidates = ['multi-hoster-uploader', 'Multi-Hoster-Upload']; for (const name of candidates) { const oldPath = path.join(appDataDir, name, 'electron-config.json'); if (oldPath !== this.filePath && fs.existsSync(oldPath)) { fs.mkdirSync(path.dirname(this.filePath), { recursive: true }); fs.copyFileSync(oldPath, this.filePath); return; } } // Also check next to the executable (portable mode previous location) const exeDir = path.dirname(app.getPath('exe')); const portablePath = path.join(exeDir, 'electron-config.json'); if (portablePath !== this.filePath && fs.existsSync(portablePath)) { fs.mkdirSync(path.dirname(this.filePath), { recursive: true }); fs.copyFileSync(portablePath, this.filePath); } } catch {} } _readAndParse(filePath) { const raw = fs.readFileSync(filePath, 'utf-8'); if (!raw || raw.trim().length < 2) return null; return JSON.parse(raw); } load() { try { let data = null; // Try main config try { data = this._readAndParse(this.filePath); } catch {} // Fallback to backup if main is empty/corrupt if (!data) { const backupPath = this.filePath + '.bak'; try { data = this._readAndParse(backupPath); } catch {} } if (!data) return JSON.parse(JSON.stringify(DEFAULTS)); // Merge with defaults so new hosters are always present const hosters = { ...DEFAULTS.hosters }; for (const [name, val] of Object.entries(data.hosters || {})) { if (hosters[name]) { hosters[name] = { ...hosters[name], ...val }; } } // Merge hoster settings with defaults const hosterSettings = {}; for (const name of Object.keys(DEFAULTS.hosterSettings)) { hosterSettings[name] = { ...HOSTER_SETTINGS_DEFAULTS, ...(data.hosterSettings && data.hosterSettings[name] || {}) }; } const savedGlobal = data.globalSettings || {}; const globalSettings = { ...DEFAULTS.globalSettings, ...savedGlobal }; // Deep-merge nested objects so new keys are always present for (const key of Object.keys(DEFAULTS.globalSettings)) { const def = DEFAULTS.globalSettings[key]; if (def && typeof def === 'object' && !Array.isArray(def)) { globalSettings[key] = { ...def, ...(savedGlobal[key] || {}) }; } } return { hosters, hosterSettings, globalSettings, history: data.history || [] }; } catch { return JSON.parse(JSON.stringify(DEFAULTS)); } } save(config) { const current = this.load(); if (config.hosters) current.hosters = config.hosters; if (config.hosterSettings) current.hosterSettings = config.hosterSettings; if (config.globalSettings) current.globalSettings = config.globalSettings; return this._atomicWrite(JSON.stringify(current, null, 2)); } loadHistory() { const config = this.load(); return config.history || []; } _atomicWrite(data) { return new Promise((resolve, reject) => { const tmpPath = this.filePath + '.tmp'; const backupPath = this.filePath + '.bak'; fs.writeFile(tmpPath, data, 'utf-8', (err) => { if (err) return reject(err); try { if (fs.existsSync(this.filePath)) { const existing = fs.readFileSync(this.filePath, 'utf-8'); if (existing && existing.trim().length > 2) { fs.writeFileSync(backupPath, existing, 'utf-8'); } } fs.renameSync(tmpPath, this.filePath); } catch (e) { return reject(e); } resolve(); }); }); } appendHistory(entry) { const config = this.load(); config.history.push(entry); if (config.history.length > MAX_HISTORY) { config.history = config.history.slice(-MAX_HISTORY); } return this._atomicWrite(JSON.stringify(config, null, 2)); } clearHistory() { const config = this.load(); config.history = []; return this._atomicWrite(JSON.stringify(config, null, 2)); } } module.exports = ConfigStore;