complete viewer
now that PDSls has blob uploading i don't have to make the uploader first. yaaay
This commit is contained in:
parent
2ce74c6391
commit
a25ec9f235
12 changed files with 411 additions and 9 deletions
12
README.md
Normal file
12
README.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# video.cerulea.blue
|
||||||
|
|
||||||
|
the application comprises a video CDN and a client that fetches a record and displays a `<video>` tag.
|
||||||
|
the appview does not ingest from any event stream, but rather fetches videos on-demand.
|
||||||
|
|
||||||
|
there is an allowlist of trusted users whose videos can be proxied,
|
||||||
|
since re-serving arbitrary user content can be a big liability.
|
||||||
|
|
||||||
|
## to-do list
|
||||||
|
|
||||||
|
- video cdn garbage collection (so that disk usage doesn't grow unbounded)
|
||||||
|
- etc
|
||||||
7
_client_build.ts
Normal file
7
_client_build.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import * as esbuild from "@char/aftercare/esbuild";
|
||||||
|
|
||||||
|
esbuild.build({
|
||||||
|
in: ["./client/viewer.tsx"],
|
||||||
|
outDir: "./web/dist",
|
||||||
|
watch: Deno.args.includes("--watch"),
|
||||||
|
});
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ensureDir } from "@std/fs";
|
import { ensureDir } from "@std/fs";
|
||||||
import { serveDir } from "@std/http/file-server";
|
import { serveDir, serveFile } from "@std/http/file-server";
|
||||||
|
|
||||||
import * as z from "@zod/mini";
|
import * as z from "@zod/mini";
|
||||||
import { db } from "./db.ts";
|
import { db } from "./db.ts";
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
PlcDidDocumentResolver,
|
PlcDidDocumentResolver,
|
||||||
WebDidDocumentResolver,
|
WebDidDocumentResolver,
|
||||||
} from "@atcute/identity-resolver";
|
} from "@atcute/identity-resolver";
|
||||||
|
import { VIDEO_PATTERN } from "../common/routes.ts";
|
||||||
|
|
||||||
const didResolver = new CompositeDidDocumentResolver({
|
const didResolver = new CompositeDidDocumentResolver({
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -117,6 +118,11 @@ export default {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const videoRoute = VIDEO_PATTERN.exec(req.url);
|
||||||
|
if (videoRoute) {
|
||||||
|
return await serveFile(req, "./web/viewer.html");
|
||||||
|
}
|
||||||
|
|
||||||
return await serveDir(req, { fsRoot: "./web", quiet: true });
|
return await serveDir(req, { fsRoot: "./web", quiet: true });
|
||||||
},
|
},
|
||||||
} satisfies Deno.ServeDefaultExport;
|
} satisfies Deno.ServeDefaultExport;
|
||||||
|
|
|
||||||
72
client/viewer.tsx
Normal file
72
client/viewer.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { resolveDid, resolveHandle } from "../common/identity.ts";
|
||||||
|
import { VIDEO_PATTERN } from "../common/routes.ts";
|
||||||
|
|
||||||
|
type BlobRef = {
|
||||||
|
$type: "blob";
|
||||||
|
ref: { $link: string };
|
||||||
|
mimeType?: string;
|
||||||
|
size?: number;
|
||||||
|
};
|
||||||
|
type VideoRecord = { video: BlobRef; title?: string; description?: string };
|
||||||
|
|
||||||
|
const fetchVideoRecord = async (
|
||||||
|
did: string,
|
||||||
|
rkey: string
|
||||||
|
): Promise<VideoRecord> => {
|
||||||
|
// TODO: we do lots of casting here that we shouldn't need once we have lexicon.ts validations
|
||||||
|
|
||||||
|
const didDoc = await resolveDid(did);
|
||||||
|
const pds = didDoc.service?.find((it) => it.id === "#atproto_pds")
|
||||||
|
?.serviceEndpoint as string | undefined;
|
||||||
|
|
||||||
|
if (!pds) throw new Error("could not resolve pds for requested repo");
|
||||||
|
|
||||||
|
const getRecordURL = new URL("/xrpc/com.atproto.repo.getRecord", pds);
|
||||||
|
getRecordURL.searchParams.set("collection", "blue.cerulea.video.video");
|
||||||
|
getRecordURL.searchParams.set("repo", did);
|
||||||
|
getRecordURL.searchParams.set("rkey", rkey);
|
||||||
|
|
||||||
|
const recordResponse = await fetch(getRecordURL);
|
||||||
|
if (recordResponse.status !== 200)
|
||||||
|
throw new Error("got error fetching record");
|
||||||
|
|
||||||
|
const record = (await recordResponse.json()).value as VideoRecord;
|
||||||
|
return record;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveVideoURL = async (did: string, blob: string): Promise<string> => {
|
||||||
|
const res = await fetch("/xrpc/blue.cerulea.video.fetchVideo", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-encoding": "application/json" },
|
||||||
|
body: JSON.stringify({ repo: did, blob }),
|
||||||
|
}).then((r) => r.json());
|
||||||
|
return `/user-content/${res.filename}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
const player = document.querySelector("#player")! as HTMLElement;
|
||||||
|
|
||||||
|
const location = VIDEO_PATTERN.exec(globalThis.location.href);
|
||||||
|
if (!location) throw new Error("video pattern did not match url");
|
||||||
|
const { repo, rkey } = location.pathname.groups as {
|
||||||
|
repo: string;
|
||||||
|
rkey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const did = repo.startsWith("did:") ? repo : await resolveHandle(repo);
|
||||||
|
|
||||||
|
const video = await fetchVideoRecord(did, rkey);
|
||||||
|
const videoURL = await resolveVideoURL(did, video.video.ref.$link);
|
||||||
|
|
||||||
|
player.append(
|
||||||
|
<video crossOrigin="anonymous" controls>
|
||||||
|
<source src={videoURL} />
|
||||||
|
</video>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (video.title) player.append(<h1>{video.title}</h1>);
|
||||||
|
if (video.description)
|
||||||
|
player.append(<p className="description">{video.description}</p>);
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
26
common/identity.ts
Normal file
26
common/identity.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import type { DidDocument } from "@atcute/identity";
|
||||||
|
import {
|
||||||
|
CompositeDidDocumentResolver,
|
||||||
|
PlcDidDocumentResolver,
|
||||||
|
WebDidDocumentResolver,
|
||||||
|
XrpcHandleResolver,
|
||||||
|
} from "@atcute/identity-resolver";
|
||||||
|
|
||||||
|
const handleResolver = new XrpcHandleResolver({
|
||||||
|
serviceUrl: "https://public.api.bsky.app",
|
||||||
|
});
|
||||||
|
|
||||||
|
const didResolver = new CompositeDidDocumentResolver({
|
||||||
|
methods: {
|
||||||
|
plc: new PlcDidDocumentResolver(),
|
||||||
|
web: new WebDidDocumentResolver(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function resolveHandle(handle: string): Promise<string> {
|
||||||
|
return handleResolver.resolve(handle as `${string}.${string}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDid(did: string): Promise<DidDocument> {
|
||||||
|
return didResolver.resolve(did as `did:${"plc" | "web"}:${string}`);
|
||||||
|
}
|
||||||
3
common/routes.ts
Normal file
3
common/routes.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
if (!globalThis.URLPattern) await import("npm:urlpattern-polyfill@10");
|
||||||
|
|
||||||
|
export const VIDEO_PATTERN = new URLPattern({ pathname: "/:repo/video/:rkey" });
|
||||||
11
deno.json
11
deno.json
|
|
@ -1,13 +1,22 @@
|
||||||
{
|
{
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"appview:start": "deno serve -A --port 4080 ./appview/main.ts"
|
"appview:start": "deno serve -A --port 4080 ./appview/main.ts",
|
||||||
|
"client:build": "deno run -A ./_client_build.ts",
|
||||||
|
"client:watch": "deno run -A ./_client_build.ts --watch"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
|
"@atcute/identity": "npm:@atcute/identity@^1.0.2",
|
||||||
"@atcute/identity-resolver": "npm:@atcute/identity-resolver@^1.1.3",
|
"@atcute/identity-resolver": "npm:@atcute/identity-resolver@^1.1.3",
|
||||||
|
"@char/aftercare": "jsr:@char/aftercare@^0.4.2",
|
||||||
"@db/sqlite": "jsr:@db/sqlite@^0.12.0",
|
"@db/sqlite": "jsr:@db/sqlite@^0.12.0",
|
||||||
"@std/fs": "jsr:@std/fs@^1.0.17",
|
"@std/fs": "jsr:@std/fs@^1.0.17",
|
||||||
"@std/http": "jsr:@std/http@^1.0.17",
|
"@std/http": "jsr:@std/http@^1.0.17",
|
||||||
"@std/path": "jsr:@std/path@^1.1.0",
|
"@std/path": "jsr:@std/path@^1.1.0",
|
||||||
"@zod/mini": "npm:@zod/mini@^4.0.0-beta.20250505T195954"
|
"@zod/mini": "npm:@zod/mini@^4.0.0-beta.20250505T195954"
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["deno.window", "dom"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "@char/aftercare"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
200
deno.lock
200
deno.lock
|
|
@ -1,14 +1,20 @@
|
||||||
{
|
{
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"specifiers": {
|
"specifiers": {
|
||||||
|
"jsr:@char/aftercare@~0.4.2": "0.4.2",
|
||||||
"jsr:@db/sqlite@0.12": "0.12.0",
|
"jsr:@db/sqlite@0.12": "0.12.0",
|
||||||
"jsr:@denosaurs/plug@1": "1.1.0",
|
"jsr:@denosaurs/plug@1": "1.1.0",
|
||||||
|
"jsr:@luca/esbuild-deno-loader@0.11": "0.11.1",
|
||||||
"jsr:@std/assert@0.217": "0.217.0",
|
"jsr:@std/assert@0.217": "0.217.0",
|
||||||
"jsr:@std/assert@0.221": "0.221.0",
|
"jsr:@std/assert@0.221": "0.221.0",
|
||||||
|
"jsr:@std/bytes@^1.0.2": "1.0.5",
|
||||||
|
"jsr:@std/cli@1": "1.0.18",
|
||||||
"jsr:@std/cli@^1.0.18": "1.0.18",
|
"jsr:@std/cli@^1.0.18": "1.0.18",
|
||||||
|
"jsr:@std/dotenv@0.225": "0.225.3",
|
||||||
"jsr:@std/encoding@0.221": "0.221.0",
|
"jsr:@std/encoding@0.221": "0.221.0",
|
||||||
"jsr:@std/encoding@1": "1.0.10",
|
"jsr:@std/encoding@1": "1.0.10",
|
||||||
"jsr:@std/encoding@^1.0.10": "1.0.10",
|
"jsr:@std/encoding@^1.0.10": "1.0.10",
|
||||||
|
"jsr:@std/encoding@^1.0.5": "1.0.10",
|
||||||
"jsr:@std/fmt@0.221": "0.221.0",
|
"jsr:@std/fmt@0.221": "0.221.0",
|
||||||
"jsr:@std/fmt@1": "1.0.8",
|
"jsr:@std/fmt@1": "1.0.8",
|
||||||
"jsr:@std/fmt@^1.0.8": "1.0.8",
|
"jsr:@std/fmt@^1.0.8": "1.0.8",
|
||||||
|
|
@ -22,13 +28,27 @@
|
||||||
"jsr:@std/path@0.217": "0.217.0",
|
"jsr:@std/path@0.217": "0.217.0",
|
||||||
"jsr:@std/path@0.221": "0.221.0",
|
"jsr:@std/path@0.221": "0.221.0",
|
||||||
"jsr:@std/path@1": "1.1.0",
|
"jsr:@std/path@1": "1.1.0",
|
||||||
|
"jsr:@std/path@^1.0.6": "1.1.0",
|
||||||
"jsr:@std/path@^1.0.9": "1.1.0",
|
"jsr:@std/path@^1.0.9": "1.1.0",
|
||||||
"jsr:@std/path@^1.1.0": "1.1.0",
|
"jsr:@std/path@^1.1.0": "1.1.0",
|
||||||
"jsr:@std/streams@^1.0.9": "1.0.9",
|
"jsr:@std/streams@^1.0.9": "1.0.9",
|
||||||
"npm:@atcute/identity-resolver@^1.1.3": "1.1.3_@atcute+identity@1.0.2",
|
"npm:@atcute/identity-resolver@^1.1.3": "1.1.3_@atcute+identity@1.0.2",
|
||||||
"npm:@zod/mini@^4.0.0-beta.20250505T195954": "4.0.0-beta.20250505T195954"
|
"npm:@atcute/identity@^1.0.2": "1.0.2",
|
||||||
|
"npm:@zod/mini@^4.0.0-beta.20250505T195954": "4.0.0-beta.20250505T195954",
|
||||||
|
"npm:esbuild@0.24": "0.24.2",
|
||||||
|
"npm:urlpattern-polyfill@10": "10.0.0"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
|
"@char/aftercare@0.4.2": {
|
||||||
|
"integrity": "54aacf6911a841b5aaf3613e04df780375e7372f1da08a81883274414b8b3e99",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@luca/esbuild-deno-loader",
|
||||||
|
"jsr:@std/cli@1",
|
||||||
|
"jsr:@std/dotenv",
|
||||||
|
"jsr:@std/path@1",
|
||||||
|
"npm:esbuild"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@db/sqlite@0.12.0": {
|
"@db/sqlite@0.12.0": {
|
||||||
"integrity": "dd1ef7f621ad50fc1e073a1c3609c4470bd51edc0994139c5bf9851de7a6d85f",
|
"integrity": "dd1ef7f621ad50fc1e073a1c3609c4470bd51edc0994139c5bf9851de7a6d85f",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -54,15 +74,29 @@
|
||||||
"jsr:@std/path@1"
|
"jsr:@std/path@1"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@luca/esbuild-deno-loader@0.11.1": {
|
||||||
|
"integrity": "dc020d16d75b591f679f6b9288b10f38bdb4f24345edb2f5732affa1d9885267",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/bytes",
|
||||||
|
"jsr:@std/encoding@^1.0.5",
|
||||||
|
"jsr:@std/path@^1.0.6"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@std/assert@0.217.0": {
|
"@std/assert@0.217.0": {
|
||||||
"integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642"
|
"integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642"
|
||||||
},
|
},
|
||||||
"@std/assert@0.221.0": {
|
"@std/assert@0.221.0": {
|
||||||
"integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a"
|
"integrity": "a5f1aa6e7909dbea271754fd4ab3f4e687aeff4873b4cef9a320af813adb489a"
|
||||||
},
|
},
|
||||||
|
"@std/bytes@1.0.5": {
|
||||||
|
"integrity": "4465dd739d7963d964c809202ebea6d5c6b8e3829ef25c6a224290fbb8a1021e"
|
||||||
|
},
|
||||||
"@std/cli@1.0.18": {
|
"@std/cli@1.0.18": {
|
||||||
"integrity": "33846eab6a7cac52156cc105a798451df06965693606e4668adfe0436a155fd7"
|
"integrity": "33846eab6a7cac52156cc105a798451df06965693606e4668adfe0436a155fd7"
|
||||||
},
|
},
|
||||||
|
"@std/dotenv@0.225.3": {
|
||||||
|
"integrity": "a95e5b812c27b0854c52acbae215856d9cce9d4bbf774d938c51d212711e8d4a"
|
||||||
|
},
|
||||||
"@std/encoding@0.221.0": {
|
"@std/encoding@0.221.0": {
|
||||||
"integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45"
|
"integrity": "d1dd76ef0dc5d14088411e6dc1dede53bf8308c95d1537df1214c97137208e45"
|
||||||
},
|
},
|
||||||
|
|
@ -94,7 +128,7 @@
|
||||||
"@std/http@1.0.17": {
|
"@std/http@1.0.17": {
|
||||||
"integrity": "98aec8ab4080d95c21f731e3008f69c29c5012d12f1b4e553f85935db601569f",
|
"integrity": "98aec8ab4080d95c21f731e3008f69c29c5012d12f1b4e553f85935db601569f",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@std/cli",
|
"jsr:@std/cli@^1.0.18",
|
||||||
"jsr:@std/encoding@^1.0.10",
|
"jsr:@std/encoding@^1.0.10",
|
||||||
"jsr:@std/fmt@^1.0.8",
|
"jsr:@std/fmt@^1.0.8",
|
||||||
"jsr:@std/html",
|
"jsr:@std/html",
|
||||||
|
|
@ -161,6 +195,131 @@
|
||||||
"@badrap/valita@0.4.5": {
|
"@badrap/valita@0.4.5": {
|
||||||
"integrity": "sha512-4QwGbuhh/JesHRQj79mO/l37PvJj4l/tlAu7+S1n4h47qwaNpZ0WDvIwUGLYUsdi9uQ5UPpiG9wb1Wm3XUFBUQ=="
|
"integrity": "sha512-4QwGbuhh/JesHRQj79mO/l37PvJj4l/tlAu7+S1n4h47qwaNpZ0WDvIwUGLYUsdi9uQ5UPpiG9wb1Wm3XUFBUQ=="
|
||||||
},
|
},
|
||||||
|
"@esbuild/aix-ppc64@0.24.2": {
|
||||||
|
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
|
||||||
|
"os": ["aix"],
|
||||||
|
"cpu": ["ppc64"]
|
||||||
|
},
|
||||||
|
"@esbuild/android-arm64@0.24.2": {
|
||||||
|
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
|
||||||
|
"os": ["android"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/android-arm@0.24.2": {
|
||||||
|
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
|
||||||
|
"os": ["android"],
|
||||||
|
"cpu": ["arm"]
|
||||||
|
},
|
||||||
|
"@esbuild/android-x64@0.24.2": {
|
||||||
|
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
|
||||||
|
"os": ["android"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/darwin-arm64@0.24.2": {
|
||||||
|
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/darwin-x64@0.24.2": {
|
||||||
|
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
|
||||||
|
"os": ["darwin"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/freebsd-arm64@0.24.2": {
|
||||||
|
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
|
||||||
|
"os": ["freebsd"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/freebsd-x64@0.24.2": {
|
||||||
|
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
|
||||||
|
"os": ["freebsd"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-arm64@0.24.2": {
|
||||||
|
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-arm@0.24.2": {
|
||||||
|
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["arm"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-ia32@0.24.2": {
|
||||||
|
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["ia32"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-loong64@0.24.2": {
|
||||||
|
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["loong64"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-mips64el@0.24.2": {
|
||||||
|
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["mips64el"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-ppc64@0.24.2": {
|
||||||
|
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["ppc64"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-riscv64@0.24.2": {
|
||||||
|
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["riscv64"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-s390x@0.24.2": {
|
||||||
|
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["s390x"]
|
||||||
|
},
|
||||||
|
"@esbuild/linux-x64@0.24.2": {
|
||||||
|
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
|
||||||
|
"os": ["linux"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/netbsd-arm64@0.24.2": {
|
||||||
|
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
|
||||||
|
"os": ["netbsd"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/netbsd-x64@0.24.2": {
|
||||||
|
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
|
||||||
|
"os": ["netbsd"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/openbsd-arm64@0.24.2": {
|
||||||
|
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
|
||||||
|
"os": ["openbsd"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/openbsd-x64@0.24.2": {
|
||||||
|
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
|
||||||
|
"os": ["openbsd"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/sunos-x64@0.24.2": {
|
||||||
|
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
|
||||||
|
"os": ["sunos"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
|
"@esbuild/win32-arm64@0.24.2": {
|
||||||
|
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["arm64"]
|
||||||
|
},
|
||||||
|
"@esbuild/win32-ia32@0.24.2": {
|
||||||
|
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["ia32"]
|
||||||
|
},
|
||||||
|
"@esbuild/win32-x64@0.24.2": {
|
||||||
|
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
|
||||||
|
"os": ["win32"],
|
||||||
|
"cpu": ["x64"]
|
||||||
|
},
|
||||||
"@zod/core@0.11.6": {
|
"@zod/core@0.11.6": {
|
||||||
"integrity": "sha512-03Bv82fFSfjDAvMfdHHdGSS6SOJs0iCcJlWJv1kJHRtoTT02hZpyip/2Lk6oo4l4FtjuwTrsEQTwg/LD8I7dJA=="
|
"integrity": "sha512-03Bv82fFSfjDAvMfdHHdGSS6SOJs0iCcJlWJv1kJHRtoTT02hZpyip/2Lk6oo4l4FtjuwTrsEQTwg/LD8I7dJA=="
|
||||||
},
|
},
|
||||||
|
|
@ -170,17 +329,54 @@
|
||||||
"@zod/core"
|
"@zod/core"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"esbuild@0.24.2": {
|
||||||
|
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
|
||||||
|
"optionalDependencies": [
|
||||||
|
"@esbuild/aix-ppc64",
|
||||||
|
"@esbuild/android-arm",
|
||||||
|
"@esbuild/android-arm64",
|
||||||
|
"@esbuild/android-x64",
|
||||||
|
"@esbuild/darwin-arm64",
|
||||||
|
"@esbuild/darwin-x64",
|
||||||
|
"@esbuild/freebsd-arm64",
|
||||||
|
"@esbuild/freebsd-x64",
|
||||||
|
"@esbuild/linux-arm",
|
||||||
|
"@esbuild/linux-arm64",
|
||||||
|
"@esbuild/linux-ia32",
|
||||||
|
"@esbuild/linux-loong64",
|
||||||
|
"@esbuild/linux-mips64el",
|
||||||
|
"@esbuild/linux-ppc64",
|
||||||
|
"@esbuild/linux-riscv64",
|
||||||
|
"@esbuild/linux-s390x",
|
||||||
|
"@esbuild/linux-x64",
|
||||||
|
"@esbuild/netbsd-arm64",
|
||||||
|
"@esbuild/netbsd-x64",
|
||||||
|
"@esbuild/openbsd-arm64",
|
||||||
|
"@esbuild/openbsd-x64",
|
||||||
|
"@esbuild/sunos-x64",
|
||||||
|
"@esbuild/win32-arm64",
|
||||||
|
"@esbuild/win32-ia32",
|
||||||
|
"@esbuild/win32-x64"
|
||||||
|
],
|
||||||
|
"scripts": true,
|
||||||
|
"bin": true
|
||||||
|
},
|
||||||
"esm-env@1.2.2": {
|
"esm-env@1.2.2": {
|
||||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
|
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
|
||||||
|
},
|
||||||
|
"urlpattern-polyfill@10.0.0": {
|
||||||
|
"integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
"jsr:@char/aftercare@~0.4.2",
|
||||||
"jsr:@db/sqlite@0.12",
|
"jsr:@db/sqlite@0.12",
|
||||||
"jsr:@std/fs@^1.0.17",
|
"jsr:@std/fs@^1.0.17",
|
||||||
"jsr:@std/http@^1.0.17",
|
"jsr:@std/http@^1.0.17",
|
||||||
"jsr:@std/path@^1.1.0",
|
"jsr:@std/path@^1.1.0",
|
||||||
"npm:@atcute/identity-resolver@^1.1.3",
|
"npm:@atcute/identity-resolver@^1.1.3",
|
||||||
|
"npm:@atcute/identity@^1.0.2",
|
||||||
"npm:@zod/mini@^4.0.0-beta.20250505T195954"
|
"npm:@zod/mini@^4.0.0-beta.20250505T195954"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
50
web/css/styles.css
Normal file
50
web/css/styles.css
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
|
||||||
|
--color-bg: 16 16 16;
|
||||||
|
--color-fg: 255 255 255;
|
||||||
|
--color-accent: 233 161 255;
|
||||||
|
|
||||||
|
--color-text: var(--color-fg);
|
||||||
|
--alpha-text: 0.9;
|
||||||
|
|
||||||
|
--font-sans:
|
||||||
|
Inter, system-ui, -apple-system, "Segoe UI", "Helvetica Neue", sans-serif;
|
||||||
|
|
||||||
|
background-color: rgb(var(--color-bg) / 1);
|
||||||
|
color: rgb(var(--color-fg) / var(--alpha-text));
|
||||||
|
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 1.125rem;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(var(--color-accent) / var(--alpha-text));
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid rgb(var(--color-accent) / var(--alpha-text));
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(h1, h2, h3) {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding-top: 2em;
|
||||||
|
max-width: 120ch;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#player {
|
||||||
|
video {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
web/dist/.gitignore
vendored
Normal file
2
web/dist/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!/.gitignore
|
||||||
|
|
@ -2,9 +2,17 @@
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>video.cerulea.blue</title>
|
<title>video.cerulea.blue</title>
|
||||||
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
|
|
||||||
<style>
|
<main>
|
||||||
:root {
|
<header>
|
||||||
color-scheme: dark;
|
<h1>video.cerulea.blue</h1>
|
||||||
}
|
<p>
|
||||||
</style>
|
super simple direct-link video on the
|
||||||
|
<a href="https://atproto.com/">AT Protocol</a>.
|
||||||
|
</p>
|
||||||
|
<p>to view a video, head to <code>/:repo/video/:rkey</code>.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="app"></div>
|
||||||
|
</main>
|
||||||
|
|
|
||||||
11
web/viewer.html
Normal file
11
web/viewer.html
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>video.cerulea.blue</title>
|
||||||
|
<link rel="stylesheet" href="/css/styles.css" />
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div id="player"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="/dist/viewer.js" type="module"></script>
|
||||||
Loading…
Reference in a new issue