// server.js (Node.js + Express) - yt-dlp + ffmpeg required const express = require('express'); const rateLimit = require('express-rate-limit'); const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); const os = require('os'); const helmet = require('helmet'); const cors = require('cors'); const app = express(); app.use(helmet()); app.use(cors()); app.use(express.json({ limit:'1mb' })); const limiter = rateLimit({ windowMs:60*1000, max:6 }); app.use(limiter); const TMP = process.env.TEMP_FOLDER || path.join(os.tmpdir(),'multiimagex-yt-mp3'); if(!fs.existsSync(TMP)) fs.mkdirSync(TMP,{recursive:true}); function safeName(pre){ return `${pre}-${Date.now()}-${Math.random().toString(36).slice(2,8)}.mp3`; } function validateYT(u){ try{ const url = new URL(u); return /youtube|youtu.be/.test(url.hostname); }catch(e){ return false; } } app.post('/api/convert-mp3', async (req,res)=>{ const { url, bitrate } = req.body || {}; if(!url || !validateYT(url)) return res.json({ ok:false, error:'Invalid YouTube URL' }); const bit = (['128','192','256','320'].includes(String(bitrate)))? String(bitrate) : '320'; const outBasename = safeName('audio'); const outTemplate = path.join(TMP, outBasename.replace('.mp3','.%\(ext\)s')); // yt-dlp template // Build args const args = [ '-f','bestaudio', '--no-playlist', '--restrict-filenames', '-o', path.join(TMP, outBasename.replace('.mp3','.%(ext)s')), url, '--extract-audio','--audio-format','mp3','--postprocessor-args', `-b:a ${bit}k` ]; // spawn yt-dlp try{ const proc = spawn('yt-dlp', args, { shell: true }); let stderr = ''; proc.stderr.on('data', d => stderr += d.toString()); proc.on('close', code => { // find newest mp3 in TMP const files = fs.readdirSync(TMP).filter(f => f.endsWith('.mp3')).map(f => ({ f, m: fs.statSync(path.join(TMP,f)).mtimeMs })).sort((a,b)=>b.m-a.m); if(!files.length) return res.json({ ok:false, error: 'conversion failed', details: stderr }); const latest = files[0].f; return res.json({ ok:true, downloadUrl: '/static/' + encodeURIComponent(latest) }); }); }catch(err){ return res.json({ ok:false, error: err.message }); } }); app.use('/static', express.static(TMP, { index:false })); // cleanup files older than 12 hours setInterval(()=>{ try{ const now = Date.now(); fs.readdirSync(TMP).forEach(f=>{ const p = path.join(TMP,f); try{ const s = fs.statSync(p); if(now - s.mtimeMs > 12*3600*1000) fs.unlinkSync(p); }catch(e){} }); }catch(e){} }, 60*60*1000); const PORT = process.env.PORT || 3000; app.listen(PORT, ()=> console.log('YT→MP3 server running on', PORT));