import { useMemo, useRef, useState } from "react"; import { Icon, Icons } from "@/components/Icon"; import { useLoading } from "@/hooks/useLoading"; import { Loading } from "@/components/layout/Loading"; import { IconPatch } from "@/components/buttons/IconPatch"; import { useVideoPlayerDescriptor } from "@/video/state/hooks"; import { useMeta } from "@/video/state/logic/meta"; import { useControls } from "@/video/state/logic/controls"; import { MWStream } from "@/backend/helpers/streams"; import { getEmbedScraperByType, getProviders, } from "@/backend/helpers/register"; import { runEmbedScraper, runProvider } from "@/backend/helpers/run"; import { MWProviderScrapeResult } from "@/backend/helpers/provider"; import { useTranslation } from "react-i18next"; import { MWEmbed, MWEmbedType } from "@/backend/helpers/embed"; import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; interface EmbedEntryProps { name: string; type: MWEmbedType; url: string; onSelect: (stream: MWStream) => void; } export function EmbedEntry(props: EmbedEntryProps) { const [scrapeEmbed, loading, error] = useLoading(async () => { const scraper = getEmbedScraperByType(props.type); if (!scraper) throw new Error("Embed scraper not found"); const stream = await runEmbedScraper(scraper, { progress: () => {}, // no progress tracking for inline scraping url: props.url, }); props.onSelect(stream); }); return ( { scrapeEmbed(); }} > {props.name} ); } export function SourceSelectionPopout() { const { t } = useTranslation(); const descriptor = useVideoPlayerDescriptor(); const controls = useControls(descriptor); const meta = useMeta(descriptor); const providers = useMemo( () => meta ? getProviders().filter((v) => v.type.includes(meta.meta.meta.type)) : [], [meta] ); const [selectedProvider, setSelectedProvider] = useState(null); const [scrapeResult, setScrapeResult] = useState(null); const showingProvider = !!selectedProvider; const selectedProviderPopulated = useMemo( () => providers.find((v) => v.id === selectedProvider) ?? null, [providers, selectedProvider] ); const [runScraper, loading, error] = useLoading( async (providerId: string) => { const theProvider = providers.find((v) => v.id === providerId); if (!theProvider) throw new Error("Invalid provider"); if (!meta) throw new Error("need meta"); return runProvider(theProvider, { media: meta.meta, progress: () => {}, type: meta.meta.meta.type, episode: meta.episode?.episodeId as any, season: meta.episode?.seasonId as any, }); } ); function selectSource(stream: MWStream) { controls.setSource({ quality: stream.quality, source: stream.streamUrl, type: stream.type, }); if (meta) { controls.setMeta({ ...meta, captions: stream.captions, }); } controls.closePopout(); } const providerRef = useRef(null); const selectProvider = (providerId?: string) => { if (!providerId) { providerRef.current = null; setSelectedProvider(null); return; } runScraper(providerId).then(async (v) => { if (!providerRef.current) return; if (v) { const len = v.embeds.length + (v.stream ? 1 : 0); if (len === 1) { const realStream = v.stream; if (!realStream) { const embed = v?.embeds[0]; if (!embed) throw new Error("Embed scraper not found"); const scraper = getEmbedScraperByType(embed.type); if (!scraper) throw new Error("Embed scraper not found"); const stream = await runEmbedScraper(scraper, { progress: () => {}, // no progress tracking for inline scraping url: embed.url, }); selectSource(stream); return; } selectSource(realStream); return; } } setScrapeResult(v ?? null); }); providerRef.current = providerId; setSelectedProvider(providerId); }; const titlePositionClass = useMemo(() => { const offset = !showingProvider ? "left-0" : "left-10"; return [ "absolute w-full transition-[left,opacity] duration-200", offset, ].join(" "); }, [showingProvider]); const visibleEmbeds = useMemo(() => { const embeds = scrapeResult?.embeds || []; // Count embed types to determine if it should show a number behind the name const embedsPerType: Record = {}; for (const embed of embeds) { if (!embed.type) continue; if (!embedsPerType[embed.type]) embedsPerType[embed.type] = []; embedsPerType[embed.type].push({ ...embed, displayName: embed.type, }); } const embedsRes = Object.entries(embedsPerType).flatMap(([_, entries]) => { if (entries.length > 1) return entries.map((embed, i) => ({ ...embed, displayName: `${embed.type} ${i + 1}`, })); return entries; }); return embedsRes; }, [scrapeResult?.embeds]); return ( <>
{selectedProviderPopulated?.displayName ?? ""} {t("videoPlayer.popouts.sources")}
{loading ? (
) : error ? (

{t("videoPlayer.popouts.errors.embedsError")}

) : ( <> {scrapeResult?.stream ? ( { if (scrapeResult.stream) selectSource(scrapeResult.stream); }} > Native source ) : null} {(visibleEmbeds?.length || 0) > 0 ? ( visibleEmbeds?.map((v) => ( { selectSource(stream); }} /> )) ) : (

{t("videoPlayer.popouts.noEmbeds")}

)} )}
{providers.map((v) => ( { selectProvider(v.id); }} > {v.displayName} ))}
); }