feat: doodstream login support, auto-remove from queue, byse URL fix
- Add doodstream.com web login (email+password) as alternative to API key - Fix doodstream login: use X-Requested-With header for JSON response - Add "Aus der Queue entfernen bei Abschluss" setting - Fix byse.sx download URLs to use /d/ prefix - Make config writes async to prevent race conditions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7d992206e8
commit
d94156943b
@ -12,7 +12,7 @@ const HOSTER_SETTINGS_DEFAULTS = {
|
|||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
hosters: {
|
hosters: {
|
||||||
'doodstream.com': { enabled: true, apiKey: '' },
|
'doodstream.com': { enabled: true, apiKey: '', username: '', password: '' },
|
||||||
'voe.sx': { enabled: true, apiKey: '' },
|
'voe.sx': { enabled: true, apiKey: '' },
|
||||||
'vidmoly.me': { enabled: true, authType: 'login', username: '', password: '' },
|
'vidmoly.me': { enabled: true, authType: 'login', username: '', password: '' },
|
||||||
'byse.sx': { enabled: true, apiKey: '' }
|
'byse.sx': { enabled: true, apiKey: '' }
|
||||||
@ -28,6 +28,7 @@ const DEFAULTS = {
|
|||||||
shutdownAfterFinish: 'nothing', // nothing | sleep | shutdown | restart
|
shutdownAfterFinish: 'nothing', // nothing | sleep | shutdown | restart
|
||||||
logFilePath: '',
|
logFilePath: '',
|
||||||
resumeQueueOnLaunch: true,
|
resumeQueueOnLaunch: true,
|
||||||
|
removeFromQueueOnDone: false,
|
||||||
pendingQueue: null,
|
pendingQueue: null,
|
||||||
scramble: {
|
scramble: {
|
||||||
active: false,
|
active: false,
|
||||||
@ -84,9 +85,12 @@ class ConfigStore {
|
|||||||
if (config.hosters) current.hosters = config.hosters;
|
if (config.hosters) current.hosters = config.hosters;
|
||||||
if (config.hosterSettings) current.hosterSettings = config.hosterSettings;
|
if (config.hosterSettings) current.hosterSettings = config.hosterSettings;
|
||||||
if (config.globalSettings) current.globalSettings = config.globalSettings;
|
if (config.globalSettings) current.globalSettings = config.globalSettings;
|
||||||
// Async write to avoid blocking main process
|
|
||||||
const data = JSON.stringify(current, null, 2);
|
const data = JSON.stringify(current, null, 2);
|
||||||
fs.writeFile(this.filePath, data, 'utf-8', () => {});
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.writeFile(this.filePath, data, 'utf-8', (err) => {
|
||||||
|
if (err) reject(err); else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadHistory() {
|
loadHistory() {
|
||||||
@ -97,17 +101,26 @@ class ConfigStore {
|
|||||||
appendHistory(entry) {
|
appendHistory(entry) {
|
||||||
const config = this.load();
|
const config = this.load();
|
||||||
config.history.push(entry);
|
config.history.push(entry);
|
||||||
// Cap at MAX_HISTORY
|
|
||||||
if (config.history.length > MAX_HISTORY) {
|
if (config.history.length > MAX_HISTORY) {
|
||||||
config.history = config.history.slice(-MAX_HISTORY);
|
config.history = config.history.slice(-MAX_HISTORY);
|
||||||
}
|
}
|
||||||
fs.writeFileSync(this.filePath, JSON.stringify(config, null, 2), 'utf-8');
|
const data = JSON.stringify(config, null, 2);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.writeFile(this.filePath, data, 'utf-8', (err) => {
|
||||||
|
if (err) reject(err); else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
clearHistory() {
|
clearHistory() {
|
||||||
const config = this.load();
|
const config = this.load();
|
||||||
config.history = [];
|
config.history = [];
|
||||||
fs.writeFileSync(this.filePath, JSON.stringify(config, null, 2), 'utf-8');
|
const data = JSON.stringify(config, null, 2);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fs.writeFile(this.filePath, data, 'utf-8', (err) => {
|
||||||
|
if (err) reject(err); else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
276
lib/doodstream-upload.js
Normal file
276
lib/doodstream-upload.js
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const { request } = require('undici');
|
||||||
|
|
||||||
|
const BASE_URL = 'https://doodstream.com';
|
||||||
|
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
||||||
|
const UPLOAD_TIMEOUT = 1800000; // 30 min
|
||||||
|
|
||||||
|
class DoodstreamUploader {
|
||||||
|
constructor() {
|
||||||
|
this.cookies = new Map();
|
||||||
|
this.sessId = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
_cookieHeader() {
|
||||||
|
return Array.from(this.cookies.entries())
|
||||||
|
.map(([k, v]) => `${k}=${v}`)
|
||||||
|
.join('; ');
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseCookiesFromHeaders(headers) {
|
||||||
|
let setCookies;
|
||||||
|
if (typeof headers.getSetCookie === 'function') {
|
||||||
|
setCookies = headers.getSetCookie();
|
||||||
|
} else if (headers['set-cookie']) {
|
||||||
|
setCookies = Array.isArray(headers['set-cookie']) ? headers['set-cookie'] : [headers['set-cookie']];
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const raw of setCookies) {
|
||||||
|
const pair = raw.split(';')[0];
|
||||||
|
const eq = pair.indexOf('=');
|
||||||
|
if (eq > 0) {
|
||||||
|
this.cookies.set(pair.substring(0, eq).trim(), pair.substring(eq + 1).trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _fetch(url, opts = {}, _redirectCount = 0) {
|
||||||
|
const MAX_REDIRECTS = 10;
|
||||||
|
const headers = {
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
...(opts.headers || {})
|
||||||
|
};
|
||||||
|
if (this.cookies.size > 0) {
|
||||||
|
headers['Cookie'] = this._cookieHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
...opts,
|
||||||
|
headers,
|
||||||
|
redirect: 'manual'
|
||||||
|
});
|
||||||
|
|
||||||
|
this._parseCookiesFromHeaders(res.headers);
|
||||||
|
|
||||||
|
if ([301, 302, 303, 307, 308].includes(res.status)) {
|
||||||
|
try { await res.text(); } catch {}
|
||||||
|
if (_redirectCount >= MAX_REDIRECTS) throw new Error('Zu viele Redirects');
|
||||||
|
const location = res.headers.get('location');
|
||||||
|
if (location) {
|
||||||
|
const nextUrl = new URL(location, url).href;
|
||||||
|
return this._fetch(nextUrl, { ...opts, method: 'GET', body: undefined }, _redirectCount + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login to DoodStream via web form
|
||||||
|
*/
|
||||||
|
async login(username, password) {
|
||||||
|
// GET homepage first to collect cookies
|
||||||
|
const homeRes = await this._fetch(BASE_URL);
|
||||||
|
await homeRes.text();
|
||||||
|
|
||||||
|
// POST login via AJAX (op in body, XHR header required for JSON response)
|
||||||
|
const loginData = new URLSearchParams({
|
||||||
|
op: 'login_ajax',
|
||||||
|
login: username,
|
||||||
|
password: password,
|
||||||
|
loginotp: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await this._fetch(BASE_URL + '/', {
|
||||||
|
method: 'POST',
|
||||||
|
body: loginData.toString(),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Referer': BASE_URL + '/',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await res.text();
|
||||||
|
let json;
|
||||||
|
try { json = JSON.parse(body); } catch { json = null; }
|
||||||
|
|
||||||
|
if (!json || json.status !== 'success') {
|
||||||
|
const msg = (json && json.message) || 'Login fehlgeschlagen';
|
||||||
|
throw new Error(`Doodstream Login: ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract sess_id from the upload page
|
||||||
|
await this._extractSessId();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _extractSessId() {
|
||||||
|
const res = await this._fetch(BASE_URL + '/?op=upload');
|
||||||
|
const html = await res.text();
|
||||||
|
|
||||||
|
// Look for sess_id in the page (Vue component prop or hidden field)
|
||||||
|
const sessMatch = html.match(/sess_id['":\s]+['"]([a-zA-Z0-9]+)['"]/);
|
||||||
|
if (sessMatch) {
|
||||||
|
this.sessId = sessMatch[1];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alternative: look in script or data attributes
|
||||||
|
const altMatch = html.match(/sess_id\s*=\s*['"]([a-zA-Z0-9]+)['"]/);
|
||||||
|
if (altMatch) {
|
||||||
|
this.sessId = altMatch[1];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Doodstream: sess_id nicht gefunden nach Login');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get upload server URL from web interface
|
||||||
|
*/
|
||||||
|
async _getUploadServer() {
|
||||||
|
// Use the standard upload server endpoint
|
||||||
|
const res = await this._fetch(BASE_URL + '/?op=upload_server');
|
||||||
|
const text = await res.text();
|
||||||
|
let json;
|
||||||
|
try { json = JSON.parse(text); } catch { json = null; }
|
||||||
|
|
||||||
|
if (json && json.result && /^https?:\/\//i.test(json.result)) {
|
||||||
|
return json.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try fetching from upload page HTML
|
||||||
|
const pageRes = await this._fetch(BASE_URL + '/?op=upload');
|
||||||
|
const html = await pageRes.text();
|
||||||
|
const srvMatch = html.match(/srv_url['":\s]+['"]?(https?:\/\/[^'">\s]+)['"]?/i);
|
||||||
|
if (srvMatch) return srvMatch[1];
|
||||||
|
|
||||||
|
// Last resort fallback
|
||||||
|
return 'https://tr1128ve.cloudatacdn.com/upload/01';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload file using web session
|
||||||
|
*/
|
||||||
|
async upload(filePath, progressCb, signal, throttle) {
|
||||||
|
const fileName = path.basename(filePath);
|
||||||
|
const fileSize = fs.statSync(filePath).size;
|
||||||
|
|
||||||
|
// Get upload server
|
||||||
|
const uploadUrl = await this._getUploadServer();
|
||||||
|
|
||||||
|
// Build multipart form
|
||||||
|
const boundary = `----WebKitFormBoundary${crypto.randomBytes(16).toString('hex')}`;
|
||||||
|
const fileStream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
|
// Build form parts
|
||||||
|
const fields = {
|
||||||
|
sess_id: this.sessId,
|
||||||
|
utype: 'reg'
|
||||||
|
};
|
||||||
|
|
||||||
|
const preamble = [];
|
||||||
|
for (const [key, val] of Object.entries(fields)) {
|
||||||
|
preamble.push(
|
||||||
|
`--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${val}\r\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
preamble.push(
|
||||||
|
`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`
|
||||||
|
);
|
||||||
|
|
||||||
|
const preambleBuffer = Buffer.from(preamble.join(''));
|
||||||
|
const epilogue = Buffer.from(`\r\n--${boundary}--\r\n`);
|
||||||
|
const totalSize = preambleBuffer.length + fileSize + epilogue.length;
|
||||||
|
|
||||||
|
// Assemble body
|
||||||
|
const { Readable } = require('stream');
|
||||||
|
let bytesSent = 0;
|
||||||
|
|
||||||
|
const bodyStream = new Readable({
|
||||||
|
read() {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Push preamble
|
||||||
|
bodyStream.push(preambleBuffer);
|
||||||
|
bytesSent += preambleBuffer.length;
|
||||||
|
|
||||||
|
// Pipe file
|
||||||
|
fileStream.on('data', (chunk) => {
|
||||||
|
if (signal && signal.aborted) {
|
||||||
|
fileStream.destroy();
|
||||||
|
bodyStream.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bodyStream.push(chunk);
|
||||||
|
bytesSent += chunk.length;
|
||||||
|
if (progressCb) progressCb(Math.max(0, bytesSent - preambleBuffer.length), fileSize);
|
||||||
|
});
|
||||||
|
|
||||||
|
fileStream.on('end', () => {
|
||||||
|
bodyStream.push(epilogue);
|
||||||
|
bodyStream.push(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
fileStream.on('error', (err) => {
|
||||||
|
bodyStream.destroy(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadRes = await request(uploadUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||||
|
'Content-Length': String(totalSize),
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
'Cookie': this._cookieHeader()
|
||||||
|
},
|
||||||
|
body: bodyStream,
|
||||||
|
signal,
|
||||||
|
bodyTimeout: UPLOAD_TIMEOUT,
|
||||||
|
headersTimeout: 60000
|
||||||
|
});
|
||||||
|
|
||||||
|
const resText = await uploadRes.body.text();
|
||||||
|
let payload;
|
||||||
|
try { payload = JSON.parse(resText); } catch {}
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
// Try to extract from HTML response
|
||||||
|
const match = resText.match(/filecode['":\s]+['"]([a-zA-Z0-9]+)['"]/i);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
download_url: `https://doodstream.com/d/${match[1]}`,
|
||||||
|
embed_url: `https://doodstream.com/e/${match[1]}`,
|
||||||
|
file_code: match[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error('Doodstream Upload: Keine gueltige Antwort erhalten');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse result
|
||||||
|
let item = null;
|
||||||
|
const result = payload.result;
|
||||||
|
if (Array.isArray(result) && result.length > 0) {
|
||||||
|
item = result[0];
|
||||||
|
} else if (typeof result === 'object' && result) {
|
||||||
|
item = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
throw new Error(`Doodstream Upload fehlgeschlagen: ${payload.msg || 'Unbekannter Fehler'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileCode = item.filecode || item.file_code || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
download_url: item.download_url || item.protected_dl || (fileCode ? `https://doodstream.com/d/${fileCode}` : null),
|
||||||
|
embed_url: item.protected_embed || (fileCode ? `https://doodstream.com/e/${fileCode}` : null),
|
||||||
|
file_code: fileCode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = DoodstreamUploader;
|
||||||
@ -212,7 +212,7 @@ function parseByseResult(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
download_url: file_code ? `https://byse.sx/${file_code}` : null,
|
download_url: file_code ? `https://byse.sx/d/${file_code}` : null,
|
||||||
embed_url: file_code ? `https://byse.sx/e/${file_code}` : null,
|
embed_url: file_code ? `https://byse.sx/e/${file_code}` : null,
|
||||||
file_code
|
file_code
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,7 @@ const crypto = require('crypto');
|
|||||||
const { uploadFile } = require('./hosters');
|
const { uploadFile } = require('./hosters');
|
||||||
const VidmolyUploader = require('./vidmoly-upload');
|
const VidmolyUploader = require('./vidmoly-upload');
|
||||||
const VoeUploader = require('./voe-upload');
|
const VoeUploader = require('./voe-upload');
|
||||||
|
const DoodstreamUploader = require('./doodstream-upload');
|
||||||
const Semaphore = require('./semaphore');
|
const Semaphore = require('./semaphore');
|
||||||
const Throttle = require('./throttle');
|
const Throttle = require('./throttle');
|
||||||
|
|
||||||
@ -254,6 +255,10 @@ class UploadManager extends EventEmitter {
|
|||||||
const voe = new VoeUploader();
|
const voe = new VoeUploader();
|
||||||
await voe.login(task.username, task.password);
|
await voe.login(task.username, task.password);
|
||||||
result = await voe.upload(task.file, progressCb, jobSignal, throttle);
|
result = await voe.upload(task.file, progressCb, jobSignal, 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, jobSignal, throttle);
|
||||||
} else {
|
} else {
|
||||||
result = await uploadFile(task.hoster, task.file, task.apiKey, progressCb, jobSignal, throttle);
|
result = await uploadFile(task.hoster, task.file, task.apiKey, progressCb, jobSignal, throttle);
|
||||||
}
|
}
|
||||||
|
|||||||
42
main.js
42
main.js
@ -6,6 +6,7 @@ const UploadManager = require('./lib/upload-manager');
|
|||||||
const { HOSTER_CONFIGS } = require('./lib/hosters');
|
const { HOSTER_CONFIGS } = require('./lib/hosters');
|
||||||
const VidmolyUploader = require('./lib/vidmoly-upload');
|
const VidmolyUploader = require('./lib/vidmoly-upload');
|
||||||
const VoeUploader = require('./lib/voe-upload');
|
const VoeUploader = require('./lib/voe-upload');
|
||||||
|
const DoodstreamUploader = require('./lib/doodstream-upload');
|
||||||
const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater');
|
const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater');
|
||||||
|
|
||||||
let mainWindow;
|
let mainWindow;
|
||||||
@ -107,6 +108,10 @@ function buildUploadTasks(config, files, hosters) {
|
|||||||
// VOE login-based upload (preferred over API)
|
// VOE login-based upload (preferred over API)
|
||||||
tasks.push({ file, hoster, username: hosterConfig.username, password: hosterConfig.password });
|
tasks.push({ file, hoster, username: hosterConfig.username, password: hosterConfig.password });
|
||||||
debugLog(` task: ${hoster} login=${hosterConfig.username.slice(0, 6)}...`);
|
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 {
|
} else {
|
||||||
if (!hosterConfig.apiKey) {
|
if (!hosterConfig.apiKey) {
|
||||||
debugLog(` skip ${hoster}: missing apiKey`);
|
debugLog(` skip ${hoster}: missing apiKey`);
|
||||||
@ -122,12 +127,27 @@ function buildUploadTasks(config, files, hosters) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkDoodstreamHealth(hosterConfig) {
|
async function checkDoodstreamHealth(hosterConfig) {
|
||||||
|
const username = hosterConfig && hosterConfig.username
|
||||||
|
? String(hosterConfig.username).trim()
|
||||||
|
: '';
|
||||||
|
const password = hosterConfig && hosterConfig.password
|
||||||
|
? String(hosterConfig.password).trim()
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Login-based check (preferred)
|
||||||
|
if (username && password) {
|
||||||
|
const uploader = new DoodstreamUploader();
|
||||||
|
await uploader.login(username, password);
|
||||||
|
return { status: 'ok', message: 'Login ok, Upload-Seite bereit' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to API key check
|
||||||
const apiKey = hosterConfig && hosterConfig.apiKey
|
const apiKey = hosterConfig && hosterConfig.apiKey
|
||||||
? String(hosterConfig.apiKey).trim()
|
? String(hosterConfig.apiKey).trim()
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
return { status: 'error', message: 'API Key fehlt' };
|
return { status: 'error', message: 'Login oder API Key fehlt' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiBase = HOSTER_CONFIGS['doodstream.com'].apiBase;
|
const apiBase = HOSTER_CONFIGS['doodstream.com'].apiBase;
|
||||||
@ -398,8 +418,8 @@ ipcMain.handle('get-config', () => {
|
|||||||
return configStore.load();
|
return configStore.load();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('save-config', (_event, config) => {
|
ipcMain.handle('save-config', async (_event, config) => {
|
||||||
configStore.save(config);
|
await configStore.save(config);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -487,9 +507,9 @@ ipcMain.handle('start-upload', (_event, payload) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
uploadManager.on('batch-done', (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}`);
|
||||||
configStore.appendHistory(summary);
|
await configStore.appendHistory(summary);
|
||||||
// Write successful uploads to fileuploader.log
|
// Write successful uploads to fileuploader.log
|
||||||
for (const file of summary.files || []) {
|
for (const file of summary.files || []) {
|
||||||
for (const result of file.results || []) {
|
for (const result of file.results || []) {
|
||||||
@ -590,8 +610,8 @@ ipcMain.handle('get-hoster-settings', () => {
|
|||||||
return config.hosterSettings || {};
|
return config.hosterSettings || {};
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('save-hoster-settings', (_event, hosterSettings) => {
|
ipcMain.handle('save-hoster-settings', async (_event, hosterSettings) => {
|
||||||
configStore.save({ hosterSettings });
|
await configStore.save({ hosterSettings });
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -601,17 +621,17 @@ ipcMain.handle('get-global-settings', () => {
|
|||||||
return config.globalSettings || {};
|
return config.globalSettings || {};
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('save-global-settings', (_event, globalSettings) => {
|
ipcMain.handle('save-global-settings', async (_event, globalSettings) => {
|
||||||
configStore.save({ globalSettings });
|
await configStore.save({ globalSettings });
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Always on top ---
|
// --- Always on top ---
|
||||||
ipcMain.handle('set-always-on-top', (_event, value) => {
|
ipcMain.handle('set-always-on-top', async (_event, value) => {
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
mainWindow.setAlwaysOnTop(!!value);
|
mainWindow.setAlwaysOnTop(!!value);
|
||||||
}
|
}
|
||||||
configStore.save({ globalSettings: { ...configStore.load().globalSettings, alwaysOnTop: !!value } });
|
await configStore.save({ globalSettings: { ...configStore.load().globalSettings, alwaysOnTop: !!value } });
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-hoster-uploader",
|
"name": "multi-hoster-uploader",
|
||||||
"version": "1.4.0",
|
"version": "1.5.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": {
|
||||||
|
|||||||
@ -99,7 +99,7 @@ document.querySelectorAll('.tab').forEach(tab => {
|
|||||||
// --- Hoster selection ---
|
// --- Hoster selection ---
|
||||||
function hosterHasCredentials(name, hoster) {
|
function hosterHasCredentials(name, hoster) {
|
||||||
if (name === 'vidmoly.me') return !!(hoster.username && hoster.password);
|
if (name === 'vidmoly.me') return !!(hoster.username && hoster.password);
|
||||||
if (name === 'voe.sx') return !!(hoster.username && hoster.password) || !!hoster.apiKey;
|
if (name === 'voe.sx' || name === 'doodstream.com') return !!(hoster.username && hoster.password) || !!hoster.apiKey;
|
||||||
return !!hoster.apiKey;
|
return !!hoster.apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -871,6 +871,19 @@ function handleBatchDone(summary) {
|
|||||||
renderRecentUploadsPanel();
|
renderRecentUploadsPanel();
|
||||||
|
|
||||||
loadHistory();
|
loadHistory();
|
||||||
|
|
||||||
|
// Auto-remove completed jobs from queue if enabled
|
||||||
|
const removeOnDone = config.globalSettings && config.globalSettings.removeFromQueueOnDone;
|
||||||
|
if (removeOnDone) {
|
||||||
|
const doneJobs = queueJobs.filter(j => j.status === 'done');
|
||||||
|
for (const job of doneJobs) {
|
||||||
|
removeJobFromIndex(job);
|
||||||
|
selectedJobIds.delete(job.id);
|
||||||
|
}
|
||||||
|
queueJobs = queueJobs.filter(j => j.status !== 'done');
|
||||||
|
renderQueueTable();
|
||||||
|
}
|
||||||
|
|
||||||
clearPersistedQueueStateSoon();
|
clearPersistedQueueStateSoon();
|
||||||
|
|
||||||
// Final stats update
|
// Final stats update
|
||||||
@ -976,6 +989,10 @@ function renderSettings() {
|
|||||||
<label>Queue nach Neustart fortsetzen</label>
|
<label>Queue nach Neustart fortsetzen</label>
|
||||||
<input type="checkbox" id="resumeQueueOnLaunchInput" ${globalSettings.resumeQueueOnLaunch === false ? '' : 'checked'}>
|
<input type="checkbox" id="resumeQueueOnLaunchInput" ${globalSettings.resumeQueueOnLaunch === false ? '' : 'checked'}>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-row checkbox-row">
|
||||||
|
<label>Aus der Queue entfernen bei Abschluss</label>
|
||||||
|
<input type="checkbox" id="removeFromQueueOnDoneInput" ${globalSettings.removeFromQueueOnDone ? 'checked' : ''}>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
container.appendChild(generalPanel);
|
container.appendChild(generalPanel);
|
||||||
@ -1054,7 +1071,8 @@ async function saveSettings() {
|
|||||||
const globalSettings = {
|
const globalSettings = {
|
||||||
...(config.globalSettings || {}),
|
...(config.globalSettings || {}),
|
||||||
logFilePath: (document.getElementById('logFilePathInput')?.value || '').trim(),
|
logFilePath: (document.getElementById('logFilePathInput')?.value || '').trim(),
|
||||||
resumeQueueOnLaunch: !!document.getElementById('resumeQueueOnLaunchInput')?.checked
|
resumeQueueOnLaunch: !!document.getElementById('resumeQueueOnLaunchInput')?.checked,
|
||||||
|
removeFromQueueOnDone: !!document.getElementById('removeFromQueueOnDoneInput')?.checked
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const name of HOSTERS) {
|
for (const name of HOSTERS) {
|
||||||
@ -1091,6 +1109,7 @@ function getHostersWithoutCreds() {
|
|||||||
function getCredentialLabel(name, hoster) {
|
function getCredentialLabel(name, hoster) {
|
||||||
if (name === 'vidmoly.me') return hoster.username || 'Login';
|
if (name === 'vidmoly.me') return hoster.username || 'Login';
|
||||||
if (name === 'voe.sx') return hoster.username && hoster.password ? (hoster.username || 'Login') : 'API-Key';
|
if (name === 'voe.sx') return hoster.username && hoster.password ? (hoster.username || 'Login') : 'API-Key';
|
||||||
|
if (name === 'doodstream.com') return hoster.username && hoster.password ? (hoster.username || 'Login') : 'API-Key';
|
||||||
return 'API-Key';
|
return 'API-Key';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1176,7 +1195,7 @@ function getCredsFieldsHtml(name, hoster) {
|
|||||||
<button class="toggle-vis" type="button" title="Anzeigen">👁</button>
|
<button class="toggle-vis" type="button" title="Anzeigen">👁</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
if (name === 'voe.sx') {
|
if (name === 'voe.sx' || name === 'doodstream.com') {
|
||||||
return `
|
return `
|
||||||
<div class="settings-row">
|
<div class="settings-row">
|
||||||
<label>E-Mail (Login)</label>
|
<label>E-Mail (Login)</label>
|
||||||
@ -1274,7 +1293,7 @@ async function deleteAccount(hosterName) {
|
|||||||
// Reset credentials to defaults
|
// Reset credentials to defaults
|
||||||
if (hosterName === 'vidmoly.me') {
|
if (hosterName === 'vidmoly.me') {
|
||||||
hosters[hosterName] = { enabled: false, authType: 'login', username: '', password: '' };
|
hosters[hosterName] = { enabled: false, authType: 'login', username: '', password: '' };
|
||||||
} else if (hosterName === 'voe.sx') {
|
} else if (hosterName === 'voe.sx' || hosterName === 'doodstream.com') {
|
||||||
hosters[hosterName] = { enabled: false, username: '', password: '', apiKey: '' };
|
hosters[hosterName] = { enabled: false, username: '', password: '', apiKey: '' };
|
||||||
} else {
|
} else {
|
||||||
hosters[hosterName] = { enabled: false, apiKey: '' };
|
hosters[hosterName] = { enabled: false, apiKey: '' };
|
||||||
@ -1296,7 +1315,7 @@ function readAccountCredsFromModal(hosterName) {
|
|||||||
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') {
|
if (hosterName === 'voe.sx' || hosterName === 'doodstream.com') {
|
||||||
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();
|
||||||
const apiKey = (document.getElementById('accField_apiKey')?.value || '').trim();
|
const apiKey = (document.getElementById('accField_apiKey')?.value || '').trim();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user