Initial commit

This commit is contained in:
admin
2026-05-26 22:46:00 +02:00
commit 7e2c2ff89c
256 changed files with 51523 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
import { NextRequest } from "next/server";
import path from "node:path";
import fs from "node:fs";
import fsp from "node:fs/promises";
import { findVideosForCode } from "@/lib/video";
import { assertLocalRequest } from "@/lib/api/localOnly";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
const MIME_BY_EXT: Record<string, string> = {
".mp4": "video/mp4",
".m4v": "video/mp4",
".mov": "video/quicktime",
".webm": "video/webm",
".mkv": "video/x-matroska",
".avi": "video/x-msvideo",
".wmv": "video/x-ms-wmv",
".ts": "video/mp2t",
".mpg": "video/mpeg",
".mpeg": "video/mpeg",
".flv": "video/x-flv",
};
export async function GET(req: NextRequest, ctx: { params: Promise<{ code: string }> }) {
const blocked = assertLocalRequest(req);
if (blocked) return blocked;
const { code } = await ctx.params;
const decoded = decodeURIComponent(code);
const url = new URL(req.url);
const partRaw = url.searchParams.get("part");
const part = partRaw == null ? 0 : Math.max(0, parseInt(partRaw, 10) || 0);
const files = findVideosForCode(decoded);
if (files.length === 0) return new Response("not found", { status: 404 });
const file = files[Math.min(part, files.length - 1)];
let stat: import("node:fs").Stats;
try {
stat = await fsp.stat(file.abs);
} catch {
return new Response("not found", { status: 404 });
}
const total = stat.size;
const ext = path.extname(file.abs).toLowerCase();
const mime = MIME_BY_EXT[ext] ?? "application/octet-stream";
// Stable identity for the byte stream — lets the browser's HTTP cache
// hold onto previously fetched ranges (the moov tail in particular)
// instead of re-hitting our endpoint on every seek / buffer-ahead.
const etag = `"${stat.size.toString(36)}-${Math.floor(stat.mtimeMs).toString(36)}"`;
const lastModified = new Date(stat.mtimeMs).toUTCString();
const range = req.headers.get("range");
const baseHeaders: Record<string, string> = {
"Content-Type": mime,
"Accept-Ranges": "bytes",
"Cache-Control": "private, max-age=3600",
"ETag": etag,
"Last-Modified": lastModified,
"Content-Disposition": `inline; filename="${encodeURIComponent(file.filename)}"`,
};
if (!range) {
return new Response(streamFile(file.abs, undefined, undefined, req.signal), {
status: 200,
headers: { ...baseHeaders, "Content-Length": String(total) },
});
}
// Parse "bytes=START-END"; END may be empty for "until end", and
// START may be empty for HTTP suffix ranges ("last N bytes").
const m = /^bytes=(\d*)-(\d*)$/.exec(range);
if (!m) return new Response("bad range", { status: 416 });
let start: number;
let end: number;
if (m[1] === "") {
const suffixLen = Number(m[2]);
if (!Number.isFinite(suffixLen) || suffixLen <= 0) {
return new Response("bad range", { status: 416, headers: { "Content-Range": `bytes */${total}` } });
}
start = Math.max(total - suffixLen, 0);
end = total - 1;
} else {
start = Number(m[1]);
end = m[2] === "" ? total - 1 : Number(m[2]);
}
if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || end >= total) {
return new Response("bad range", { status: 416, headers: { "Content-Range": `bytes */${total}` } });
}
const len = end - start + 1;
return new Response(streamFile(file.abs, start, end, req.signal), {
status: 206,
headers: {
...baseHeaders,
"Content-Range": `bytes ${start}-${end}/${total}`,
"Content-Length": String(len),
},
});
}
/**
* Pipe a file slice into a Web ReadableStream that the runtime can hand
* to fetch's Response. Tying the read stream to the request's AbortSignal
* is the bit that fixes "Invalid state: Controller is already closed":
* when the browser cancels (modal close, seek, network blip) the Node
* stream is destroyed before it can push more bytes into a stream the
* runtime has already closed.
*/
function streamFile(
abs: string,
start: number | undefined,
end: number | undefined,
signal: AbortSignal,
): ReadableStream<Uint8Array> {
let node: fs.ReadStream | null = null;
let closed = false;
return new ReadableStream<Uint8Array>({
start(controller) {
node = fs.createReadStream(abs, { start, end });
const finish = () => {
if (closed) return;
closed = true;
try { controller.close(); } catch { /* already closed */ }
};
const fail = (err: Error) => {
if (closed) return;
closed = true;
try { controller.error(err); } catch { /* already closed */ }
};
node.on("data", (chunk: unknown) => {
if (closed) return;
try {
const u8 = chunk instanceof Uint8Array
? chunk
: new Uint8Array(chunk as ArrayBufferLike);
controller.enqueue(u8);
} catch {
closed = true;
node?.destroy();
}
});
node.on("end", finish);
node.on("error", (err) => fail(err as Error));
const onAbort = () => {
closed = true;
node?.destroy();
};
if (signal.aborted) onAbort();
else signal.addEventListener("abort", onAbort, { once: true });
},
cancel() {
// ReadableStream.cancel() fires when the consumer is done before
// req.signal aborts (e.g. browser closes the response body cleanly
// after a Range fulfill). Without destroying the node stream here,
// the open file handle leaks until GC.
closed = true;
node?.destroy();
},
});
}