"use client"; // Client fetch for a site's shipped /duplicates.json, indexed by member slug so // a search result can answer "does this video exist elsewhere?" in O(1). Mirrors // aliasesCache / summariesCache: one root-level JSON, fetched once, cached // forever (it is static per export build). // // A missing file resolves to an empty map rather than an error. That is the // common case, not an edge one — compose-site writes the file only when the site // has at least one shippable cluster, and every duplicate affordance here is // purely additive, so its absence must never break search. // // WHAT SHIPS HERE IS ALREADY FILTERED. compose-site drops `needsReview` clusters // unless a human confirmed them, so anything in this map has had its CONTENT // compared, not just its title and runtime. The one thing still worth checking // per member is `aligned` — see duplicateSiblings below. import { useQuery } from "@tanstack/react-query"; import { readerFor } from "../lib/archive/readers"; import type { DuplicateCluster, DuplicateReport, DuplicateVideoRef, } from "../lib/duplicates"; export type DuplicateLookup = ReadonlyMap; const EMPTY: DuplicateLookup = new Map(); // The raw shipped report, or null when the site ships none (the common case — // compose-site writes the file only for a site with at least one publishable // cluster). Absent, unreachable and unparseable all fold to null: every // duplicate affordance is additive, so none of them may break a page. export async function fetchDuplicateReport( origin = "", ): Promise { try { return await readerFor(origin).readDuplicates(); } catch { return null; } } export async function fetchDuplicates(origin = ""): Promise { const report = await fetchDuplicateReport(origin); if (!report) return EMPTY; const out = new Map(); for (const cluster of report?.clusters ?? []) { for (const ref of cluster?.videoRefs ?? []) { if (ref?.slug) out.set(ref.slug, cluster); } } return out; } export function useDuplicates(origin = ""): DuplicateLookup { const { data } = useQuery({ queryKey: ["duplicates", origin], queryFn: () => fetchDuplicates(origin), staleTime: Infinity, // static per export build }); return data ?? EMPTY; } // The other members of `slug`'s cluster, or [] when it is in none. export function duplicateSiblings( lookup: DuplicateLookup, slug: string, ): DuplicateVideoRef[] { const cluster = lookup.get(slug); if (!cluster) return []; return cluster.videoRefs.filter((r) => r.slug !== slug); } // Where to land when opening a sibling, given where the viewer is in THIS copy. // // The honesty rule. Matching content does NOT imply matching timings: a mirror // with a longer intro or an extra ad break carries the same words at shifted // times, so seeking to the same timestamp lands in the wrong place while looking // perfectly plausible. `aligned` is the detector's measured verdict on exactly // that, and it is only ever true when several anchors were located within the // tolerance. Absent (an older report, or no timed cues to measure) reads as NOT // aligned, so the fallback is the start of the video — a jump the viewer can // always make sense of. export function duplicateJumpSeconds( ref: DuplicateVideoRef, seconds: number | undefined, ): number { if (ref.aligned !== true) return 0; return typeof seconds === "number" && seconds > 0 ? seconds : 0; }