← Files CastReaderARCHIVED FILE

assets/answer-reader/server.mjs

2.68 KB · Sep 30, 2026 · 23:16 UTC

↓ Download file

// Local, read-only experiment page. No API credentials or paid synthesis.
import {createServer} from 'node:http';
import {readFile} from 'node:fs/promises';
import {resolve,join} from 'node:path';
import {fileURLToPath} from 'node:url';

const files=new Map([
 ['/', ['index.html','text/html; charset=utf-8']],
 ['/index.html', ['index.html','text/html; charset=utf-8']],
 ['/reader.mjs', ['reader.mjs','text/javascript; charset=utf-8']],
 ['/style.css', ['style.css','text/css; charset=utf-8']],
 ['/sync.mjs', ['sync.mjs','text/javascript; charset=utf-8']],
 ['/reader-data.json', ['reader-data.json','application/json; charset=utf-8']],
 ['/answer.txt', ['answer.txt','text/plain; charset=utf-8']],
 ['/audio.wav', ['audio.wav','audio/wav']],
 ['/audio.mp3', ['audio.mp3','audio/mpeg']],
]);
export function createReaderServer(directory){
 const server=createServer((req,res)=>{
  const respond=async()=>{
   res.setHeader('Cache-Control','no-store');
   res.setHeader('X-Content-Type-Options','nosniff');
   res.setHeader('Cross-Origin-Resource-Policy','same-origin');
   if(req.headers.host!==`127.0.0.1:${server.address().port}`){res.writeHead(403).end();return;}
   if(!['GET','HEAD'].includes(req.method)){res.writeHead(405,{'Allow':'GET, HEAD'}).end();return;}
   const item=files.get(req.url);
   if(!item){res.writeHead(404).end();return;}
   const data=await readFile(join(directory,item[0]));
   let start=0,end=data.length-1,status=200;
   res.setHeader('Content-Type',item[1]);
   res.setHeader('Accept-Ranges','bytes');
   if(req.headers.range){
    const match=/^bytes=(\d*)-(\d*)$/.exec(req.headers.range);
    if(match&&(match[1]||match[2])){
     start=match[1]?Number(match[1]):Math.max(0,data.length-Number(match[2]));
     end=match[1]&&match[2]?Math.min(Number(match[2]),data.length-1):data.length-1;
    }else start=NaN;
    if(!Number.isSafeInteger(start)||!Number.isSafeInteger(end)||start<0||start>end||start>=data.length){
     res.writeHead(416,{'Content-Range':`bytes */${data.length}`}).end();return;
    }
    status=206;res.setHeader('Content-Range',`bytes ${start}-${end}/${data.length}`);
   }
   res.setHeader('Content-Length',end-start+1);
   res.writeHead(status);res.end(req.method==='HEAD'?undefined:data.subarray(start,end+1));
  };
  respond().catch(()=>{if(!res.headersSent)res.writeHead(500);res.end();});
 });
 return server;
}
if(process.argv[1]&&resolve(process.argv[1])===fileURLToPath(import.meta.url)){
 if(!process.argv[2])throw Error('Usage: node server.mjs APP_DIRECTORY [PORT]');
 const server=createReaderServer(resolve(process.argv[2]));
 server.listen(Number(process.argv[3]??0),'127.0.0.1',()=>console.log(`CastReader local experiment: http://127.0.0.1:${server.address().port}/`));
}

SHA-256: ad52aa3024bc7e804ce34dcc0a1dbff5b01915e31d6ccaed440a0ffbe26bd5b7