/** * Tube module — community video hosting & live streaming. * * Video library (Cloudflare R2 bucket), HLS live streaming, * and RTMP ingest support. No database — metadata from S3 listing. */ import { Hono } from "hono"; import { renderShell } from "../../server/shell"; import { getModuleInfoList } from "../../shared/module"; import type { RSpaceModule } from "../../shared/module"; const routes = new Hono(); // ── R2 / S3 config ── const R2_ENDPOINT = process.env.R2_ENDPOINT || ""; const R2_BUCKET = process.env.R2_BUCKET || "rtube-videos"; const R2_ACCESS_KEY = process.env.R2_ACCESS_KEY || ""; const R2_SECRET_KEY = process.env.R2_SECRET_KEY || ""; const R2_PUBLIC_URL = process.env.R2_PUBLIC_URL || ""; const VIDEO_EXTENSIONS = new Set([ ".mp4", ".mkv", ".webm", ".mov", ".avi", ".wmv", ".flv", ".m4v", ]); // ── S3-compatible listing (lightweight, no AWS SDK needed) ── async function listVideos(): Promise> { if (!R2_ENDPOINT) { // Return demo data when R2 not configured return [ { name: "welcome-to-rtube.mp4", size: 12_500_000 }, { name: "community-meeting-2026.mp4", size: 85_000_000 }, { name: "workshop-recording.webm", size: 45_000_000 }, ]; } try { // Use S3 ListObjectsV2 via signed request const url = `${R2_ENDPOINT}/${R2_BUCKET}?list-type=2&max-keys=200`; const resp = await fetch(url, { headers: { Authorization: `AWS4-HMAC-SHA256 ...`, // Simplified — real impl uses aws4 signing }, }); if (!resp.ok) return []; const text = await resp.text(); // Parse simple XML response const items: Array<{ name: string; size: number }> = []; const keyMatches = text.matchAll(/([^<]+)<\/Key>/g); const sizeMatches = text.matchAll(/(\d+)<\/Size>/g); const keys = [...keyMatches].map((m) => m[1]); const sizes = [...sizeMatches].map((m) => parseInt(m[1])); for (let i = 0; i < keys.length; i++) { const ext = keys[i].substring(keys[i].lastIndexOf(".")).toLowerCase(); if (VIDEO_EXTENSIONS.has(ext)) { items.push({ name: keys[i], size: sizes[i] || 0 }); } } return items.sort((a, b) => a.name.localeCompare(b.name)); } catch { return []; } } // ── API routes ── // GET /api/videos — list videos from bucket routes.get("/api/videos", async (c) => { const videos = await listVideos(); return c.json({ videos }); }); // GET /api/info — module info routes.get("/api/info", (c) => { return c.json({ module: "tube", name: "rTube", r2Configured: !!R2_ENDPOINT, rtmpIngest: "rtmp://rtube.online:1936/live", features: ["video-library", "hls-streaming", "rtmp-ingest"], }); }); // GET /api/health routes.get("/api/health", (c) => c.json({ ok: true })); // ── Page route ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; return c.html(renderShell({ title: `${space} — Tube | rSpace`, moduleId: "tube", spaceSlug: space, modules: getModuleInfoList(), theme: "light", styles: ``, body: ``, scripts: ``, })); }); export const tubeModule: RSpaceModule = { id: "tube", name: "rTube", icon: "\u{1F3AC}", description: "Community video hosting & live streaming", routes, standaloneDomain: "rtube.online", };