From 06d03e6978b3d7cbbbb9b33bc0bb31d9ee859733 Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 11 Mar 2026 00:04:18 +0100 Subject: [PATCH] feat: add voe.sx login-based upload support --- lib/upload-manager.js | 5 + lib/voe-upload.js | 373 ++++++++++++++++++++++++++++++++++++++++++ main.js | 5 + renderer/app.js | 28 +++- 4 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 lib/voe-upload.js diff --git a/lib/upload-manager.js b/lib/upload-manager.js index d0a8bb9..8be88fb 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -4,6 +4,7 @@ const fs = require('fs'); const crypto = require('crypto'); const { uploadFile } = require('./hosters'); const VidmolyUploader = require('./vidmoly-upload'); +const VoeUploader = require('./voe-upload'); const Semaphore = require('./semaphore'); const Throttle = require('./throttle'); @@ -249,6 +250,10 @@ class UploadManager extends EventEmitter { const vidmoly = new VidmolyUploader(); await vidmoly.login(task.username, task.password); result = await vidmoly.upload(task.file, progressCb, jobSignal, 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, jobSignal, throttle); } else { result = await uploadFile(task.hoster, task.file, task.apiKey, progressCb, jobSignal, throttle); } diff --git a/lib/voe-upload.js b/lib/voe-upload.js new file mode 100644 index 0000000..c925a1c --- /dev/null +++ b/lib/voe-upload.js @@ -0,0 +1,373 @@ +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const { request } = require('undici'); + +const BASE_URL = 'https://voe.sx'; +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 +const RESULT_POLL_ATTEMPTS = 10; +const RESULT_POLL_DELAY_MS = 2000; + +/** + * Login-based upload for VOE.sx (Laravel / FilePond) + * Fallback when API-based upload fails or is unavailable. + */ +class VoeUploader { + constructor() { + this.cookies = new Map(); + } + + _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()); + } + } + } + + /** + * GET/POST with cookie management and manual redirect following + */ + 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; + } + + /** + * Extract CSRF token from page HTML + */ + _extractCsrfToken(html) { + // Laravel meta tag + const metaMatch = html.match(/