You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
266 lines
7.0 KiB
266 lines
7.0 KiB
import { |
|
FullScraperEvents, |
|
RunOutput, |
|
ScrapeMedia, |
|
} from "@movie-web/providers"; |
|
import { RefObject, useCallback, useEffect, useRef, useState } from "react"; |
|
|
|
import { |
|
connectServerSideEvents, |
|
getCachedMetadata, |
|
makeProviderUrl, |
|
} from "@/backend/helpers/providerApi"; |
|
import { getLoadbalancedProviderApiUrl, providers } from "@/utils/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?: unknown; |
|
percentage: number; |
|
} |
|
|
|
type ScraperEvent<Event extends keyof FullScraperEvents> = Parameters< |
|
NonNullable<FullScraperEvents[Event]> |
|
>[0]; |
|
|
|
function useBaseScrape() { |
|
const [sources, setSources] = useState<Record<string, ScrapingSegment>>({}); |
|
const [sourceOrder, setSourceOrder] = useState<ScrapingItems[]>([]); |
|
const [currentSource, setCurrentSource] = useState<string>(); |
|
const lastId = useRef<string | null>(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<Record<string, ScrapingSegment>>((a, v) => { |
|
a[v.id] = v; |
|
return a; |
|
}, {}), |
|
); |
|
setSourceOrder(evt.sourceIds.map((v) => ({ id: v, children: [] }))); |
|
}, []); |
|
|
|
const startEvent = useCallback((id: ScraperEvent<"start">) => { |
|
setSources((s) => { |
|
if (s[id]) s[id].status = "pending"; |
|
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) { |
|
startScrape(); |
|
const baseUrlMaker = makeProviderUrl(providerApiUrl); |
|
const conn = await connectServerSideEvents<RunOutput | "">( |
|
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(); |
|
|
|
return getResult(sseOutput === "" ? null : sseOutput); |
|
} |
|
|
|
if (!providers) return null; |
|
startScrape(); |
|
const output = await providers.runAll({ |
|
media, |
|
events: { |
|
init: initEvent, |
|
start: startEvent, |
|
update: updateEvent, |
|
discoverEmbeds: discoverEmbedsEvent, |
|
}, |
|
}); |
|
return getResult(output); |
|
}, |
|
[ |
|
initEvent, |
|
startEvent, |
|
updateEvent, |
|
discoverEmbedsEvent, |
|
getResult, |
|
startScrape, |
|
], |
|
); |
|
|
|
return { |
|
startScraping, |
|
sourceOrder, |
|
sources, |
|
currentSource, |
|
}; |
|
} |
|
|
|
export function useListCenter( |
|
containerRef: RefObject<HTMLDivElement | null>, |
|
listRef: RefObject<HTMLDivElement | null>, |
|
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; |
|
}
|
|
|