server.mjs
import http from 'node:http';
import {stat,writeFile} from 'node:fs/promises';
import {watch,createReadStream} from 'node:fs';
import {fileURLToPath} from 'node:url';
import path from 'node:path';
const root=path.dirname(fileURLToPath(import.meta.url));
const ghostAssets=path.resolve(root,'../../<3/assets');
const media=new Map([
['/media/lotus-square.mp4',path.join(ghostAssets,'lotus-bloom-square-10s.mp4')],
['/media/lotus-poster.jpg',path.join(ghostAssets,'lotus-bloom-poster.jpg')],
['/media/announcement-music.wav',path.join(ghostAssets,'lotus-announcement-music-37-47.wav')]
]);
const clients=new Set();
const mime={'.html':'text/html','.js':'text/javascript','.mjs':'text/javascript','.css':'text/css','.svg':'image/svg+xml','.json':'application/json','.png':'image/png','.jpg':'image/jpeg','.mp4':'video/mp4','.wav':'audio/wav'};
const server=http.createServer(async(req,res)=>{
try{
const url=new URL(req.url,'http://localhost');
if(url.pathname==='/__export'&&req.method==='POST'){
const expectedOrigin=`http://127.0.0.1:${server.address().port}`;
const type=req.headers['content-type'];
if(req.headers.origin!==expectedOrigin||!['video/mp4','video/webm'].includes(type)){res.writeHead(403);res.end('Invalid export');return;}
const pop=Number(url.searchParams.get('pop')),grain=Number(url.searchParams.get('grain'));
if(![pop,grain].every(n=>Number.isInteger(n)&&n>=0&&n<=100)){res.writeHead(400);res.end('Invalid settings');return;}
const chunks=[];let size=0;
for await(const chunk of req){size+=chunk.length;if(size>100*1024*1024){res.writeHead(413);res.end('Export too large');return;}chunks.push(chunk);}
const extension=type==='video/mp4'?'mp4':'webm';
const filename=`lotus-pop${pop}-grain${grain}-recording-${Date.now()}.${extension}`;
await writeFile(path.join(ghostAssets,filename),Buffer.concat(chunks),{flag:'wx'});
res.writeHead(201,{'Content-Type':'application/json'});res.end(JSON.stringify({filename}));return;
}
if(url.pathname==='/__events'){
res.writeHead(200,{'Content-Type':'text/event-stream','Cache-Control':'no-cache','Connection':'keep-alive'});
res.write(': connected\n\n');clients.add(res);req.on('close',()=>clients.delete(res));return;
}
if(url.pathname==='/__health'){res.writeHead(200,{'Content-Type':'application/json'});res.end('{"ok":true,"scene":"lotus-video"}');return;}
const relative=decodeURIComponent(url.pathname==='/'?'/index.html':url.pathname);
const mapped=media.get(relative);
const file=mapped||path.resolve(root,'.'+relative);
if(!mapped&&!file.startsWith(root+path.sep)){res.writeHead(404);res.end('Not found');return;}
const info=await stat(file);
if(!info.isFile()){res.writeHead(404);res.end('Not found');return;}
const headers={'Content-Type':mime[path.extname(file)]||'application/octet-stream','Cache-Control':'no-store','Accept-Ranges':'bytes'};
let start=0,end=info.size-1,status=200;
if(req.headers.range){
const range=/^bytes=(\d*)-(\d*)$/.exec(req.headers.range);
if(!range||(!range[1]&&!range[2])){res.writeHead(416,{'Content-Range':`bytes */${info.size}`});res.end();return;}
if(!range[1])start=Math.max(0,info.size-Number(range[2]));
else{start=Number(range[1]);if(range[2])end=Math.min(end,Number(range[2]));}
if(start>end||start>=info.size){res.writeHead(416,{'Content-Range':`bytes */${info.size}`});res.end();return;}
status=206;headers['Content-Range']=`bytes ${start}-${end}/${info.size}`;
}
headers['Content-Length']=end-start+1;res.writeHead(status,headers);
if(req.method==='HEAD'){res.end();return;}
const stream=createReadStream(file,{start,end});stream.on('error',()=>res.destroy());res.on('close',()=>stream.destroy());stream.pipe(res);
}catch{if(!res.headersSent){res.writeHead(404);res.end('Not found');}else res.destroy();}
});
let debounce;
watch(root,{recursive:true},(_,file)=>{
if(!file||file.startsWith('vendor/')||file.startsWith('previous-procedural/')||!/\.(html|css|js)$/.test(file))return;
clearTimeout(debounce);debounce=setTimeout(()=>clients.forEach(c=>c.write('data: reload\n\n')),150);
});
server.listen(Number(process.env.PORT||4173),'127.0.0.1',()=>console.log(`Local: http://127.0.0.1:${server.address().port}`));
process.on('SIGTERM',()=>{for(const client of clients)client.end();server.close(()=>process.exit());});