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 }; // Template for each hoster type (used as defaults for new accounts) const HOSTER_ACCOUNT_TEMPLATES = { 'doodstream.com': { enabled: true, authType: 'login', username: '', password: '' }, 'doodstream.com:api': { enabled: true, authType: 'api', apiKey: '' }, 'voe.sx': { enabled: true, authType: 'login', username: '', password: '' }, 'voe.sx:api': { enabled: true, authType: 'api', apiKey: '' }, 'vidmoly.me': { enabled: true, authType: 'login', username: '', password: '' }, 'byse.sx': { enabled: true, authType: 'api', apiKey: '' } }; // All known hoster names (used for iteration) const HOSTER_NAMES = ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx']; // Dropdown options for "Add Account" modal: value -> label const HOSTER_ADD_OPTIONS = [ { value: 'doodstream.com', label: 'Doodstream (Web Login)', hoster: 'doodstream.com', authType: 'login' }, { value: 'doodstream.com:api', label: 'Doodstream (API)', hoster: 'doodstream.com', authType: 'api' }, { value: 'voe.sx', label: 'Voe (Web Login)', hoster: 'voe.sx', authType: 'login' }, { value: 'voe.sx:api', label: 'Voe (API)', hoster: 'voe.sx', authType: 'api' }, { value: 'vidmoly.me', label: 'Vidmoly (Web Login)', hoster: 'vidmoly.me', authType: 'login' }, { value: 'byse.sx', label: 'Byse (API)', hoster: 'byse.sx', authType: 'api' } ]; const DEFAULTS = { hosters: { 'doodstream.com': [], 'voe.sx': [], 'vidmoly.me': [], 'byse.sx': [] }, 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, showDropTarget: 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, hosters: [] // pre-selected hosters, empty = ask via modal }, remote: { enabled: false, port: 9100, token: '', allowInput: 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'); this._writeQueue = Promise.resolve(); // Serializes all writes to prevent race conditions // 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)); // Migrate old single-object format to array format for (const [name, val] of Object.entries(data.hosters || {})) { if (val && !Array.isArray(val)) { if (!val.id) val.id = `${name}-migrated-${Date.now()}`; // Infer authType for old format accounts if (!val.authType) { if (name === 'byse.sx') val.authType = 'api'; else if (name === 'vidmoly.me') val.authType = 'login'; else if (val.username && val.password) val.authType = 'login'; else if (val.apiKey) val.authType = 'api'; else val.authType = 'login'; } data.hosters[name] = [val]; } } // Merge hosters: ensure all known hosters exist as arrays const hosters = {}; for (const name of HOSTER_NAMES) { const saved = data.hosters && data.hosters[name]; if (Array.isArray(saved) && saved.length > 0) { hosters[name] = saved.map((acc, i) => { // Ensure authType is set on every account if (!acc.authType) { if (name === 'byse.sx') acc.authType = 'api'; else if (name === 'vidmoly.me') acc.authType = 'login'; else if (acc.username && acc.password) acc.authType = 'login'; else if (acc.apiKey) acc.authType = 'api'; else acc.authType = 'login'; } return { ...acc, id: acc.id || `${name}-${Date.now()}-${i}` }; }); } else { hosters[name] = []; } } // 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)); } } _enqueueWrite(fn) { this._writeQueue = this._writeQueue.then(fn, fn); return this._writeQueue; } save(config) { return this._enqueueWrite(() => { 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) { return this._enqueueWrite(() => { 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() { return this._enqueueWrite(() => { const config = this.load(); config.history = []; return this._atomicWrite(JSON.stringify(config, null, 2)); }); } } module.exports = ConfigStore; module.exports.HOSTER_ACCOUNT_TEMPLATES = HOSTER_ACCOUNT_TEMPLATES; module.exports.HOSTER_NAMES = HOSTER_NAMES; module.exports.HOSTER_ADD_OPTIONS = HOSTER_ADD_OPTIONS;