feat: multi-account support with primary/fallback and separate API/login types
- Multiple accounts per hoster with drag-sortable priority (primary + fallbacks) - Separate account types: Web Login and API selectable per hoster - Account fallback: after all retries fail, automatically switches to next fallback account - Fix: Byse health check returning [Fehler] OK when API responds with msg "OK" - Fix: retry during active upload sets status to "Wartet" instead of "Bereit" - Config migration from single-object to multi-account array format Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2c9726a33d
commit
d53eea443e
@ -10,12 +10,35 @@ const HOSTER_SETTINGS_DEFAULTS = {
|
|||||||
maxSizeMb: 0 // 0 = unlimited
|
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 = {
|
const DEFAULTS = {
|
||||||
hosters: {
|
hosters: {
|
||||||
'doodstream.com': { enabled: true, apiKey: '', username: '', password: '' },
|
'doodstream.com': [],
|
||||||
'voe.sx': { enabled: true, apiKey: '' },
|
'voe.sx': [],
|
||||||
'vidmoly.me': { enabled: true, authType: 'login', username: '', password: '' },
|
'vidmoly.me': [],
|
||||||
'byse.sx': { enabled: true, apiKey: '' }
|
'byse.sx': []
|
||||||
},
|
},
|
||||||
hosterSettings: {
|
hosterSettings: {
|
||||||
'doodstream.com': { ...HOSTER_SETTINGS_DEFAULTS },
|
'doodstream.com': { ...HOSTER_SETTINGS_DEFAULTS },
|
||||||
@ -113,13 +136,46 @@ class ConfigStore {
|
|||||||
}
|
}
|
||||||
if (!data) return JSON.parse(JSON.stringify(DEFAULTS));
|
if (!data) return JSON.parse(JSON.stringify(DEFAULTS));
|
||||||
|
|
||||||
// Merge with defaults so new hosters are always present
|
// Migrate old single-object format to array format
|
||||||
const hosters = { ...DEFAULTS.hosters };
|
|
||||||
for (const [name, val] of Object.entries(data.hosters || {})) {
|
for (const [name, val] of Object.entries(data.hosters || {})) {
|
||||||
if (hosters[name]) {
|
if (val && !Array.isArray(val)) {
|
||||||
hosters[name] = { ...hosters[name], ...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
|
// Merge hoster settings with defaults
|
||||||
const hosterSettings = {};
|
const hosterSettings = {};
|
||||||
for (const name of Object.keys(DEFAULTS.hosterSettings)) {
|
for (const name of Object.keys(DEFAULTS.hosterSettings)) {
|
||||||
@ -196,3 +252,6 @@ class ConfigStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ConfigStore;
|
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;
|
||||||
|
|||||||
@ -37,6 +37,12 @@ class UploadManager extends EventEmitter {
|
|||||||
this.lastStartTime = {}; // hoster -> timestamp of last upload start
|
this.lastStartTime = {}; // hoster -> timestamp of last upload start
|
||||||
this.intervalLocks = {}; // hoster -> Promise chain for serialized interval waits
|
this.intervalLocks = {}; // hoster -> Promise chain for serialized interval waits
|
||||||
this.globalThrottle = null;
|
this.globalThrottle = null;
|
||||||
|
this._failedAccounts = new Map(); // hoster -> Set of failed accountIds
|
||||||
|
this._accountOverrides = new Map(); // hoster -> fallback account object
|
||||||
|
}
|
||||||
|
|
||||||
|
switchAccount(hoster, fallbackAccount) {
|
||||||
|
this._accountOverrides.set(hoster, fallbackAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSettings(hosterSettings, globalSettings) {
|
updateSettings(hosterSettings, globalSettings) {
|
||||||
@ -364,22 +370,7 @@ class UploadManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let result;
|
const result = await this._executeUpload(task, progressCb, uploadSignalBundle.signal, throttle);
|
||||||
if (task.hoster === 'vidmoly.me' && task.username) {
|
|
||||||
const vidmoly = new VidmolyUploader();
|
|
||||||
await vidmoly.login(task.username, task.password);
|
|
||||||
result = await vidmoly.upload(task.file, progressCb, uploadSignalBundle.signal, throttle);
|
|
||||||
} else if (task.hoster === 'voe.sx' && task.username) {
|
|
||||||
const voe = new VoeUploader();
|
|
||||||
await voe.login(task.username, task.password);
|
|
||||||
result = await voe.upload(task.file, progressCb, uploadSignalBundle.signal, throttle);
|
|
||||||
} else if (task.hoster === 'doodstream.com' && task.username) {
|
|
||||||
const dood = new DoodstreamUploader();
|
|
||||||
await dood.login(task.username, task.password);
|
|
||||||
result = await dood.upload(task.file, progressCb, uploadSignalBundle.signal, throttle);
|
|
||||||
} else {
|
|
||||||
result = await uploadFile(task.hoster, task.file, task.apiKey, progressCb, uploadSignalBundle.signal, throttle);
|
|
||||||
}
|
|
||||||
|
|
||||||
const elapsed = Math.round((Date.now() - jobStart) / 1000);
|
const elapsed = Math.round((Date.now() - jobStart) / 1000);
|
||||||
this.sessionBytes += fileSize;
|
this.sessionBytes += fileSize;
|
||||||
@ -432,6 +423,83 @@ class UploadManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Account fallback: if this account hasn't failed before, try switching
|
||||||
|
if (task.accountId && !this._failedAccounts.has(task.hoster + ':' + task.accountId)) {
|
||||||
|
this._failedAccounts.set(task.hoster + ':' + task.accountId, true);
|
||||||
|
this.emit('account-failed', { hoster: task.hoster, accountId: task.accountId });
|
||||||
|
// Wait briefly for switchAccount() to be called from main process
|
||||||
|
await this._sleep(800, signal);
|
||||||
|
const override = this._accountOverrides.get(task.hoster);
|
||||||
|
if (override && !this._failedAccounts.has(task.hoster + ':' + override.id)) {
|
||||||
|
// Switch to fallback account and retry this file
|
||||||
|
task.accountId = override.id;
|
||||||
|
task.username = override.username;
|
||||||
|
task.password = override.password;
|
||||||
|
task.apiKey = override.apiKey;
|
||||||
|
this._emitProgress(uploadId, fileName, task.hoster, {
|
||||||
|
jobId, status: 'retrying', progress: 0, bytesUploaded: 0, bytesTotal: fileSize,
|
||||||
|
speedKbs: 0, elapsed: 0, remaining: 0,
|
||||||
|
error: 'Account-Wechsel zu Fallback', result: null, attempt: 1, maxAttempts
|
||||||
|
});
|
||||||
|
// Re-run retry loop with new account
|
||||||
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
if (signal.aborted || this.stopAfterActive) break;
|
||||||
|
if (attempt > 1) {
|
||||||
|
this._emitProgress(uploadId, fileName, task.hoster, {
|
||||||
|
jobId, status: 'retrying', progress: 0, bytesUploaded: 0, bytesTotal: fileSize,
|
||||||
|
speedKbs: 0, elapsed: 0, remaining: 0,
|
||||||
|
error: lastError ? lastError.message : '', result: null, attempt, maxAttempts
|
||||||
|
});
|
||||||
|
await this._sleep(3000, signal);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const jobStart = Date.now();
|
||||||
|
let lastBytes = 0;
|
||||||
|
let lastSpeedTime = jobStart;
|
||||||
|
let currentSpeedKbs = 0;
|
||||||
|
this.activeJobs.set(uploadId, { jobId, speedKbs: 0, bytesUploaded: 0 });
|
||||||
|
|
||||||
|
const progressCb = (bytesUploaded, bytesTotal) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const timeDelta = (now - lastSpeedTime) / 1000;
|
||||||
|
if (timeDelta >= 1) {
|
||||||
|
currentSpeedKbs = Math.round((bytesUploaded - lastBytes) / timeDelta / 1024);
|
||||||
|
lastBytes = bytesUploaded;
|
||||||
|
lastSpeedTime = now;
|
||||||
|
}
|
||||||
|
this.activeJobs.set(uploadId, { jobId, speedKbs: currentSpeedKbs, bytesUploaded });
|
||||||
|
const elapsed = Math.round((now - jobStart) / 1000);
|
||||||
|
const remaining = currentSpeedKbs > 0 ? Math.round((bytesTotal - bytesUploaded) / (currentSpeedKbs * 1024)) : 0;
|
||||||
|
this._emitProgress(uploadId, fileName, task.hoster, {
|
||||||
|
jobId, status: 'uploading',
|
||||||
|
progress: bytesTotal > 0 ? Math.min(1, bytesUploaded / bytesTotal) : 0,
|
||||||
|
bytesUploaded, bytesTotal, speedKbs: currentSpeedKbs,
|
||||||
|
elapsed, remaining, error: null, result: null, attempt, maxAttempts
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const hosterThrottle = settings.maxSpeedKbs > 0 ? new Throttle(settings.maxSpeedKbs * 1024) : null;
|
||||||
|
const globalThrottle = this._getGlobalThrottle();
|
||||||
|
const throttle = hosterThrottle && globalThrottle
|
||||||
|
? { consume: async (bytes, sig) => { await hosterThrottle.consume(bytes, sig); await globalThrottle.consume(bytes, sig); } }
|
||||||
|
: hosterThrottle || globalThrottle;
|
||||||
|
|
||||||
|
const result = await this._executeUpload(task, progressCb, signal, throttle);
|
||||||
|
this.activeJobs.delete(uploadId);
|
||||||
|
this.sessionBytes += fileSize;
|
||||||
|
emitFinalStatus('done', { result, speedKbs: currentSpeedKbs, elapsed: Math.round((Date.now() - jobStart) / 1000), attempt });
|
||||||
|
recordFinalResult('done', { result });
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
this.activeJobs.delete(uploadId);
|
||||||
|
lastError = err;
|
||||||
|
if (signal.aborted || this.stopAfterActive) break;
|
||||||
|
if (attempt >= maxAttempts) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const error = lastError && lastError.message ? lastError.message : 'Unbekannter Fehler';
|
const error = lastError && lastError.message ? lastError.message : 'Unbekannter Fehler';
|
||||||
emitFinalStatus('error', { error });
|
emitFinalStatus('error', { error });
|
||||||
recordFinalResult('error', { error });
|
recordFinalResult('error', { error });
|
||||||
@ -453,6 +521,24 @@ class UploadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _executeUpload(task, progressCb, signal, throttle) {
|
||||||
|
if (task.hoster === 'vidmoly.me' && task.username) {
|
||||||
|
const vidmoly = new VidmolyUploader();
|
||||||
|
await vidmoly.login(task.username, task.password);
|
||||||
|
return vidmoly.upload(task.file, progressCb, signal, throttle);
|
||||||
|
} else if (task.hoster === 'voe.sx' && task.username) {
|
||||||
|
const voe = new VoeUploader();
|
||||||
|
await voe.login(task.username, task.password);
|
||||||
|
return voe.upload(task.file, progressCb, signal, throttle);
|
||||||
|
} else if (task.hoster === 'doodstream.com' && task.username) {
|
||||||
|
const dood = new DoodstreamUploader();
|
||||||
|
await dood.login(task.username, task.password);
|
||||||
|
return dood.upload(task.file, progressCb, signal, throttle);
|
||||||
|
} else {
|
||||||
|
return uploadFile(task.hoster, task.file, task.apiKey, progressCb, signal, throttle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_emitProgress(uploadId, fileName, hoster, data) {
|
_emitProgress(uploadId, fileName, hoster, data) {
|
||||||
this.emit('progress', { uploadId, fileName, hoster, ...data });
|
this.emit('progress', { uploadId, fileName, hoster, ...data });
|
||||||
}
|
}
|
||||||
|
|||||||
243
main.js
243
main.js
@ -115,82 +115,68 @@ function appendUploadLog(hoster, link, fileName) {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Multi-account helpers ---
|
||||||
|
function hosterAccountHasCreds(name, account) {
|
||||||
|
if (!account) return false;
|
||||||
|
if (account.authType === 'api') return !!account.apiKey;
|
||||||
|
if (account.authType === 'login') return !!(account.username && account.password);
|
||||||
|
// Fallback for old format
|
||||||
|
if (name === 'vidmoly.me') return !!(account.username && account.password);
|
||||||
|
if (name === 'voe.sx' || name === 'doodstream.com') return !!(account.username && account.password) || !!account.apiKey;
|
||||||
|
return !!account.apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPrimaryAccount(config, hosterName) {
|
||||||
|
const accounts = config.hosters[hosterName];
|
||||||
|
if (!Array.isArray(accounts)) return null;
|
||||||
|
return accounts.find(a => a.enabled !== false && hosterAccountHasCreds(hosterName, a)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextFallbackAccount(config, hosterName, failedAccountId) {
|
||||||
|
const accounts = config.hosters[hosterName];
|
||||||
|
if (!Array.isArray(accounts)) return null;
|
||||||
|
const failedIndex = accounts.findIndex(a => a.id === failedAccountId);
|
||||||
|
if (failedIndex < 0) return null;
|
||||||
|
for (let i = failedIndex + 1; i < accounts.length; i++) {
|
||||||
|
if (accounts[i].enabled !== false && hosterAccountHasCreds(hosterName, accounts[i])) {
|
||||||
|
return accounts[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTaskFromAccount(hoster, account, extra) {
|
||||||
|
const task = { ...extra, hoster, accountId: account.id };
|
||||||
|
if (account.authType === 'api' && account.apiKey) {
|
||||||
|
task.apiKey = account.apiKey;
|
||||||
|
} else if (account.username && account.password) {
|
||||||
|
task.username = account.username;
|
||||||
|
task.password = account.password;
|
||||||
|
} else if (account.apiKey) {
|
||||||
|
task.apiKey = account.apiKey;
|
||||||
|
}
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
function buildUploadTasks(config, files, hosters) {
|
function buildUploadTasks(config, files, hosters) {
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
for (const hoster of hosters) {
|
for (const hoster of hosters) {
|
||||||
const hosterConfig = config.hosters[hoster];
|
const account = getPrimaryAccount(config, hoster);
|
||||||
if (!hosterConfig) {
|
if (!account) { debugLog(` skip ${hoster}: no enabled account with creds`); continue; }
|
||||||
debugLog(` skip ${hoster}: no config`);
|
tasks.push(buildTaskFromAccount(hoster, account, { file }));
|
||||||
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;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUploadTasksFromJobs(config, jobs) {
|
function buildUploadTasksFromJobs(config, jobs) {
|
||||||
if (!Array.isArray(jobs)) return [];
|
if (!Array.isArray(jobs)) return [];
|
||||||
|
|
||||||
return jobs.flatMap((job) => {
|
return jobs.flatMap((job) => {
|
||||||
if (!job || !job.file || !job.hoster) return [];
|
if (!job || !job.file || !job.hoster) return [];
|
||||||
const hosterConfig = config.hosters[job.hoster];
|
const account = getPrimaryAccount(config, job.hoster);
|
||||||
if (!hosterConfig) {
|
if (!account) { debugLog(` skip ${job.hoster}: no enabled account`); return []; }
|
||||||
debugLog(` skip ${job.hoster}: no config for queued job`);
|
return [buildTaskFromAccount(job.hoster, account, { file: job.file, jobId: job.id || job.jobId || null })];
|
||||||
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 }];
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,6 +346,13 @@ async function checkByseHealth(hosterConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const msg = String(serverPayload.msg || serverPayload.message || '').trim();
|
const msg = String(serverPayload.msg || serverPayload.message || '').trim();
|
||||||
|
|
||||||
|
// Byse API returns { msg: "OK", result: <server-url> } on success.
|
||||||
|
// If msg is "OK" but result wasn't a valid URL, treat as success with warning.
|
||||||
|
if (/^ok$/i.test(msg)) {
|
||||||
|
return { status: 'ok', message: 'API Key gültig' };
|
||||||
|
}
|
||||||
|
|
||||||
if (msg) {
|
if (msg) {
|
||||||
return { status: 'error', message: msg };
|
return { status: 'error', message: msg };
|
||||||
}
|
}
|
||||||
@ -367,75 +360,69 @@ async function checkByseHealth(hosterConfig) {
|
|||||||
return { status: 'error', message: 'API Key ungültig oder Server nicht erreichbar' };
|
return { status: 'error', message: 'API Key ungültig oder Server nicht erreichbar' };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runHosterHealthCheck(config, requestedHosters) {
|
// requestedChecks can be:
|
||||||
|
// - array of strings (hoster names) for legacy/all-accounts check
|
||||||
|
// - array of { hoster, accountId } for specific account checks
|
||||||
|
async function runHosterHealthCheck(config, requestedChecks) {
|
||||||
const allowed = ['doodstream.com', 'vidmoly.me', 'voe.sx', 'byse.sx'];
|
const allowed = ['doodstream.com', 'vidmoly.me', 'voe.sx', 'byse.sx'];
|
||||||
const source = Array.isArray(requestedHosters) && requestedHosters.length > 0
|
|
||||||
? requestedHosters
|
|
||||||
: allowed;
|
|
||||||
|
|
||||||
const hosters = source
|
// Normalize input to [{ hoster, accountId? }]
|
||||||
.map((name) => String(name || '').trim())
|
let checks;
|
||||||
.filter((name, index, arr) => name && arr.indexOf(name) === index);
|
if (!Array.isArray(requestedChecks) || requestedChecks.length === 0) {
|
||||||
|
// Check all accounts for all hosters
|
||||||
const checks = hosters.map(async (hoster) => {
|
checks = [];
|
||||||
if (!allowed.includes(hoster)) {
|
for (const name of allowed) {
|
||||||
return { hoster, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
|
const accounts = config.hosters[name];
|
||||||
|
if (Array.isArray(accounts)) {
|
||||||
|
for (const acc of accounts) {
|
||||||
|
if (hosterAccountHasCreds(name, acc)) checks.push({ hoster: name, accountId: acc.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof requestedChecks[0] === 'string') {
|
||||||
|
// Legacy: array of hoster names — check all accounts for each
|
||||||
|
checks = [];
|
||||||
|
for (const name of requestedChecks) {
|
||||||
|
const accounts = config.hosters[name];
|
||||||
|
if (Array.isArray(accounts)) {
|
||||||
|
for (const acc of accounts) {
|
||||||
|
if (hosterAccountHasCreds(name, acc)) checks.push({ hoster: name, accountId: acc.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
checks = requestedChecks;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hosterConfig = config && config.hosters ? config.hosters[hoster] : null;
|
const results = await Promise.all(checks.map(async ({ hoster, accountId }) => {
|
||||||
|
if (!allowed.includes(hoster)) {
|
||||||
|
return { hoster, accountId, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find specific account
|
||||||
|
const accounts = config.hosters[hoster];
|
||||||
|
const hosterConfig = Array.isArray(accounts) ? accounts.find(a => a.id === accountId) : null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let result;
|
||||||
if (hoster === 'doodstream.com') {
|
if (hoster === 'doodstream.com') {
|
||||||
const result = await withTimeout(
|
result = await withTimeout(checkDoodstreamHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Doodstream-Check');
|
||||||
checkDoodstreamHealth(hosterConfig),
|
} else if (hoster === 'vidmoly.me') {
|
||||||
HEALTH_CHECK_TIMEOUT,
|
result = await withTimeout(checkVidmolyHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Vidmoly-Check');
|
||||||
'Doodstream-Check'
|
} else if (hoster === 'voe.sx') {
|
||||||
);
|
result = await withTimeout(checkVoeHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'VOE-Check');
|
||||||
return { hoster, ...result };
|
} else if (hoster === 'byse.sx') {
|
||||||
|
result = await withTimeout(checkByseHealth(hosterConfig), HEALTH_CHECK_TIMEOUT, 'Byse-Check');
|
||||||
|
} else {
|
||||||
|
return { hoster, accountId, status: 'skipped', message: 'Kein Health-Check fuer diesen Hoster' };
|
||||||
}
|
}
|
||||||
|
return { hoster, accountId, ...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) {
|
} catch (err) {
|
||||||
return {
|
return { hoster, accountId, status: 'error', message: err && err.message ? err.message : 'Health-Check fehlgeschlagen' };
|
||||||
hoster,
|
|
||||||
status: 'error',
|
|
||||||
message: err && err.message ? err.message : 'Health-Check fehlgeschlagen'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
|
||||||
const results = await Promise.all(checks);
|
return { checkedAt: new Date().toISOString(), results };
|
||||||
return {
|
|
||||||
checkedAt: new Date().toISOString(),
|
|
||||||
results
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
@ -680,6 +667,20 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
uploadManager.on('account-failed', ({ hoster, accountId }) => {
|
||||||
|
const cfg = configStore.load();
|
||||||
|
const fallback = getNextFallbackAccount(cfg, hoster, accountId);
|
||||||
|
if (fallback) {
|
||||||
|
debugLog(`account-failed: ${hoster} ${accountId} → fallback to ${fallback.id}`);
|
||||||
|
uploadManager.switchAccount(hoster, fallback);
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('account-switched', { hoster, fromAccountId: accountId, toAccountId: fallback.id });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugLog(`account-failed: ${hoster} ${accountId} → no fallback available`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
uploadManager.on('batch-done', async (summary) => {
|
uploadManager.on('batch-done', async (summary) => {
|
||||||
debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`);
|
debugLog(`batch-done: total=${summary.total} ok=${summary.succeeded} fail=${summary.failed}`);
|
||||||
await configStore.appendHistory(summary);
|
await configStore.appendHistory(summary);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "1.9.8",
|
"version": "2.0.0",
|
||||||
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -64,6 +64,11 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
ipcRenderer.on('folder-monitor:new-files', (_event, data) => callback(data));
|
ipcRenderer.on('folder-monitor:new-files', (_event, data) => callback(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Account switched event
|
||||||
|
onAccountSwitched: (callback) => {
|
||||||
|
ipcRenderer.on('account-switched', (_event, data) => callback(data));
|
||||||
|
},
|
||||||
|
|
||||||
// Drop Target
|
// Drop Target
|
||||||
showDropTarget: () => ipcRenderer.invoke('show-drop-target'),
|
showDropTarget: () => ipcRenderer.invoke('show-drop-target'),
|
||||||
hideDropTarget: () => ipcRenderer.invoke('hide-drop-target'),
|
hideDropTarget: () => ipcRenderer.invoke('hide-drop-target'),
|
||||||
@ -99,5 +104,6 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
ipcRenderer.removeAllListeners('shutdown-countdown');
|
ipcRenderer.removeAllListeners('shutdown-countdown');
|
||||||
ipcRenderer.removeAllListeners('folder-monitor:new-files');
|
ipcRenderer.removeAllListeners('folder-monitor:new-files');
|
||||||
ipcRenderer.removeAllListeners('drop-target:files');
|
ipcRenderer.removeAllListeners('drop-target:files');
|
||||||
|
ipcRenderer.removeAllListeners('account-switched');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
494
renderer/app.js
494
renderer/app.js
@ -1,5 +1,15 @@
|
|||||||
const HOSTERS = ['doodstream.com', 'voe.sx', 'vidmoly.me', 'byse.sx'];
|
const HOSTERS = ['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' }
|
||||||
|
];
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
let selectedFiles = []; // { path, name, size }
|
let selectedFiles = []; // { path, name, size }
|
||||||
let selectedUploadHosters = [];
|
let selectedUploadHosters = [];
|
||||||
@ -7,8 +17,8 @@ let config = { hosters: {}, hosterSettings: {}, globalSettings: {} };
|
|||||||
let hosterSettings = {};
|
let hosterSettings = {};
|
||||||
let uploading = false;
|
let uploading = false;
|
||||||
let healthCheckRunning = false;
|
let healthCheckRunning = false;
|
||||||
let accountStatuses = {}; // { 'voe.sx': { status: 'ok'|'warn'|'error'|'checking'|'unchecked', message: '' } }
|
let accountStatuses = {}; // { accountId: { status: 'ok'|'warn'|'error'|'checking'|'unchecked', message: '' } }
|
||||||
let editingAccountHoster = null; // null = adding, string = editing
|
let editingAccountId = null; // null = adding, string = editing account by ID
|
||||||
let autoHealthCheckEnabled = true;
|
let autoHealthCheckEnabled = true;
|
||||||
let queuePersistTimer = null;
|
let queuePersistTimer = null;
|
||||||
let settingsSaveTimer = null;
|
let settingsSaveTimer = null;
|
||||||
@ -108,6 +118,11 @@ async function init() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Account switched notification
|
||||||
|
window.api.onAccountSwitched((data) => {
|
||||||
|
window.api.debugLog(`account-switched: ${data.hoster} ${data.fromAccountId} -> ${data.toAccountId}`);
|
||||||
|
});
|
||||||
|
|
||||||
// Drop target window: files dropped on the small floating window
|
// Drop target window: files dropped on the small floating window
|
||||||
window.api.onDropTargetFiles((paths) => {
|
window.api.onDropTargetFiles((paths) => {
|
||||||
addPathsToQueue(paths);
|
addPathsToQueue(paths);
|
||||||
@ -136,19 +151,26 @@ document.querySelectorAll('.tab').forEach(tab => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Hoster selection ---
|
// --- Hoster selection ---
|
||||||
function hosterHasCredentials(name, hoster) {
|
function accountHasCreds(name, account) {
|
||||||
if (name === 'vidmoly.me') return !!(hoster.username && hoster.password);
|
if (!account) return false;
|
||||||
if (name === 'voe.sx' || name === 'doodstream.com') return !!(hoster.username && hoster.password) || !!hoster.apiKey;
|
if (account.authType === 'api') return !!account.apiKey;
|
||||||
return !!hoster.apiKey;
|
if (account.authType === 'login') return !!(account.username && account.password);
|
||||||
|
// Fallback
|
||||||
|
if (name === 'vidmoly.me') return !!(account.username && account.password);
|
||||||
|
if (name === 'voe.sx' || name === 'doodstream.com') return !!(account.username && account.password) || !!account.apiKey;
|
||||||
|
return !!account.apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns hosters that have at least one enabled account with credentials
|
||||||
function getAvailableHosters() {
|
function getAvailableHosters() {
|
||||||
return HOSTERS
|
const result = [];
|
||||||
.map(name => {
|
for (const name of HOSTERS) {
|
||||||
const hoster = config.hosters[name] || {};
|
const accounts = config.hosters[name];
|
||||||
return { name, hoster, hasCreds: hosterHasCredentials(name, hoster) };
|
if (!Array.isArray(accounts)) continue;
|
||||||
})
|
const hasEnabledAccount = accounts.some(a => a.enabled !== false && accountHasCreds(name, a));
|
||||||
.filter(item => item.hasCreds && item.hoster.enabled !== false);
|
if (hasEnabledAccount) result.push({ name });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncSelectedUploadHosters() {
|
function syncSelectedUploadHosters() {
|
||||||
@ -156,8 +178,8 @@ function syncSelectedUploadHosters() {
|
|||||||
selectedUploadHosters = selectedUploadHosters.filter(name => available.has(name));
|
selectedUploadHosters = selectedUploadHosters.filter(name => available.has(name));
|
||||||
if (selectedUploadHosters.length === 0) {
|
if (selectedUploadHosters.length === 0) {
|
||||||
selectedUploadHosters = HOSTERS.filter(name => {
|
selectedUploadHosters = HOSTERS.filter(name => {
|
||||||
const hoster = config.hosters[name] || {};
|
const accounts = config.hosters[name];
|
||||||
return !!hoster.enabled && hosterHasCredentials(name, hoster);
|
return Array.isArray(accounts) && accounts.some(a => a.enabled !== false && accountHasCreds(name, a));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -176,24 +198,17 @@ function getHosterLabel(name) {
|
|||||||
return labels[name] || name;
|
return labels[name] || name;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAccountModeParts(name, hoster) {
|
function getAccountAuthLabel(account) {
|
||||||
if (!hoster) return [];
|
if (!account) return '';
|
||||||
const hasLogin = !!(hoster.username && hoster.password);
|
if (account.authType === 'api') return 'API';
|
||||||
const hasApi = !!hoster.apiKey;
|
if (account.authType === 'login') return 'Web Login';
|
||||||
|
return '';
|
||||||
if (name === 'vidmoly.me') return hasLogin ? ['Login Web'] : [];
|
|
||||||
if (name === 'byse.sx') return hasApi ? ['API'] : [];
|
|
||||||
|
|
||||||
const parts = [];
|
|
||||||
if (hasLogin) parts.push('Login Web');
|
|
||||||
if (hasApi) parts.push('API');
|
|
||||||
return parts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAccountDisplayName(name, hoster) {
|
function getAccountDisplayName(name, account) {
|
||||||
const parts = getAccountModeParts(name, hoster);
|
const authLabel = getAccountAuthLabel(account);
|
||||||
return parts.length > 0
|
return authLabel
|
||||||
? `${getHosterLabel(name)} (${parts.join(' + ')})`
|
? `${getHosterLabel(name)} (${authLabel})`
|
||||||
: getHosterLabel(name);
|
: getHosterLabel(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,14 +221,43 @@ function maskCredential(value, keep = 4) {
|
|||||||
|
|
||||||
function ensureAccountStatusEntries() {
|
function ensureAccountStatusEntries() {
|
||||||
const nextStatuses = {};
|
const nextStatuses = {};
|
||||||
for (const { name } of getAccountsWithCreds()) {
|
for (const { account } of getAllAccountsFlat()) {
|
||||||
nextStatuses[name] = accountStatuses[name] || { status: 'unchecked', message: '' };
|
if (account.id) {
|
||||||
|
nextStatuses[account.id] = accountStatuses[account.id] || { status: 'unchecked', message: '' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
accountStatuses = nextStatuses;
|
accountStatuses = nextStatuses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns flat array of all accounts: [{ name, account, index }]
|
||||||
|
function getAllAccountsFlat() {
|
||||||
|
const result = [];
|
||||||
|
for (const name of HOSTERS) {
|
||||||
|
const accounts = config.hosters[name];
|
||||||
|
if (!Array.isArray(accounts)) continue;
|
||||||
|
accounts.forEach((account, index) => result.push({ name, account, index }));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns flat array of accounts with credentials
|
||||||
|
function getAccountsWithCredsFlat() {
|
||||||
|
return getAllAccountsFlat().filter(({ name, account }) => accountHasCreds(name, account));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find account by ID across all hosters
|
||||||
|
function findAccountById(accountId) {
|
||||||
|
for (const name of HOSTERS) {
|
||||||
|
const accounts = config.hosters[name];
|
||||||
|
if (!Array.isArray(accounts)) continue;
|
||||||
|
const account = accounts.find(a => a.id === accountId);
|
||||||
|
if (account) return { name, account };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleStartupAccountCheck() {
|
function scheduleStartupAccountCheck() {
|
||||||
const accounts = getAccountsWithCreds();
|
const accounts = getAccountsWithCredsFlat();
|
||||||
if (!accounts.length) return;
|
if (!accounts.length) return;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
runHealthCheck('startup').catch(() => {});
|
runHealthCheck('startup').catch(() => {});
|
||||||
@ -227,7 +271,7 @@ function renderHosterSummary() {
|
|||||||
if (hosters.length === 0) {
|
if (hosters.length === 0) {
|
||||||
summary.textContent = 'Keine Upload-Ziele ausgewählt';
|
summary.textContent = 'Keine Upload-Ziele ausgewählt';
|
||||||
} else if (hosters.length === 1) {
|
} else if (hosters.length === 1) {
|
||||||
summary.textContent = `Aktives Ziel: ${getAccountDisplayName(hosters[0], config.hosters[hosters[0]] || {})}`;
|
summary.textContent = `Aktives Ziel: ${getHosterLabel(hosters[0])}`;
|
||||||
} else {
|
} else {
|
||||||
summary.textContent = `${hosters.length} Ziele aktiv: ${hosters.map((name) => getHosterLabel(name)).join(', ')}`;
|
summary.textContent = `${hosters.length} Ziele aktiv: ${hosters.map((name) => getHosterLabel(name)).join(', ')}`;
|
||||||
}
|
}
|
||||||
@ -247,17 +291,21 @@ function renderHosterModal() {
|
|||||||
|
|
||||||
list.innerHTML = available.map(item => {
|
list.innerHTML = available.map(item => {
|
||||||
const checked = selectedUploadHosters.includes(item.name);
|
const checked = selectedUploadHosters.includes(item.name);
|
||||||
const h = config.hosters[item.name] || {};
|
// Get first enabled account's status for subtitle
|
||||||
const st = accountStatuses[item.name];
|
const accounts = config.hosters[item.name] || [];
|
||||||
const subtitle = st && st.status === 'ok' ? 'Bereit'
|
const enabledAccounts = accounts.filter(a => a.enabled !== false && accountHasCreds(item.name, a));
|
||||||
: st && st.status === 'warn' ? 'Prüfung mit Warnung'
|
const accountCount = enabledAccounts.length;
|
||||||
: st && st.status === 'error' ? 'Login-Fehler'
|
let subtitle = `${accountCount} Account${accountCount !== 1 ? 's' : ''}`;
|
||||||
: `${getCredentialLabel(item.name, h)} hinterlegt`;
|
// Check if any account has ok status
|
||||||
|
const hasOk = enabledAccounts.some(a => accountStatuses[a.id] && accountStatuses[a.id].status === 'ok');
|
||||||
|
const hasError = enabledAccounts.some(a => accountStatuses[a.id] && accountStatuses[a.id].status === 'error');
|
||||||
|
if (hasOk) subtitle += ' • Bereit';
|
||||||
|
else if (hasError) subtitle += ' • Fehler';
|
||||||
return `
|
return `
|
||||||
<label class="hoster-option${checked ? ' selected' : ''}" data-hoster-option="${item.name}">
|
<label class="hoster-option${checked ? ' selected' : ''}" data-hoster-option="${item.name}">
|
||||||
<input type="checkbox" data-hoster-modal="${item.name}" ${checked ? 'checked' : ''}>
|
<input type="checkbox" data-hoster-modal="${item.name}" ${checked ? 'checked' : ''}>
|
||||||
<div class="hoster-option-main">
|
<div class="hoster-option-main">
|
||||||
<div class="hoster-option-title">${escapeHtml(getAccountDisplayName(item.name, h))}</div>
|
<div class="hoster-option-title">${escapeHtml(getHosterLabel(item.name))}</div>
|
||||||
<div class="hoster-option-subtitle">${subtitle}</div>
|
<div class="hoster-option-subtitle">${subtitle}</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@ -1459,7 +1507,7 @@ async function retrySelectedJobs() {
|
|||||||
const retryJobs = [];
|
const retryJobs = [];
|
||||||
queueJobs.forEach(j => {
|
queueJobs.forEach(j => {
|
||||||
if (selectedJobIds.has(j.id) && ['error', 'done', 'aborted', 'skipped'].includes(j.status)) {
|
if (selectedJobIds.has(j.id) && ['error', 'done', 'aborted', 'skipped'].includes(j.status)) {
|
||||||
j.status = 'preview';
|
j.status = uploading ? 'queued' : 'preview';
|
||||||
j.error = null;
|
j.error = null;
|
||||||
j.result = null;
|
j.result = null;
|
||||||
j.bytesUploaded = 0;
|
j.bytesUploaded = 0;
|
||||||
@ -1720,11 +1768,14 @@ async function executeHealthCheck(hosters, mode) {
|
|||||||
const result = await window.api.runHealthCheck({ hosters });
|
const result = await window.api.runHealthCheck({ hosters });
|
||||||
const rows = result && Array.isArray(result.results) ? result.results : [];
|
const rows = result && Array.isArray(result.results) ? result.results : [];
|
||||||
rows.forEach((row) => {
|
rows.forEach((row) => {
|
||||||
if (!row || !row.hoster) return;
|
if (!row) return;
|
||||||
accountStatuses[row.hoster] = {
|
const key = row.accountId || row.hoster;
|
||||||
|
if (key) {
|
||||||
|
accountStatuses[key] = {
|
||||||
status: row.status || 'unchecked',
|
status: row.status || 'unchecked',
|
||||||
message: row.message || ''
|
message: row.message || ''
|
||||||
};
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
renderHealthCheckResults(rows);
|
renderHealthCheckResults(rows);
|
||||||
renderAccounts();
|
renderAccounts();
|
||||||
@ -1734,17 +1785,25 @@ async function executeHealthCheck(hosters, mode) {
|
|||||||
|
|
||||||
async function runHealthCheck(mode = 'manual', requestedHosters = null) {
|
async function runHealthCheck(mode = 'manual', requestedHosters = null) {
|
||||||
if (healthCheckRunning || (uploading && mode === 'manual')) return [];
|
if (healthCheckRunning || (uploading && mode === 'manual')) return [];
|
||||||
const hosters = Array.isArray(requestedHosters) && requestedHosters.length > 0
|
// Build check list: all enabled accounts with creds
|
||||||
? requestedHosters
|
let hosters;
|
||||||
: HOSTERS.filter((name) => hosterHasCredentials(name, config.hosters[name] || {}));
|
if (Array.isArray(requestedHosters) && requestedHosters.length > 0) {
|
||||||
|
hosters = requestedHosters;
|
||||||
|
} else {
|
||||||
|
hosters = getAccountsWithCredsFlat()
|
||||||
|
.filter(({ account }) => account.enabled !== false)
|
||||||
|
.map(({ name, account }) => ({ hoster: name, accountId: account.id }));
|
||||||
|
}
|
||||||
if (hosters.length === 0) {
|
if (hosters.length === 0) {
|
||||||
if (mode === 'manual') alert('Keine Hoster mit Zugangsdaten für einen Check.');
|
if (mode === 'manual') alert('Keine Hoster mit Zugangsdaten für einen Check.');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
healthCheckRunning = true;
|
healthCheckRunning = true;
|
||||||
hosters.forEach((hoster) => {
|
// Mark all accounts as checking
|
||||||
accountStatuses[hoster] = { status: 'checking', message: '' };
|
for (const h of hosters) {
|
||||||
});
|
const key = typeof h === 'string' ? h : (h.accountId || h.hoster);
|
||||||
|
accountStatuses[key] = { status: 'checking', message: '' };
|
||||||
|
}
|
||||||
renderAccounts();
|
renderAccounts();
|
||||||
try {
|
try {
|
||||||
return await executeHealthCheck(hosters, mode);
|
return await executeHealthCheck(hosters, mode);
|
||||||
@ -1763,7 +1822,7 @@ function renderSettings() {
|
|||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
const globalSettings = config.globalSettings || {};
|
const globalSettings = config.globalSettings || {};
|
||||||
const configuredAccounts = getAccountsWithCreds();
|
const configuredAccounts = getAvailableHosters();
|
||||||
const generalPanel = document.createElement('div');
|
const generalPanel = document.createElement('div');
|
||||||
generalPanel.className = 'hoster-settings-panel';
|
generalPanel.className = 'hoster-settings-panel';
|
||||||
generalPanel.innerHTML = `
|
generalPanel.innerHTML = `
|
||||||
@ -1969,7 +2028,7 @@ function renderSettings() {
|
|||||||
container.appendChild(empty);
|
container.appendChild(empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const { name, hoster } of configuredAccounts) {
|
for (const { name } of configuredAccounts) {
|
||||||
const hs = hosterSettings[name] || {};
|
const hs = hosterSettings[name] || {};
|
||||||
const maxSpeedMbs = hs.maxSpeedKbs > 0 ? (hs.maxSpeedKbs / 1024).toFixed(2).replace(/\.00$/, '') : '0';
|
const maxSpeedMbs = hs.maxSpeedKbs > 0 ? (hs.maxSpeedKbs / 1024).toFixed(2).replace(/\.00$/, '') : '0';
|
||||||
|
|
||||||
@ -1979,7 +2038,7 @@ function renderSettings() {
|
|||||||
panel.innerHTML = `
|
panel.innerHTML = `
|
||||||
<div class="hoster-panel-header" data-hoster="${name}">
|
<div class="hoster-panel-header" data-hoster="${name}">
|
||||||
<span class="panel-arrow">▶</span>
|
<span class="panel-arrow">▶</span>
|
||||||
<span class="panel-title">${escapeHtml(getAccountDisplayName(name, hoster))}</span>
|
<span class="panel-title">${escapeHtml(getHosterLabel(name))}</span>
|
||||||
<span class="panel-status active">Aktiv</span>
|
<span class="panel-status active">Aktiv</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hoster-panel-body" data-panel="${name}" style="display:none">
|
<div class="hoster-panel-body" data-panel="${name}" style="display:none">
|
||||||
@ -2155,25 +2214,14 @@ async function saveSettings(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Accounts ---
|
// --- Accounts ---
|
||||||
function getAccountsWithCreds() {
|
function getCredentialLabel(name, account) {
|
||||||
return HOSTERS
|
if (!account) return 'Keine Zugangsdaten';
|
||||||
.map(name => ({ name, hoster: config.hosters[name] || {} }))
|
if (account.authType === 'api') return `API: ${maskCredential(account.apiKey) || 'nicht gesetzt'}`;
|
||||||
.filter(item => hosterHasCredentials(item.name, item.hoster));
|
if (account.authType === 'login') return `Login: ${account.username || 'nicht gesetzt'}`;
|
||||||
}
|
// Fallback
|
||||||
|
if (account.username && account.password) return `Login: ${account.username}`;
|
||||||
function getHostersWithoutCreds() {
|
if (account.apiKey) return `API: ${maskCredential(account.apiKey)}`;
|
||||||
return HOSTERS.filter(name => !hosterHasCredentials(name, config.hosters[name] || {}));
|
return 'Keine Zugangsdaten';
|
||||||
}
|
|
||||||
|
|
||||||
function getCredentialLabel(name, hoster) {
|
|
||||||
if (name === 'vidmoly.me') return `Login: ${hoster.username || 'nicht gesetzt'}`;
|
|
||||||
if (name === 'voe.sx' || name === 'doodstream.com') {
|
|
||||||
const parts = [];
|
|
||||||
if (hoster.username && hoster.password) parts.push(`Login: ${hoster.username}`);
|
|
||||||
if (hoster.apiKey) parts.push(`API: ${maskCredential(hoster.apiKey)}`);
|
|
||||||
return parts.join(' • ') || 'Keine Zugangsdaten';
|
|
||||||
}
|
|
||||||
return `API: ${maskCredential(hoster.apiKey) || 'nicht gesetzt'}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAccounts() {
|
function renderAccounts() {
|
||||||
@ -2181,11 +2229,11 @@ function renderAccounts() {
|
|||||||
if (!container) return;
|
if (!container) return;
|
||||||
ensureAccountStatusEntries();
|
ensureAccountStatusEntries();
|
||||||
|
|
||||||
const accounts = getAccountsWithCreds();
|
const allAccounts = getAllAccountsFlat();
|
||||||
const runCheckBtn = document.getElementById('accountsRunHealthCheckBtn');
|
const runCheckBtn = document.getElementById('accountsRunHealthCheckBtn');
|
||||||
if (runCheckBtn) runCheckBtn.disabled = healthCheckRunning;
|
if (runCheckBtn) runCheckBtn.disabled = healthCheckRunning;
|
||||||
|
|
||||||
if (accounts.length === 0) {
|
if (allAccounts.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="accounts-empty">
|
<div class="accounts-empty">
|
||||||
<p>Keine Accounts vorhanden</p>
|
<p>Keine Accounts vorhanden</p>
|
||||||
@ -2194,19 +2242,34 @@ function renderAccounts() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = accounts.map(({ name, hoster }) => {
|
// Group by hoster for drag reorder sections
|
||||||
const isDisabled = hoster.enabled === false;
|
const byHoster = {};
|
||||||
const st = accountStatuses[name] || { status: 'unchecked', message: '' };
|
for (const { name, account } of allAccounts) {
|
||||||
|
if (!byHoster[name]) byHoster[name] = [];
|
||||||
|
byHoster[name].push(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
for (const name of HOSTERS) {
|
||||||
|
const accounts = byHoster[name];
|
||||||
|
if (!accounts || accounts.length === 0) continue;
|
||||||
|
html += `<div class="account-hoster-group" data-hoster-group="${name}">
|
||||||
|
<div class="account-hoster-group-title">${escapeHtml(getHosterLabel(name))}</div>`;
|
||||||
|
accounts.forEach((account, idx) => {
|
||||||
|
const isDisabled = account.enabled === false;
|
||||||
|
const st = accountStatuses[account.id] || { status: 'unchecked', message: '' };
|
||||||
const statusLabels = { ok: 'Bereit', warn: 'Warnung', checking: 'Prüfe...', error: 'Fehler', unchecked: 'Nicht geprüft' };
|
const statusLabels = { ok: 'Bereit', warn: 'Warnung', checking: 'Prüfe...', error: 'Fehler', unchecked: 'Nicht geprüft' };
|
||||||
const statusLabel = isDisabled ? 'Deaktiviert' : (statusLabels[st.status] || 'Nicht geprüft');
|
const statusLabel = isDisabled ? 'Deaktiviert' : (statusLabels[st.status] || 'Nicht geprüft');
|
||||||
const statusClass = isDisabled ? 'disabled' : st.status;
|
const statusClass = isDisabled ? 'disabled' : st.status;
|
||||||
const credLabel = getCredentialLabel(name, hoster);
|
const credLabel = getCredentialLabel(name, account);
|
||||||
const toggleLabel = isDisabled ? 'Aktivieren' : 'Deaktivieren';
|
const toggleLabel = isDisabled ? 'Aktivieren' : 'Deaktivieren';
|
||||||
|
const priorityLabel = idx === 0 ? 'Primär' : `Fallback #${idx}`;
|
||||||
|
|
||||||
return `
|
html += `
|
||||||
<div class="account-card${isDisabled ? ' account-disabled' : ''}" data-account="${name}">
|
<div class="account-card${isDisabled ? ' account-disabled' : ''}" data-account-id="${account.id}" data-account-hoster="${name}" draggable="true">
|
||||||
|
<div class="account-card-drag-handle" title="Ziehen zum Sortieren">☰</div>
|
||||||
<div class="account-card-info">
|
<div class="account-card-info">
|
||||||
<div class="account-card-title">${escapeHtml(getAccountDisplayName(name, hoster))}</div>
|
<div class="account-card-title">${escapeHtml(getAccountDisplayName(name, account))} <span class="account-priority-badge">${priorityLabel}</span></div>
|
||||||
<div class="account-card-subtitle" title="${escapeAttr(credLabel)}">${escapeHtml(credLabel)}${st.message && !isDisabled ? ` • ${escapeHtml(st.message)}` : ''}</div>
|
<div class="account-card-subtitle" title="${escapeAttr(credLabel)}">${escapeHtml(credLabel)}${st.message && !isDisabled ? ` • ${escapeHtml(st.message)}` : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="account-status status-${statusClass}">
|
<span class="account-status status-${statusClass}">
|
||||||
@ -2214,13 +2277,16 @@ function renderAccounts() {
|
|||||||
${statusLabel}
|
${statusLabel}
|
||||||
</span>
|
</span>
|
||||||
<div class="account-card-actions">
|
<div class="account-card-actions">
|
||||||
<button class="btn btn-xs btn-secondary" data-account-toggle="${name}">${toggleLabel}</button>
|
<button class="btn btn-xs btn-secondary" data-account-toggle="${account.id}">${toggleLabel}</button>
|
||||||
<button class="btn btn-xs btn-secondary" data-account-check="${name}" ${isDisabled ? 'disabled' : ''}>Prüfen</button>
|
<button class="btn btn-xs btn-secondary" data-account-check="${account.id}" ${isDisabled ? 'disabled' : ''}>Prüfen</button>
|
||||||
<button class="btn btn-xs btn-secondary" data-account-edit="${name}">Bearbeiten</button>
|
<button class="btn btn-xs btn-secondary" data-account-edit="${account.id}">Bearbeiten</button>
|
||||||
<button class="btn btn-xs btn-danger" data-account-delete="${name}">Löschen</button>
|
<button class="btn btn-xs btn-danger" data-account-delete="${account.id}">Löschen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
});
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
// Wire up buttons
|
// Wire up buttons
|
||||||
container.querySelectorAll('[data-account-toggle]').forEach(btn => {
|
container.querySelectorAll('[data-account-toggle]').forEach(btn => {
|
||||||
@ -2235,14 +2301,73 @@ function renderAccounts() {
|
|||||||
container.querySelectorAll('[data-account-check]').forEach(btn => {
|
container.querySelectorAll('[data-account-check]').forEach(btn => {
|
||||||
btn.addEventListener('click', () => checkSingleAccount(btn.dataset.accountCheck));
|
btn.addEventListener('click', () => checkSingleAccount(btn.dataset.accountCheck));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Drag-and-drop reorder within each hoster group
|
||||||
|
setupAccountDragReorder(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleAccount(hosterName) {
|
function setupAccountDragReorder(container) {
|
||||||
const hosters = { ...config.hosters };
|
let draggedCard = null;
|
||||||
const hoster = { ...hosters[hosterName] };
|
container.querySelectorAll('.account-card[draggable]').forEach(card => {
|
||||||
hoster.enabled = hoster.enabled === false ? true : false;
|
card.addEventListener('dragstart', (e) => {
|
||||||
hosters[hosterName] = hoster;
|
draggedCard = card;
|
||||||
await window.api.saveConfig({ hosters });
|
card.classList.add('dragging');
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
});
|
||||||
|
card.addEventListener('dragend', () => {
|
||||||
|
if (draggedCard) draggedCard.classList.remove('dragging');
|
||||||
|
draggedCard = null;
|
||||||
|
container.querySelectorAll('.account-card').forEach(c => c.classList.remove('drag-over-above', 'drag-over-below'));
|
||||||
|
});
|
||||||
|
card.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!draggedCard || draggedCard === card) return;
|
||||||
|
if (draggedCard.dataset.accountHoster !== card.dataset.accountHoster) return;
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const midY = rect.top + rect.height / 2;
|
||||||
|
card.classList.toggle('drag-over-above', e.clientY < midY);
|
||||||
|
card.classList.toggle('drag-over-below', e.clientY >= midY);
|
||||||
|
});
|
||||||
|
card.addEventListener('dragleave', () => {
|
||||||
|
card.classList.remove('drag-over-above', 'drag-over-below');
|
||||||
|
});
|
||||||
|
card.addEventListener('drop', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
card.classList.remove('drag-over-above', 'drag-over-below');
|
||||||
|
if (!draggedCard || draggedCard === card) return;
|
||||||
|
const hosterName = card.dataset.accountHoster;
|
||||||
|
if (draggedCard.dataset.accountHoster !== hosterName) return;
|
||||||
|
|
||||||
|
const draggedId = draggedCard.dataset.accountId;
|
||||||
|
const targetId = card.dataset.accountId;
|
||||||
|
const accounts = config.hosters[hosterName];
|
||||||
|
if (!Array.isArray(accounts)) return;
|
||||||
|
|
||||||
|
const fromIdx = accounts.findIndex(a => a.id === draggedId);
|
||||||
|
const toIdx = accounts.findIndex(a => a.id === targetId);
|
||||||
|
if (fromIdx < 0 || toIdx < 0) return;
|
||||||
|
|
||||||
|
// Move account in array
|
||||||
|
const [moved] = accounts.splice(fromIdx, 1);
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const insertBefore = e.clientY < rect.top + rect.height / 2;
|
||||||
|
const newToIdx = accounts.findIndex(a => a.id === targetId);
|
||||||
|
accounts.splice(insertBefore ? newToIdx : newToIdx + 1, 0, moved);
|
||||||
|
|
||||||
|
// Save and re-render
|
||||||
|
await window.api.saveConfig({ hosters: config.hosters });
|
||||||
|
config = await window.api.getConfig();
|
||||||
|
renderAccounts();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleAccount(accountId) {
|
||||||
|
const found = findAccountById(accountId);
|
||||||
|
if (!found) return;
|
||||||
|
found.account.enabled = found.account.enabled === false ? true : false;
|
||||||
|
await window.api.saveConfig({ hosters: config.hosters });
|
||||||
config = await window.api.getConfig();
|
config = await window.api.getConfig();
|
||||||
syncSelectedUploadHosters();
|
syncSelectedUploadHosters();
|
||||||
renderAccounts();
|
renderAccounts();
|
||||||
@ -2251,66 +2376,51 @@ async function toggleAccount(hosterName) {
|
|||||||
renderSettings();
|
renderSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkSingleAccount(hosterName) {
|
async function checkSingleAccount(accountId) {
|
||||||
if (!hosterName || healthCheckRunning) return;
|
if (!accountId || healthCheckRunning) return;
|
||||||
|
const found = findAccountById(accountId);
|
||||||
|
if (!found) return;
|
||||||
healthCheckRunning = true;
|
healthCheckRunning = true;
|
||||||
accountStatuses[hosterName] = { status: 'checking', message: '' };
|
accountStatuses[accountId] = { status: 'checking', message: '' };
|
||||||
renderAccounts();
|
renderAccounts();
|
||||||
try {
|
try {
|
||||||
const rows = await executeHealthCheck([hosterName], 'manual');
|
const result = await window.api.runHealthCheck({ hosters: [{ hoster: found.name, accountId }] });
|
||||||
const row = rows.find(r => r.hoster === hosterName);
|
const rows = result && Array.isArray(result.results) ? result.results : [];
|
||||||
if (row) accountStatuses[hosterName] = { status: row.status || 'error', message: row.message || '' };
|
const row = rows.find(r => r.accountId === accountId);
|
||||||
|
if (row) accountStatuses[accountId] = { status: row.status || 'error', message: row.message || '' };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
accountStatuses[hosterName] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' };
|
accountStatuses[accountId] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' };
|
||||||
} finally {
|
} finally {
|
||||||
healthCheckRunning = false;
|
healthCheckRunning = false;
|
||||||
}
|
}
|
||||||
renderAccounts();
|
renderAccounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCredsFieldsHtml(name, hoster) {
|
function getCredsFieldsHtml(authType, account) {
|
||||||
hoster = hoster || {};
|
account = account || {};
|
||||||
if (name === 'vidmoly.me') {
|
if (authType === 'login') {
|
||||||
return `
|
return `
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label>Username</label>
|
<label>Username / E-Mail</label>
|
||||||
<input type="text" class="key-input" id="accField_username" value="${escapeAttr(hoster.username || '')}" placeholder="Username">
|
<input type="text" class="key-input" id="accField_username" value="${escapeAttr(account.username || '')}" placeholder="Username oder E-Mail">
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label>Passwort</label>
|
<label>Passwort</label>
|
||||||
<input type="password" class="key-input" id="accField_password" value="${escapeAttr(hoster.password || '')}" placeholder="Passwort">
|
<input type="password" class="key-input" id="accField_password" value="${escapeAttr(account.password || '')}" placeholder="Passwort">
|
||||||
<button class="toggle-vis" type="button" title="Anzeigen">👁</button>
|
<button class="toggle-vis" type="button" title="Anzeigen">👁</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
if (name === 'voe.sx' || name === 'doodstream.com') {
|
// API key
|
||||||
return `
|
|
||||||
<div class="settings-row">
|
|
||||||
<label>E-Mail (Login)</label>
|
|
||||||
<input type="text" class="key-input" id="accField_username" value="${escapeAttr(hoster.username || '')}" placeholder="E-Mail für Login">
|
|
||||||
</div>
|
|
||||||
<div class="settings-row">
|
|
||||||
<label>Passwort (Login)</label>
|
|
||||||
<input type="password" class="key-input" id="accField_password" value="${escapeAttr(hoster.password || '')}" placeholder="Passwort für Login">
|
|
||||||
<button class="toggle-vis" type="button" title="Anzeigen">👁</button>
|
|
||||||
</div>
|
|
||||||
<div class="settings-row">
|
|
||||||
<label>API Key (optional)</label>
|
|
||||||
<input type="password" class="key-input" id="accField_apiKey" value="${escapeAttr(hoster.apiKey || '')}" placeholder="API Key (Fallback)">
|
|
||||||
<button class="toggle-vis" type="button" title="Anzeigen">👁</button>
|
|
||||||
</div>
|
|
||||||
<p class="hint" style="margin:4px 0 0;opacity:0.6">Login wird bevorzugt. API-Key nur als Fallback.</p>`;
|
|
||||||
}
|
|
||||||
// Default: API key only
|
|
||||||
return `
|
return `
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label>API Key</label>
|
<label>API Key</label>
|
||||||
<input type="password" class="key-input" id="accField_apiKey" value="${escapeAttr(hoster.apiKey || '')}" placeholder="API Key">
|
<input type="password" class="key-input" id="accField_apiKey" value="${escapeAttr(account.apiKey || '')}" placeholder="API Key">
|
||||||
<button class="toggle-vis" type="button" title="Anzeigen">👁</button>
|
<button class="toggle-vis" type="button" title="Anzeigen">👁</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAccountModal(editHoster) {
|
function openAccountModal(editAccountId) {
|
||||||
editingAccountHoster = editHoster || null;
|
editingAccountId = editAccountId || null;
|
||||||
const modal = document.getElementById('accountModal');
|
const modal = document.getElementById('accountModal');
|
||||||
const title = document.getElementById('accountModalTitle');
|
const title = document.getElementById('accountModalTitle');
|
||||||
const subtitle = document.getElementById('accountModalSubtitle');
|
const subtitle = document.getElementById('accountModalSubtitle');
|
||||||
@ -2323,28 +2433,26 @@ function openAccountModal(editHoster) {
|
|||||||
statusEl.textContent = '';
|
statusEl.textContent = '';
|
||||||
statusEl.className = 'account-modal-status';
|
statusEl.className = 'account-modal-status';
|
||||||
|
|
||||||
if (editingAccountHoster) {
|
if (editingAccountId) {
|
||||||
// Edit mode
|
// Edit mode
|
||||||
|
const found = findAccountById(editingAccountId);
|
||||||
|
if (!found) return;
|
||||||
title.textContent = 'Account bearbeiten';
|
title.textContent = 'Account bearbeiten';
|
||||||
subtitle.textContent = `Zugangsdaten für ${getAccountDisplayName(editingAccountHoster, config.hosters[editingAccountHoster] || {})} bearbeiten.`;
|
subtitle.textContent = `Zugangsdaten für ${getAccountDisplayName(found.name, found.account)} bearbeiten.`;
|
||||||
hosterRow.style.display = 'none';
|
hosterRow.style.display = 'none';
|
||||||
saveBtn.textContent = 'Speichern & prüfen';
|
saveBtn.textContent = 'Speichern & prüfen';
|
||||||
const hoster = config.hosters[editingAccountHoster] || {};
|
credsContainer.innerHTML = getCredsFieldsHtml(found.account.authType || 'login', found.account);
|
||||||
credsContainer.innerHTML = getCredsFieldsHtml(editingAccountHoster, hoster);
|
|
||||||
} else {
|
} else {
|
||||||
// Add mode
|
// Add mode — always show all options (multiple accounts per hoster allowed)
|
||||||
title.textContent = 'Account hinzufügen';
|
title.textContent = 'Account hinzufügen';
|
||||||
subtitle.textContent = 'Wähle einen Hoster und gib deine Zugangsdaten ein.';
|
subtitle.textContent = 'Wähle einen Hoster und gib deine Zugangsdaten ein.';
|
||||||
hosterRow.style.display = 'flex';
|
hosterRow.style.display = 'flex';
|
||||||
saveBtn.textContent = 'Anlegen & prüfen';
|
saveBtn.textContent = 'Anlegen & prüfen';
|
||||||
const available = getHostersWithoutCreds();
|
hosterSelect.innerHTML = HOSTER_ADD_OPTIONS.map(opt =>
|
||||||
if (available.length === 0) {
|
`<option value="${opt.value}">${escapeHtml(opt.label)}</option>`
|
||||||
hosterSelect.innerHTML = '<option value="">Alle Hoster bereits eingerichtet</option>';
|
).join('');
|
||||||
credsContainer.innerHTML = '';
|
const firstOpt = HOSTER_ADD_OPTIONS[0];
|
||||||
} else {
|
credsContainer.innerHTML = getCredsFieldsHtml(firstOpt.authType, {});
|
||||||
hosterSelect.innerHTML = available.map(name => `<option value="${name}">${getHosterLabel(name)}</option>`).join('');
|
|
||||||
credsContainer.innerHTML = getCredsFieldsHtml(available[0], {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle visibility buttons
|
// Toggle visibility buttons
|
||||||
@ -2360,14 +2468,16 @@ function openAccountModal(editHoster) {
|
|||||||
|
|
||||||
function closeAccountModal() {
|
function closeAccountModal() {
|
||||||
document.getElementById('accountModal').style.display = 'none';
|
document.getElementById('accountModal').style.display = 'none';
|
||||||
editingAccountHoster = null;
|
editingAccountId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDeleteAccountModal(hosterName) {
|
function openDeleteAccountModal(accountId) {
|
||||||
|
const found = findAccountById(accountId);
|
||||||
|
if (!found) return;
|
||||||
const modal = document.getElementById('deleteAccountModal');
|
const modal = document.getElementById('deleteAccountModal');
|
||||||
const msg = document.getElementById('deleteAccountMessage');
|
const msg = document.getElementById('deleteAccountMessage');
|
||||||
msg.textContent = `Account für "${getHosterLabel(hosterName)}" wirklich löschen? Alle Zugangsdaten werden entfernt.`;
|
msg.textContent = `Account "${getAccountDisplayName(found.name, found.account)}" wirklich löschen?`;
|
||||||
modal.dataset.hoster = hosterName;
|
modal.dataset.accountId = accountId;
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2375,22 +2485,20 @@ function closeDeleteModal() {
|
|||||||
document.getElementById('deleteAccountModal').style.display = 'none';
|
document.getElementById('deleteAccountModal').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAccount(hosterName) {
|
async function deleteAccount(accountId) {
|
||||||
const hosters = { ...config.hosters };
|
const found = findAccountById(accountId);
|
||||||
// Reset credentials to defaults
|
if (!found) return;
|
||||||
if (hosterName === 'vidmoly.me') {
|
// Remove account from the array
|
||||||
hosters[hosterName] = { enabled: false, authType: 'login', username: '', password: '' };
|
const accounts = config.hosters[found.name];
|
||||||
} else if (hosterName === 'voe.sx' || hosterName === 'doodstream.com') {
|
if (Array.isArray(accounts)) {
|
||||||
hosters[hosterName] = { enabled: false, username: '', password: '', apiKey: '' };
|
config.hosters[found.name] = accounts.filter(a => a.id !== accountId);
|
||||||
} else {
|
|
||||||
hosters[hosterName] = { enabled: false, apiKey: '' };
|
|
||||||
}
|
}
|
||||||
delete accountStatuses[hosterName];
|
delete accountStatuses[accountId];
|
||||||
await window.api.saveConfig({ hosters });
|
await window.api.saveConfig({ hosters: config.hosters });
|
||||||
config = await window.api.getConfig();
|
config = await window.api.getConfig();
|
||||||
ensureAccountStatusEntries();
|
ensureAccountStatusEntries();
|
||||||
syncSelectedUploadHosters();
|
syncSelectedUploadHosters();
|
||||||
if (getAccountsWithCreds().length === 0) renderHealthCheckResults([]);
|
if (getAllAccountsFlat().length === 0) renderHealthCheckResults([]);
|
||||||
renderAccounts();
|
renderAccounts();
|
||||||
renderHosterSummary();
|
renderHosterSummary();
|
||||||
renderHosterModal();
|
renderHosterModal();
|
||||||
@ -2398,27 +2506,39 @@ async function deleteAccount(hosterName) {
|
|||||||
closeDeleteModal();
|
closeDeleteModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function readAccountCredsFromModal(hosterName) {
|
function readAccountCredsFromModal(authType) {
|
||||||
if (hosterName === 'vidmoly.me') {
|
if (authType === 'login') {
|
||||||
const username = (document.getElementById('accField_username')?.value || '').trim();
|
const username = (document.getElementById('accField_username')?.value || '').trim();
|
||||||
const password = (document.getElementById('accField_password')?.value || '').trim();
|
const password = (document.getElementById('accField_password')?.value || '').trim();
|
||||||
return { enabled: !!(username && password), authType: 'login', username, password };
|
return { enabled: !!(username && password), authType: 'login', username, password };
|
||||||
}
|
}
|
||||||
if (hosterName === 'voe.sx' || hosterName === 'doodstream.com') {
|
// API
|
||||||
const username = (document.getElementById('accField_username')?.value || '').trim();
|
|
||||||
const password = (document.getElementById('accField_password')?.value || '').trim();
|
|
||||||
const apiKey = (document.getElementById('accField_apiKey')?.value || '').trim();
|
const apiKey = (document.getElementById('accField_apiKey')?.value || '').trim();
|
||||||
return { enabled: !!(username && password) || !!apiKey, username, password, apiKey };
|
return { enabled: !!apiKey, authType: 'api', apiKey };
|
||||||
}
|
|
||||||
const apiKey = (document.getElementById('accField_apiKey')?.value || '').trim();
|
|
||||||
return { enabled: !!apiKey, apiKey };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveAccount() {
|
async function saveAccount() {
|
||||||
const hosterName = editingAccountHoster || document.getElementById('accountHosterSelect')?.value;
|
let hosterName, authType, accountId;
|
||||||
if (!hosterName) return;
|
|
||||||
|
|
||||||
const creds = readAccountCredsFromModal(hosterName);
|
if (editingAccountId) {
|
||||||
|
// Edit existing account
|
||||||
|
const found = findAccountById(editingAccountId);
|
||||||
|
if (!found) return;
|
||||||
|
hosterName = found.name;
|
||||||
|
authType = found.account.authType || 'login';
|
||||||
|
accountId = editingAccountId;
|
||||||
|
} else {
|
||||||
|
// Add new account
|
||||||
|
const selectValue = document.getElementById('accountHosterSelect')?.value;
|
||||||
|
if (!selectValue) return;
|
||||||
|
const opt = HOSTER_ADD_OPTIONS.find(o => o.value === selectValue);
|
||||||
|
if (!opt) return;
|
||||||
|
hosterName = opt.hoster;
|
||||||
|
authType = opt.authType;
|
||||||
|
accountId = `${hosterName}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const creds = readAccountCredsFromModal(authType);
|
||||||
if (!creds.enabled) {
|
if (!creds.enabled) {
|
||||||
const statusEl = document.getElementById('accountModalStatus');
|
const statusEl = document.getElementById('accountModalStatus');
|
||||||
statusEl.textContent = 'Bitte Zugangsdaten eingeben.';
|
statusEl.textContent = 'Bitte Zugangsdaten eingeben.';
|
||||||
@ -2427,9 +2547,18 @@ async function saveAccount() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save credentials
|
// Save credentials
|
||||||
const hosters = { ...config.hosters };
|
if (!Array.isArray(config.hosters[hosterName])) config.hosters[hosterName] = [];
|
||||||
hosters[hosterName] = creds;
|
if (editingAccountId) {
|
||||||
await window.api.saveConfig({ hosters });
|
// Update existing account in array
|
||||||
|
const idx = config.hosters[hosterName].findIndex(a => a.id === editingAccountId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
config.hosters[hosterName][idx] = { ...config.hosters[hosterName][idx], ...creds };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add new account
|
||||||
|
config.hosters[hosterName].push({ id: accountId, ...creds });
|
||||||
|
}
|
||||||
|
await window.api.saveConfig({ hosters: config.hosters });
|
||||||
config = await window.api.getConfig();
|
config = await window.api.getConfig();
|
||||||
|
|
||||||
// Show checking status
|
// Show checking status
|
||||||
@ -2439,30 +2568,31 @@ async function saveAccount() {
|
|||||||
statusEl.className = 'account-modal-status checking';
|
statusEl.className = 'account-modal-status checking';
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
|
|
||||||
accountStatuses[hosterName] = { status: 'checking', message: '' };
|
accountStatuses[accountId] = { status: 'checking', message: '' };
|
||||||
syncSelectedUploadHosters();
|
syncSelectedUploadHosters();
|
||||||
renderAccounts();
|
renderAccounts();
|
||||||
renderHosterSummary();
|
renderHosterSummary();
|
||||||
renderHosterModal();
|
renderHosterModal();
|
||||||
renderSettings();
|
renderSettings();
|
||||||
|
|
||||||
// Run health check
|
// Run health check for this specific account
|
||||||
try {
|
try {
|
||||||
const rows = await executeHealthCheck([hosterName], 'auto');
|
const result = await window.api.runHealthCheck({ hosters: [{ hoster: hosterName, accountId }] });
|
||||||
const row = rows.find(r => r.hoster === hosterName);
|
const rows = result && Array.isArray(result.results) ? result.results : [];
|
||||||
|
const row = rows.find(r => r.accountId === accountId);
|
||||||
if (row && (row.status === 'ok' || row.status === 'warn')) {
|
if (row && (row.status === 'ok' || row.status === 'warn')) {
|
||||||
accountStatuses[hosterName] = { status: row.status || 'ok', message: row.message || '' };
|
accountStatuses[accountId] = { status: row.status || 'ok', message: row.message || '' };
|
||||||
statusEl.textContent = row.status === 'warn' ? row.message || 'Prüfung mit Warnung abgeschlossen.' : 'Login erfolgreich!';
|
statusEl.textContent = row.status === 'warn' ? row.message || 'Prüfung mit Warnung abgeschlossen.' : 'Login erfolgreich!';
|
||||||
statusEl.className = 'account-modal-status ok';
|
statusEl.className = 'account-modal-status ok';
|
||||||
setTimeout(() => closeAccountModal(), 1200);
|
setTimeout(() => closeAccountModal(), 1200);
|
||||||
} else {
|
} else {
|
||||||
const msg = (row && row.message) || 'Login fehlgeschlagen';
|
const msg = (row && row.message) || 'Login fehlgeschlagen';
|
||||||
accountStatuses[hosterName] = { status: 'error', message: msg };
|
accountStatuses[accountId] = { status: 'error', message: msg };
|
||||||
statusEl.textContent = msg;
|
statusEl.textContent = msg;
|
||||||
statusEl.className = 'account-modal-status error';
|
statusEl.className = 'account-modal-status error';
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
accountStatuses[hosterName] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' };
|
accountStatuses[accountId] = { status: 'error', message: err.message || 'Prüfung fehlgeschlagen' };
|
||||||
statusEl.textContent = err.message || 'Prüfung fehlgeschlagen';
|
statusEl.textContent = err.message || 'Prüfung fehlgeschlagen';
|
||||||
statusEl.className = 'account-modal-status error';
|
statusEl.className = 'account-modal-status error';
|
||||||
} finally {
|
} finally {
|
||||||
@ -2822,8 +2952,10 @@ function setupListeners() {
|
|||||||
|
|
||||||
// Account hoster select change → update credential fields
|
// Account hoster select change → update credential fields
|
||||||
document.getElementById('accountHosterSelect').addEventListener('change', (e) => {
|
document.getElementById('accountHosterSelect').addEventListener('change', (e) => {
|
||||||
|
const opt = HOSTER_ADD_OPTIONS.find(o => o.value === e.target.value);
|
||||||
|
const authType = opt ? opt.authType : 'login';
|
||||||
const credsContainer = document.getElementById('accountCredsFields');
|
const credsContainer = document.getElementById('accountCredsFields');
|
||||||
credsContainer.innerHTML = getCredsFieldsHtml(e.target.value, {});
|
credsContainer.innerHTML = getCredsFieldsHtml(authType, {});
|
||||||
credsContainer.querySelectorAll('.toggle-vis').forEach(btn => {
|
credsContainer.querySelectorAll('.toggle-vis').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const input = btn.previousElementSibling;
|
const input = btn.previousElementSibling;
|
||||||
@ -2839,8 +2971,8 @@ function setupListeners() {
|
|||||||
document.getElementById('cancelDeleteBtn').addEventListener('click', closeDeleteModal);
|
document.getElementById('cancelDeleteBtn').addEventListener('click', closeDeleteModal);
|
||||||
document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
|
document.getElementById('confirmDeleteBtn').addEventListener('click', () => {
|
||||||
const modal = document.getElementById('deleteAccountModal');
|
const modal = document.getElementById('deleteAccountModal');
|
||||||
const hoster = modal.dataset.hoster;
|
const accountId = modal.dataset.accountId;
|
||||||
if (hoster) deleteAccount(hoster);
|
if (accountId) deleteAccount(accountId);
|
||||||
});
|
});
|
||||||
document.getElementById('deleteAccountModal').addEventListener('click', (e) => {
|
document.getElementById('deleteAccountModal').addEventListener('click', (e) => {
|
||||||
if (e.target.id === 'deleteAccountModal') closeDeleteModal();
|
if (e.target.id === 'deleteAccountModal') closeDeleteModal();
|
||||||
|
|||||||
@ -812,6 +812,44 @@ body {
|
|||||||
.account-modal-status.ok { color: var(--success); }
|
.account-modal-status.ok { color: var(--success); }
|
||||||
.account-modal-status.error { color: var(--danger); }
|
.account-modal-status.error { color: var(--danger); }
|
||||||
|
|
||||||
|
/* Multi-account: drag handle, priority badge, hoster group */
|
||||||
|
.account-card-drag-handle {
|
||||||
|
cursor: grab;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
padding: 2px 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.account-card-drag-handle:hover { color: var(--text-muted); }
|
||||||
|
.account-card.dragging { opacity: 0.4; }
|
||||||
|
.account-card.drag-over-above { border-top: 2px solid var(--accent); }
|
||||||
|
.account-card.drag-over-below { border-bottom: 2px solid var(--accent); }
|
||||||
|
|
||||||
|
.account-priority-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-dim);
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-hoster-group {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.account-hoster-group-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
.account-hoster-group .account-card { margin-bottom: 4px; }
|
||||||
|
|
||||||
.accounts-empty {
|
.accounts-empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 48px 16px;
|
padding: 48px 16px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user