this lets us send opengraph tags in the response :D might make the player also be server-side rendered as well
147 lines
4.2 KiB
TypeScript
147 lines
4.2 KiB
TypeScript
import { ensureDir } from "@std/fs";
|
|
import { serveDir, serveFile } from "@std/http/file-server";
|
|
|
|
import * as z from "@zod/mini";
|
|
import vento from "ventojs";
|
|
import { db } from "./db.ts";
|
|
|
|
import { getPdsUrl, resolveHandle } from "../common/identity.ts";
|
|
import type { VideoRecord } from "../common/lexicons.ts";
|
|
import { VIDEO_PATTERN } from "../common/routes.ts";
|
|
import { getVideoRecord } from "../common/video.ts";
|
|
|
|
const vto = vento({ includes: "./web" });
|
|
|
|
async function getOrFetchVideo(did: string, blobCid: string): Promise<string> {
|
|
const cached = db.getVideo(did, blobCid);
|
|
if (cached) return cached;
|
|
|
|
const pdsUrl = await getPdsUrl(did);
|
|
if (!pdsUrl) throw new Error("Could not resolve PDS");
|
|
|
|
let filename: string = crypto.randomUUID();
|
|
|
|
const getBlobURL = new URL("/xrpc/com.atproto.sync.getBlob", pdsUrl);
|
|
getBlobURL.searchParams.set("did", did);
|
|
getBlobURL.searchParams.set("cid", blobCid);
|
|
|
|
const blobResponse = await fetch(getBlobURL, {
|
|
headers: { "user-agent": "video.cerulea.app" },
|
|
});
|
|
|
|
const contentType = blobResponse.headers.get("content-type");
|
|
if (contentType === "video/mp4") filename += ".mp4";
|
|
if (contentType === "video/webm") filename += ".webm";
|
|
|
|
await ensureDir("./data/videos");
|
|
using file = await Deno.open("./data/videos/" + filename, {
|
|
write: true,
|
|
createNew: true,
|
|
});
|
|
|
|
await blobResponse.body?.pipeTo(file.writable);
|
|
|
|
db.addVideo(did, blobCid, filename);
|
|
return filename;
|
|
}
|
|
|
|
async function fetchVideo(req: Request): Promise<Response> {
|
|
if (req.method === "OPTIONS") {
|
|
return new Response(null, {
|
|
headers: {
|
|
"access-control-allow-origin": "*",
|
|
},
|
|
});
|
|
}
|
|
|
|
if (req.method !== "POST") {
|
|
return new Response("Method Not Allowed", { status: 405 });
|
|
}
|
|
|
|
const BodySchema = z.object({
|
|
repo: z.string().check(z.startsWith("did:")),
|
|
blob: z.string(),
|
|
});
|
|
const body = BodySchema.parse(await req.json());
|
|
|
|
if (!db.inAllowlist(body.repo)) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: "Denied",
|
|
message: "repo is not allowlisted on AppView",
|
|
}),
|
|
{
|
|
status: 400,
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"access-control-allow-origin": "*",
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
const filename = await getOrFetchVideo(body.repo, body.blob);
|
|
return new Response(JSON.stringify({ filename }), {
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"access-control-allow-origin": "*",
|
|
},
|
|
});
|
|
}
|
|
|
|
export default {
|
|
async fetch(req: Request, _info): Promise<Response> {
|
|
const pathname = new URL(req.url).pathname;
|
|
if (pathname === "/xrpc/blue.cerulea.video.fetchVideo") {
|
|
return await fetchVideo(req);
|
|
}
|
|
|
|
if (pathname.startsWith("/user-content/")) {
|
|
return await serveDir(req, {
|
|
fsRoot: "./data/videos",
|
|
quiet: true,
|
|
enableCors: true,
|
|
urlRoot: "user-content",
|
|
});
|
|
}
|
|
|
|
const videoRoute = VIDEO_PATTERN.exec(req.url);
|
|
if (videoRoute) {
|
|
const { repo, rkey } = videoRoute.pathname.groups as Record<"repo" | "rkey", string>;
|
|
const did = repo.startsWith("did:") ? repo : await resolveHandle(repo);
|
|
|
|
if (!db.inAllowlist(did)) {
|
|
return await serveFile(req, "./web/viewer.html");
|
|
}
|
|
|
|
let record = db.getRecord<VideoRecord>(did, rkey);
|
|
if (!record) {
|
|
record = await getVideoRecord(did, rkey);
|
|
db.addRecord(did, rkey, record);
|
|
}
|
|
|
|
let videoUrl: string | undefined;
|
|
let videoType: string | undefined;
|
|
if ("$type" in record.video && record.video?.$type === "blob") {
|
|
const blobCid = record.video.ref.$link;
|
|
const filename = await getOrFetchVideo(did, blobCid);
|
|
videoUrl = `${new URL(req.url).origin}/user-content/${filename}`;
|
|
videoType = record.video.mimeType;
|
|
}
|
|
|
|
const result = await vto.run("viewer.vto", {
|
|
title: record.title,
|
|
description: record.description,
|
|
pageUrl: req.url,
|
|
videoUrl,
|
|
videoType,
|
|
});
|
|
|
|
return new Response(result.content, {
|
|
headers: { "content-type": "text/html; charset=utf-8" },
|
|
});
|
|
}
|
|
|
|
return await serveDir(req, { fsRoot: "./web", quiet: true });
|
|
},
|
|
} satisfies Deno.ServeDefaultExport;
|