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;