← back to Clipreviewbydave
server.js
130 lines
'use strict';
// ClipReviewbyDave — zero-dependency static server + local-media picker on a fixed port.
// Serves public/ (index.html = the clip-review tool by Dave) AND exposes the user's
// own video files so they can be loaded by URL instead of dragged. Still 100% local:
// files are read straight off disk and streamed to the same-origin browser; nothing
// is uploaded anywhere.
const http = require('http');
const fs = require('fs');
const os = require('os');
const path = require('path');
const PORT = process.env.PORT || 9736;
const ROOT = path.join(__dirname, 'public');
const HOME = os.homedir();
// Directories whose video files the picker may list + stream. Override with
// MEDIA_DIRS=/a:/b. Defaults cover where Steve's renders land.
const MEDIA_DIRS = (process.env.MEDIA_DIRS
? process.env.MEDIA_DIRS.split(':')
: ['Videos', 'Downloads', 'Movies', 'Desktop'].map((d) => path.join(HOME, d))
).map((d) => { try { return fs.realpathSync(d); } catch { return null; } }).filter(Boolean);
const VIDEO_RE = /\.(mp4|mov|m4v|webm|mkv|mpg|mpeg)$/i;
const SKIP_DIR = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'tmp']);
const WALK_CAP = 1500; // safety bound on files inspected per request
const TYPES = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.ico': 'image/x-icon',
'.mp4': 'video/mp4', '.m4v': 'video/x-m4v', '.mov': 'video/quicktime',
'.webm': 'video/webm', '.mkv': 'video/x-matroska', '.mpg': 'video/mpeg', '.mpeg': 'video/mpeg',
};
// --- local media discovery ---------------------------------------------------
function listVideos() {
const out = [];
let seen = 0;
for (const root of MEDIA_DIRS) {
const stack = [root];
while (stack.length && seen < WALK_CAP) {
const dir = stack.pop();
let entries;
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
for (const e of entries) {
if (seen >= WALK_CAP) break;
const full = path.join(dir, e.name);
if (e.isDirectory()) { if (!SKIP_DIR.has(e.name) && !e.name.startsWith('.')) stack.push(full); continue; }
if (!VIDEO_RE.test(e.name)) continue;
seen++;
let st; try { st = fs.statSync(full); } catch { continue; }
out.push({
name: e.name,
url: '/media?p=' + encodeURIComponent(full),
folder: path.basename(dir),
size: st.size,
mtime: st.mtimeMs,
when: new Date(st.mtimeMs).toLocaleString(undefined,
{ year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }),
});
}
}
}
out.sort((a, b) => b.mtime - a.mtime); // newest first
return out;
}
// --- byte-range media streaming (required for scrubbing large video) ---------
function isAllowed(abs) {
let real; try { real = fs.realpathSync(abs); } catch { return false; }
return MEDIA_DIRS.some((root) => real === root || real.startsWith(root + path.sep));
}
function serveMedia(req, res, abs) {
if (!isAllowed(abs)) { res.writeHead(403); return res.end('forbidden'); }
let st; try { st = fs.statSync(abs); } catch { res.writeHead(404); return res.end('not found'); }
const type = TYPES[path.extname(abs).toLowerCase()] || 'application/octet-stream';
const range = req.headers.range;
if (range) {
const m = /bytes=(\d*)-(\d*)/.exec(range) || [];
let start = m[1] ? parseInt(m[1], 10) : 0;
let end = m[2] ? parseInt(m[2], 10) : st.size - 1;
if (isNaN(start) || isNaN(end) || start > end || start >= st.size) {
res.writeHead(416, { 'Content-Range': `bytes */${st.size}` }); return res.end();
}
res.writeHead(206, {
'Content-Type': type, 'Accept-Ranges': 'bytes',
'Content-Range': `bytes ${start}-${end}/${st.size}`, 'Content-Length': end - start + 1,
});
fs.createReadStream(abs, { start, end }).pipe(res);
} else {
res.writeHead(200, { 'Content-Type': type, 'Accept-Ranges': 'bytes', 'Content-Length': st.size });
fs.createReadStream(abs).pipe(res);
}
}
// --- static public/ ----------------------------------------------------------
function serveStatic(req, res, rel) {
if (rel === '/' || rel === '') rel = '/index.html';
const filePath = path.normalize(path.join(ROOT, rel));
if (!filePath.startsWith(ROOT)) { res.writeHead(403); return res.end('forbidden'); }
fs.readFile(filePath, (err, buf) => {
if (err) { res.writeHead(404); return res.end('not found'); }
res.writeHead(200, { 'Content-Type': TYPES[path.extname(filePath).toLowerCase()] || 'application/octet-stream' });
res.end(buf);
});
}
const server = http.createServer((req, res) => {
const u = new URL(req.url, `http://127.0.0.1:${PORT}`);
if (u.pathname === '/healthz') { res.writeHead(200); return res.end('ok'); }
if (u.pathname === '/api/videos') {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
return res.end(JSON.stringify(listVideos()));
}
if (u.pathname === '/media') {
const p = u.searchParams.get('p');
if (!p) { res.writeHead(400); return res.end('missing p'); }
return serveMedia(req, res, path.normalize(p));
}
serveStatic(req, res, decodeURIComponent(u.pathname));
});
server.listen(PORT, () => {
console.log(`ClipReviewbyDave serving on http://127.0.0.1:${PORT}/`);
console.log(` media dirs: ${MEDIA_DIRS.join(', ') || '(none)'}`);
});