diff --git a/src/backend/helpers/providerApi.ts b/src/backend/helpers/providerApi.ts index 4292ee7d..c6898d2c 100644 --- a/src/backend/helpers/providerApi.ts +++ b/src/backend/helpers/providerApi.ts @@ -1,4 +1,22 @@ -import { ScrapeMedia } from "@movie-web/providers"; +import { MetaOutput, ScrapeMedia } from "@movie-web/providers"; + +import { mwFetch } from "@/backend/helpers/fetch"; + +let metaDataCache: MetaOutput[] | null = null; + +export function setCachedMetadata(data: MetaOutput[]) { + metaDataCache = data; +} + +export function getCachedMetadata(): MetaOutput[] { + return metaDataCache ?? []; +} + +export async function fetchMetadata(base: string) { + if (metaDataCache) return; + const data = await mwFetch(`${base}/metadata`); + metaDataCache = data.flat(); +} function scrapeMediaToQueryMedia(media: ScrapeMedia) { let extra: Record = {}; @@ -15,6 +33,7 @@ function scrapeMediaToQueryMedia(media: ScrapeMedia) { type: media.type, releaseYear: media.releaseYear.toString(), imdbId: media.imdbId, + tmdbId: media.tmdbId, title: media.title, ...extra, }; @@ -48,8 +67,31 @@ export function makeProviderUrl(base: string) { }; } -export function connectServerSideEvents(url: string, endEvents: string[]) { - const; +export function connectServerSideEvents(url: string, endEvents: string[]) { + const eventSource = new EventSource(url); + let promReject: (reason?: any) => void; + let promResolve: (value: T) => void; + const promise = new Promise((resolve, reject) => { + promResolve = resolve; + promReject = reject; + }); - return {}; + endEvents.forEach((evt) => { + eventSource.addEventListener(evt, (e) => { + eventSource.close(); + promResolve(JSON.parse(e.data)); + }); + }); + + eventSource.addEventListener("error", (err) => { + console.error("Failed to connect to SSE", err); + promReject(err); + }); + + return { + promise: () => promise, + on(event: string, cb: (data: Data) => void) { + eventSource.addEventListener(event, (e) => cb(JSON.parse(e.data))); + }, + }; } diff --git a/src/components/player/atoms/settings/SettingsMenu.tsx b/src/components/player/atoms/settings/SettingsMenu.tsx index 4ab971ff..0691156c 100644 --- a/src/components/player/atoms/settings/SettingsMenu.tsx +++ b/src/components/player/atoms/settings/SettingsMenu.tsx @@ -1,6 +1,7 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { getCachedMetadata } from "@/backend/helpers/providerApi"; import { Toggle } from "@/components/buttons/Toggle"; import { Icon, Icons } from "@/components/Icon"; import { useCaptions } from "@/components/player/hooks/useCaptions"; @@ -10,7 +11,6 @@ import { useOverlayRouter } from "@/hooks/useOverlayRouter"; import { usePlayerStore } from "@/stores/player/store"; import { qualityToString } from "@/stores/player/utils/qualities"; import { useSubtitleStore } from "@/stores/subtitles"; -import { providers } from "@/utils/providers"; export function SettingsMenu({ id }: { id: string }) { const { t } = useTranslation(); @@ -23,7 +23,10 @@ export function SettingsMenu({ id }: { id: string }) { const currentSourceId = usePlayerStore((s) => s.sourceId); const sourceName = useMemo(() => { if (!currentSourceId) return "..."; - return providers.getMetadata(currentSourceId)?.name ?? "..."; + const source = getCachedMetadata().find( + (src) => src.id === currentSourceId + ); + return source?.name ?? "..."; }, [currentSourceId]); const { toggleLastUsed } = useCaptions(); diff --git a/src/components/player/atoms/settings/SourceSelectingView.tsx b/src/components/player/atoms/settings/SourceSelectingView.tsx index 83c08914..c3937791 100644 --- a/src/components/player/atoms/settings/SourceSelectingView.tsx +++ b/src/components/player/atoms/settings/SourceSelectingView.tsx @@ -1,6 +1,7 @@ import { ReactNode, useEffect, useMemo, useRef } from "react"; import { useTranslation } from "react-i18next"; +import { getCachedMetadata } from "@/backend/helpers/providerApi"; import { Loading } from "@/components/layout/Loading"; import { useEmbedScraping, @@ -33,7 +34,7 @@ export function EmbedOption(props: { const embedName = useMemo(() => { if (!props.embedId) return unknownEmbedName; - const sourceMeta = providers.getMetadata(props.embedId); + const sourceMeta = getCachedMetadata().find((s) => s.id === props.embedId); return sourceMeta?.name ?? unknownEmbedName; }, [props.embedId, unknownEmbedName]); @@ -61,7 +62,7 @@ export function EmbedSelectionView({ sourceId, id }: EmbedSelectionViewProps) { const sourceName = useMemo(() => { if (!sourceId) return "..."; - const sourceMeta = providers.getMetadata(sourceId); + const sourceMeta = getCachedMetadata().find((s) => s.id === sourceId); return sourceMeta?.name ?? "..."; }, [sourceId]); diff --git a/src/hooks/useProviderScrape.tsx b/src/hooks/useProviderScrape.tsx index 4c16d6a8..c00d6d60 100644 --- a/src/hooks/useProviderScrape.tsx +++ b/src/hooks/useProviderScrape.tsx @@ -5,7 +5,11 @@ import { } from "@movie-web/providers"; import { RefObject, useCallback, useEffect, useRef, useState } from "react"; -import { makeProviderUrl } from "@/backend/helpers/providerApi"; +import { + connectServerSideEvents, + getCachedMetadata, + makeProviderUrl, +} from "@/backend/helpers/providerApi"; import { getLoadbalancedProviderApiUrl, providers } from "@/utils/providers"; export interface ScrapingItems { @@ -37,7 +41,7 @@ function useBaseScrape() { setSources( evt.sourceIds .map((v) => { - const source = providers.getMetadata(v); + const source = getCachedMetadata().find((s) => s.id === v); if (!source) throw new Error("invalid source id"); const out: ScrapingSegment = { name: source.name, @@ -80,7 +84,9 @@ function useBaseScrape() { (evt: ScraperEvent<"discoverEmbeds">) => { setSources((s) => { evt.embeds.forEach((v) => { - const source = providers.getMetadata(v.embedScraperId); + const source = getCachedMetadata().find( + (src) => src.id === v.embedScraperId + ); if (!source) throw new Error("invalid source id"); const out: ScrapingSegment = { embedId: v.embedScraperId, @@ -149,37 +155,18 @@ export function useScrape() { const providerApiUrl = getLoadbalancedProviderApiUrl(); if (providerApiUrl) { startScrape(); - const sseOutput = await new Promise( - (resolve, reject) => { - const baseUrlMaker = makeProviderUrl(providerApiUrl); - const scrapeEvents = new EventSource(baseUrlMaker.scrapeAll(media)); - scrapeEvents.addEventListener("init", (e) => { - initEvent(JSON.parse(e.data)); - }); - scrapeEvents.addEventListener("error", (err) => { - console.error("failed to use provider api", err); - reject(err); - }); - scrapeEvents.addEventListener("start", (e) => - startEvent(JSON.parse(e.data)) - ); - scrapeEvents.addEventListener("update", (e) => - updateEvent(JSON.parse(e.data)) - ); - scrapeEvents.addEventListener("discoverEmbeds", (e) => - discoverEmbedsEvent(JSON.parse(e.data)) - ); - scrapeEvents.addEventListener("completed", (e) => { - scrapeEvents.close(); - resolve(JSON.parse(e.data)); - }); - scrapeEvents.addEventListener("noOutput", () => { - scrapeEvents.close(); - resolve(null); - }); - } + const baseUrlMaker = makeProviderUrl(providerApiUrl); + const conn = connectServerSideEvents( + baseUrlMaker.scrapeAll(media), + ["completed", "noOutput"] ); - return getResult(sseOutput); + 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; diff --git a/src/pages/parts/player/MetaPart.tsx b/src/pages/parts/player/MetaPart.tsx index 1101542c..86a79a8a 100644 --- a/src/pages/parts/player/MetaPart.tsx +++ b/src/pages/parts/player/MetaPart.tsx @@ -3,6 +3,10 @@ import { useHistory, useParams } from "react-router-dom"; import { useAsync } from "react-use"; import type { AsyncReturnType } from "type-fest"; +import { + fetchMetadata, + setCachedMetadata, +} from "@/backend/helpers/providerApi"; import { DetailedMeta, getMetaFromId } from "@/backend/metadata/getmeta"; import { decodeTMDBId } from "@/backend/metadata/tmdb"; import { MWMediaType } from "@/backend/metadata/types/mw"; @@ -14,6 +18,7 @@ import { Paragraph } from "@/components/text/Paragraph"; import { Title } from "@/components/text/Title"; import { ErrorContainer, ErrorLayout } from "@/pages/layouts/ErrorLayout"; import { conf } from "@/setup/config"; +import { getLoadbalancedProviderApiUrl, providers } from "@/utils/providers"; export interface MetaPartProps { onGetMeta?: (meta: DetailedMeta, episodeId?: string) => void; @@ -36,6 +41,16 @@ export function MetaPart(props: MetaPartProps) { const history = useHistory(); const { error, value, loading } = useAsync(async () => { + const providerApiUrl = getLoadbalancedProviderApiUrl(); + if (providerApiUrl) { + await fetchMetadata(providerApiUrl); + } else { + setCachedMetadata([ + ...providers.listSources(), + ...providers.listEmbeds(), + ]); + } + let data: ReturnType = null; try { data = decodeTMDBId(params.media);