const express = require('express') const { chromium } = require('playwright') const fs = require('fs') const path = require('path') const http = require('http') const https = require('https') const app = express() app.use(express.json()) const PORT = parseInt(process.env.PORT || '7860') const MAX_WORKERS = 4 const JOB_EXPIRY_MS = 30 * 60 * 1000 const SCREENSHOT_EXPIRY_MS = 10 * 60 * 1000 const HARD_TIMEOUT_MS = 120000 const SCREENSHOT_BASE_URL = process.env.SCREENSHOT_BASE_URL || `http://localhost:${PORT}` const PUBLIC_DIR = path.join(__dirname, 'public') if (!fs.existsSync(PUBLIC_DIR)) fs.mkdirSync(PUBLIC_DIR) const rooms = new Map() const jobs = new Map() let availableRooms = [] let nextRoomId = 1 function makeJobId() { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' let id do { id = Array.from({ length: 3 }, () => chars[Math.floor(Math.random() * chars.length)]).join('') } while (jobs.has(id)) return id } async function createRoom() { const roomId = nextRoomId++ const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-blink-features=AutomationControlled'] }) const screenshotsDir = path.join(PUBLIC_DIR, `room_${roomId}`) if (!fs.existsSync(screenshotsDir)) fs.mkdirSync(screenshotsDir) const room = { roomId, browser, context: null, page: null, cookies: [], isBusy: false, currentJobId: null, lastActivity: Date.now(), screenshotsDir } rooms.set(roomId, room) return room } async function resetRoomContext(room) { try { if (room.page) { try { await room.page.close() } catch {} room.page = null } if (room.context) { try { room.cookies = await room.context.cookies() } catch {} try { await room.context.close() } catch {} room.context = null } } catch (e) {} } function cleanupScreenshots(room) { try { const now = Date.now() for (const f of fs.readdirSync(room.screenshotsDir)) { const fp = path.join(room.screenshotsDir, f) if (now - fs.statSync(fp).mtimeMs > SCREENSHOT_EXPIRY_MS) fs.unlinkSync(fp) } } catch {} } function acquireRoom(jobId) { return new Promise((resolve, reject) => { const start = Date.now() const tryAcquire = () => { const idx = availableRooms.findIndex(id => { const r = rooms.get(id); return r && !r.isBusy }) if (idx !== -1) { const roomId = availableRooms.splice(idx, 1)[0] const room = rooms.get(roomId) room.isBusy = true room.currentJobId = jobId room.lastActivity = Date.now() return resolve(room) } if (Date.now() - start > 120000) return reject(new Error('Timeout waiting for available room')) setTimeout(tryAcquire, 1000) } tryAcquire() }) } function releaseRoom(room) { room.isBusy = false room.currentJobId = null room.lastActivity = Date.now() availableRooms = availableRooms.filter(id => id !== room.roomId) availableRooms.push(room.roomId) } async function executeInRoom(code, room, jobId) { const result = { screenshots: [], data: null, error: null } await resetRoomContext(room) try { const context = await room.browser.newContext({ viewport: { width: 1920, height: 1080 }, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', ignoreHTTPSErrors: true, locale: 'en-US', }) room.context = context if (room.cookies.length > 0) { try { await context.addCookies(room.cookies) } catch {} } const page = await context.newPage() room.page = page const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor const fn = new AsyncFunction('browser', 'context', 'page', 'roomCookies', 'publicDir', 'rootPublicDir', 'roomId', 'jobId', 'result', code) const hardTimeout = new Promise((_, rej) => setTimeout(() => rej(new Error(`Hard timeout after ${HARD_TIMEOUT_MS}ms`)), HARD_TIMEOUT_MS)) const returnValue = await Promise.race([ fn(room.browser, context, page, room.cookies, room.screenshotsDir, PUBLIC_DIR, room.roomId, jobId, result), hardTimeout ]) if (returnValue !== undefined) result.data = returnValue } catch (e) { result.error = e.message + '\n' + e.stack } finally { await resetRoomContext(room) } return result } async function processJob(job) { let room = null try { job.status = 'running' job.startedAt = new Date() room = await acquireRoom(job.jobId) job.roomId = room.roomId const result = await executeInRoom(job.code, room, job.jobId) job.screenshots = fs.readdirSync(room.screenshotsDir) .filter(f => f.endsWith('.png')) .sort((a, b) => fs.statSync(path.join(room.screenshotsDir, a)).mtimeMs - fs.statSync(path.join(room.screenshotsDir, b)).mtimeMs) .map(f => ({ name: f, publicURL: `${SCREENSHOT_BASE_URL}/files/room_${room.roomId}/${f}` })) job.completedAt = new Date() if (result.error) { job.status = 'failed'; job.error = result.error } else { job.status = 'completed'; job.result = result.data } } catch (e) { job.status = 'failed' job.error = e.message + '\n' + e.stack job.completedAt = new Date() } finally { if (room) { cleanupScreenshots(room); releaseRoom(room) } } } function repairRooms() { for (const room of rooms.values()) { if (!room.isBusy && !availableRooms.includes(room.roomId)) availableRooms.push(room.roomId) } } function streamProxy(targetUrl, cookie, referer, req, res) { const doRequest = (url, redirectCount = 0) => { if (redirectCount > 10) return res.status(500).send('Too many redirects') const mod = url.startsWith('https') ? https : http mod.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36', 'Referer': referer || 'https://vidssave.com/', 'Origin': 'https://vidssave.com', ...(cookie ? { 'Cookie': cookie } : {}), ...(req.headers.range ? { 'Range': req.headers.range } : {}) } }, (upstream) => { if (upstream.statusCode >= 300 && upstream.statusCode < 400 && upstream.headers.location) { const loc = upstream.headers.location return doRequest(loc.startsWith('http') ? loc : new URL(loc, url).href, redirectCount + 1) } const headers = { 'Content-Type': upstream.headers['content-type'] || 'application/octet-stream', 'Accept-Ranges': 'bytes', 'Access-Control-Allow-Origin': '*', } if (upstream.headers['content-length']) headers['Content-Length'] = upstream.headers['content-length'] if (upstream.headers['content-range']) headers['Content-Range'] = upstream.headers['content-range'] if (upstream.headers['content-disposition']) headers['Content-Disposition'] = upstream.headers['content-disposition'] res.writeHead(upstream.statusCode, headers) upstream.pipe(res) upstream.on('error', () => res.end()) }).on('error', (e) => { if (!res.headersSent) res.status(500).send(e.message) }) } doRequest(targetUrl) } app.get('/proxy', (req, res) => { const targetUrl = req.query.url if (!targetUrl) return res.status(400).json({ error: 'Missing url parameter' }) const cookie = req.query.cookie ? decodeURIComponent(req.query.cookie) : '' const referer = req.query.referer ? decodeURIComponent(req.query.referer) : 'https://vidssave.com/' streamProxy(decodeURIComponent(targetUrl), cookie, referer, req, res) }) app.post('/api/s-playwright', (req, res) => { try { const { code, lang } = req.body || {} if (!code || !lang) return res.status(400).json({ success: false, error: 'Missing code or lang' }) if (lang.toLowerCase() !== 'js') return res.status(400).json({ success: false, error: `Only JS supported, got: ${lang}` }) const jobId = makeJobId() const job = { jobId, code, lang, status: 'queued', result: null, error: null, createdAt: new Date(), startedAt: null, completedAt: null, roomId: null, screenshots: [] } jobs.set(jobId, job) processJob(job) return res.json({ success: true, jobId, status: 'queued', checkUrl: `/job/${jobId}` }) } catch (e) { return res.status(500).json({ success: false, error: e.message }) } }) app.get('/job/:id', (req, res) => { const job = jobs.get(req.params.id.toUpperCase()) if (!job) return res.status(404).json({ success: false, error: 'Job not found' }) res.json({ success: true, job }) }) app.use('/files', express.static(PUBLIC_DIR)) app.get('/health', (req, res) => { repairRooms() const jobList = [...jobs.values()] res.json({ status: 'healthy', rooms: { total: MAX_WORKERS, available: availableRooms.length, busy: [...rooms.values()].filter(r => r.isBusy).length }, jobs: { total: jobList.length, queued: jobList.filter(j => j.status === 'queued').length, running: jobList.filter(j => j.status === 'running').length }, timestamp: Date.now(), }) }) app.get('/', (req, res) => { res.json({ message: 'Playwright API', endpoints: { 'POST /api/s-playwright': '{ code, lang: "js" }', 'GET /job/:id': 'Check job status', 'GET /proxy?url=&cookie=&referer=': 'Stream proxy', 'GET /health': 'Health check', }, availableVars: ['browser', 'context', 'page', 'roomCookies', 'publicDir', 'rootPublicDir', 'roomId', 'jobId', 'result'], }) }) async function init() { for (let i = 0; i < MAX_WORKERS; i++) { try { const room = await createRoom() availableRooms.push(room.roomId) console.log(`[POOL] Room ${room.roomId} ready`) } catch (e) { console.error(`[POOL] Failed to create room:`, e.message) } } setInterval(() => { repairRooms() const now = Date.now() for (const [id, job] of jobs.entries()) { if (now - new Date(job.createdAt).getTime() > JOB_EXPIRY_MS) jobs.delete(id) } for (const room of rooms.values()) { if (!room.isBusy) cleanupScreenshots(room) } }, 30000) app.listen(PORT, () => console.log(`Running on :${PORT}`)) } init()