Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit fb6dc5d7726a72a65ef466e132ec756c7b7de8cc
parent d5cb49794a3ebc589dbcc2bc70409bc0d0e9a7f3
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 22 Apr 2026 20:40:55 -0400

Add odysee player and p-limit for scripts

Diffstat:
Aapp/OdyseePlayer.tsx | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mapp/PlayerProvider.tsx | 30++++++++++++++++++++++++------
Mlib/transcripts-server.ts | 10++++++++--
Mlib/transcripts.ts | 2+-
Mpackage.json | 1+
Mpnpm-lock.yaml | 17+++++++++++++++++
Mscripts/build-index.ts | 2+-
7 files changed, 99 insertions(+), 10 deletions(-)

diff --git a/app/OdyseePlayer.tsx b/app/OdyseePlayer.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { forwardRef, useImperativeHandle, useState } from "react"; + +export type OdyseePlayerHandle = { + seekTo: (seconds: number, unit?: "seconds") => void; +}; + +type Props = { + videoId: string; + startSeconds?: number; +}; + +// Odysee's iframe embed — reloading with a new `t` query param is our only +// way to seek from the outside (no JS API, no currentTime tracking). +const OdyseePlayer = forwardRef<OdyseePlayerHandle, Props>(function OdyseePlayer( + { videoId, startSeconds = 0 }, + ref, +) { + const [start, setStart] = useState(() => + Math.max(0, Math.floor(startSeconds)), + ); + + useImperativeHandle( + ref, + () => ({ + seekTo(seconds: number) { + setStart(Math.max(0, Math.floor(seconds))); + }, + }), + [], + ); + + const src = `https://odysee.com/%24/embed/${encodeURIComponent(videoId)}?t=${start}`; + + return ( + <iframe + key={start} + src={src} + allow="autoplay; fullscreen; encrypted-media; picture-in-picture" + allowFullScreen + style={{ border: 0, width: "100%", height: "100%" }} + /> + ); +}); + +export default OdyseePlayer; diff --git a/app/PlayerProvider.tsx b/app/PlayerProvider.tsx @@ -16,6 +16,7 @@ import { useUrlParams, writeUrlParams } from "./urlState"; import { formatDate, formatDuration } from "@/lib/format"; import type { Platform } from "@/lib/transcripts"; import type { RumblePlayerHandle } from "./RumblePlayer"; +import type { OdyseePlayerHandle } from "./OdyseePlayer"; const ReactPlayer = dynamic(() => import("react-player/youtube"), { ssr: false, @@ -25,7 +26,14 @@ const RumblePlayer = dynamic(() => import("./RumblePlayer"), { ssr: false, }); -type PlayerHandle = Pick<ReactPlayerType, "seekTo"> | RumblePlayerHandle; +const OdyseePlayer = dynamic(() => import("./OdyseePlayer"), { + ssr: false, +}); + +type PlayerHandle = + | Pick<ReactPlayerType, "seekTo"> + | RumblePlayerHandle + | OdyseePlayerHandle; export type Cue = { start: number; end: number; text: string }; @@ -228,7 +236,7 @@ export function PlayerProvider({ const copyShareUrl = useCallback(async (): Promise<boolean> => { if (!data) return false; - const t = data.platform === "rumble" ? (urlTime ?? 0) : currentTime; + const t = data.platform === "youtube" ? currentTime : (urlTime ?? 0); const secs = Math.max(0, Math.floor(t)); const params = new URLSearchParams(); params.set("v", data.slug); @@ -302,11 +310,12 @@ export function PlayerProvider({ setCurrentTime(urlTime); }, [urlSlug, urlTime]); - // Rumble has no onReady or progress events; seed currentTime from the URL so - // clip-mark buttons and active-cue highlighting have a baseline, and mark - // the slug ready so later ?t= changes can seek via the imperative handle. + // Rumble / Odysee have no onReady or progress events; seed currentTime from + // the URL so clip-mark buttons and active-cue highlighting have a baseline, + // and mark the slug ready so later ?t= changes can seek via the imperative + // handle. useEffect(() => { - if (detail?.platform === "rumble") { + if (detail?.platform === "rumble" || detail?.platform === "odysee") { readyForSlugRef.current = detail.slug; pendingSeekRef.current = null; setCurrentTime(urlTime ?? 0); @@ -390,6 +399,15 @@ export function PlayerProvider({ videoId={data.id} startSeconds={urlTime ?? 0} /> + ) : data.platform === "odysee" ? ( + <OdyseePlayer + key={data.id} + ref={(p: OdyseePlayerHandle | null) => { + playerRef.current = p; + }} + videoId={data.id} + startSeconds={urlTime ?? 0} + /> ) : ( <ReactPlayer ref={(p: ReactPlayerType | null) => { diff --git a/lib/transcripts-server.ts b/lib/transcripts-server.ts @@ -16,16 +16,19 @@ export type RawMetadata = { extractor?: string; extractor_key?: string; webpage_url?: string; + webpage_url_basename?: string; }; function detectPlatform(meta: RawMetadata): Platform { const key = meta.extractor_key ?? meta.extractor ?? ""; if (/^rumble/i.test(key)) return "rumble"; + if (/^lbry/i.test(key)) return "odysee"; return "youtube"; } function defaultWebpageUrl(platform: Platform, id: string): string { if (platform === "rumble") return `https://rumble.com/${id}`; + if (platform === "odysee") return `https://odysee.com/${id}`; return `https://www.youtube.com/watch?v=${id}`; } @@ -42,9 +45,12 @@ export function summarize( liveStatus === "was_live" || liveStatus === "is_live" || liveStatus === "is_upcoming"; - const id = meta.id ?? videoDir; - const dateFromDir = videoDir.match(/^(\d{8})(?:_|$)/)?.[1]; const platform = detectPlatform(meta); + const id = + platform === "odysee" + ? (meta.webpage_url_basename ?? meta.id ?? videoDir) + : (meta.id ?? videoDir); + const dateFromDir = videoDir.match(/^(\d{8})(?:_|$)/)?.[1]; return { slug: `${channelSlug}/${id}`, id, diff --git a/lib/transcripts.ts b/lib/transcripts.ts @@ -9,7 +9,7 @@ const MANIFEST_PATH = path.join( "manifest.json", ); -export type Platform = "youtube" | "rumble"; +export type Platform = "youtube" | "rumble" | "odysee"; export type TranscriptSummary = { slug: string; diff --git a/package.json b/package.json @@ -17,6 +17,7 @@ "execa": "^9.6.1", "lmdb": "^3.5.4", "next": "16.2.3", + "p-limit": "^7.3.0", "react": "19.2.4", "react-dom": "19.2.4", "react-player": "^2.16.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: next: specifier: 16.2.3 version: 16.2.3(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + p-limit: + specifier: ^7.3.0 + version: 7.3.0 react: specifier: 19.2.4 version: 19.2.4 @@ -2146,6 +2149,10 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} + p-limit@7.3.0: + resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} + engines: {node: '>=20'} + p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -2604,6 +2611,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} @@ -4692,6 +4703,10 @@ snapshots: dependencies: yocto-queue: 0.1.0 + p-limit@7.3.0: + dependencies: + yocto-queue: 1.2.2 + p-locate@5.0.0: dependencies: p-limit: 3.1.0 @@ -5278,6 +5293,8 @@ snapshots: yocto-queue@0.1.0: {} + yocto-queue@1.2.2: {} + yoctocolors@2.1.2: {} zod-validation-error@4.0.2(zod@4.3.6): diff --git a/scripts/build-index.ts b/scripts/build-index.ts @@ -57,7 +57,7 @@ const SUMMARIES_DIR = path.join(PUBLIC_DIR, "summaries"); const TRANSCRIPTS_DIR = path.join(PUBLIC_DIR, "transcripts"); const MANIFEST_PATH = path.join(SUMMARIES_DIR, "manifest.json"); -const SCHEMA_VERSION = 4; +const SCHEMA_VERSION = 5; type Handling = "youtube" | "transcribe"; type ChannelConfig = { handling: Handling; name?: string };