import { FullScraperEvents, RunOutput, ScrapeMedia, } from "@movie-web/providers"; import { RefObject, useCallback, useEffect, useRef, useState } from "react"; import { isExtensionActiveCached } from "@/backend/extension/messaging"; import { prepareStream } from "@/backend/extension/streams"; import { connectServerSideEvents, getCachedMetadata, makeProviderUrl, } from "@/backend/helpers/providerApi"; import { getLoadbalancedProviderApiUrl } from "@/backend/providers/fetchers"; import { getProviders } from "@/backend/providers/providers"; export interface ScrapingItems { id: string; children: string[]; } export interface ScrapingSegment { name: string; id: string; embedId?: string; status: "failure" | "pending" | "notfound" | "success" | "waiting"; reason?: string; error?: any; percentage: number; } type ScraperEvent = Parameters< NonNullable >[0]; function useBaseScrape() { const [sources, setSources] = useState>({}); const [sourceOrder, setSourceOrder] = useState([]); const [currentSource, setCurrentSource] = useState(); const lastId = useRef(null); const initEvent = useCallback((evt: ScraperEvent<"init">) => { setSources( evt.sourceIds .map((v) => { const source = getCachedMetadata().find((s) => s.id === v); if (!source) throw new Error("invalid source id"); const out: ScrapingSegment = { name: source.name, id: source.id, status: "waiting", percentage: 0, }; return out; }) .reduce>((a, v) => { a[v.id] = v; return a; }, {}), ); setSourceOrder(evt.sourceIds.map((v) => ({ id: v, children: [] }))); }, []); const startEvent = useCallback((id: ScraperEvent<"start">) => { const lastIdTmp = lastId.current; setSources((s) => { if (s[id]) s[id].status = "pending"; if (lastIdTmp && s[lastIdTmp] && s[lastIdTmp].status === "pending") s[lastIdTmp].status = "success"; return { ...s }; }); setCurrentSource(id); lastId.current = id; }, []); const updateEvent = useCallback((evt: ScraperEvent<"update">) => { setSources((s) => { if (s[evt.id]) { s[evt.id].status = evt.status; s[evt.id].reason = evt.reason; s[evt.id].error = evt.error; s[evt.id].percentage = evt.percentage; } return { ...s }; }); }, []); const discoverEmbedsEvent = useCallback( (evt: ScraperEvent<"discoverEmbeds">) => { setSources((s) => { evt.embeds.forEach((v) => { const source = getCachedMetadata().find( (src) => src.id === v.embedScraperId, ); if (!source) throw new Error("invalid source id"); const out: ScrapingSegment = { embedId: v.embedScraperId, name: source.name, id: v.id, status: "waiting", percentage: 0, }; s[v.id] = out; }); return { ...s }; }); setSourceOrder((s) => { const source = s.find((v) => v.id === evt.sourceId); if (!source) throw new Error("invalid source id"); source.children = evt.embeds.map((v) => v.id); return [...s]; }); }, [], ); const startScrape = useCallback(() => { lastId.current = null; }, []); const getResult = useCallback((output: RunOutput | null) => { if (output && lastId.current) { setSources((s) => { if (!lastId.current) return s; if (s[lastId.current]) s[lastId.current].status = "success"; return { ...s }; }); } return output; }, []); return { initEvent, startEvent, updateEvent, discoverEmbedsEvent, startScrape, getResult, sources, sourceOrder, currentSource, }; } export function useScrape() { const { sources, sourceOrder, currentSource, updateEvent, discoverEmbedsEvent, initEvent, getResult, startEvent, startScrape, } = useBaseScrape(); const startScraping = useCallback( async (media: ScrapeMedia) => { const providerApiUrl = getLoadbalancedProviderApiUrl(); if (providerApiUrl && !isExtensionActiveCached()) { startScrape(); const baseUrlMaker = makeProviderUrl(providerApiUrl); const conn = await connectServerSideEvents( baseUrlMaker.scrapeAll(media), ["completed", "noOutput"], ); conn.on("init", initEvent); conn.on("start", startEvent); conn.on("update", updateEvent); conn.on("discoverEmbeds", discoverEmbedsEvent); const sseOutput = await conn.promise(); if (sseOutput && isExtensionActiveCached()) await prepareStream(sseOutput.stream); return getResult(sseOutput === "" ? null : sseOutput); } startScrape(); const providers = getProviders(); const output = await providers.runAll({ media, events: { init: initEvent, start: startEvent, update: updateEvent, discoverEmbeds: discoverEmbedsEvent, }, }); if (output && isExtensionActiveCached()) await prepareStream(output.stream); return getResult(output); }, [ initEvent, startEvent, updateEvent, discoverEmbedsEvent, getResult, startScrape, ], ); return { startScraping, sourceOrder, sources, currentSource, }; } export function useListCenter( containerRef: RefObject, listRef: RefObject, sourceOrder: ScrapingItems[], currentSource: string | undefined, ) { const [renderedOnce, setRenderedOnce] = useState(false); const updatePosition = useCallback(() => { if (!containerRef.current) return; if (!listRef.current) return; const elements = [ ...listRef.current.querySelectorAll("div[data-source-id]"), ] as HTMLDivElement[]; const currentIndex = elements.findIndex( (e) => e.getAttribute("data-source-id") === currentSource, ); const currentElement = elements[currentIndex]; if (!currentElement) return; const containerWidth = containerRef.current.getBoundingClientRect().width; const listWidth = listRef.current.getBoundingClientRect().width; const containerHeight = containerRef.current.getBoundingClientRect().height; const listTop = listRef.current.getBoundingClientRect().top; const currentTop = currentElement.getBoundingClientRect().top; const currentHeight = currentElement.getBoundingClientRect().height; const topDifference = currentTop - listTop; const listNewLeft = containerWidth / 2 - listWidth / 2; const listNewTop = containerHeight / 2 - topDifference - currentHeight / 2; listRef.current.style.transform = `translateY(${listNewTop}px) translateX(${listNewLeft}px)`; setTimeout(() => { setRenderedOnce(true); }, 150); }, [currentSource, containerRef, listRef, setRenderedOnce]); const updatePositionRef = useRef(updatePosition); useEffect(() => { updatePosition(); updatePositionRef.current = updatePosition; }, [updatePosition, sourceOrder]); useEffect(() => { function resize() { updatePositionRef.current(); } window.addEventListener("resize", resize); return () => { window.removeEventListener("resize", resize); }; }, []); return renderedOnce; }