mirror of
https://github.com/movie-web/movie-web.git
synced 2025-01-01 13:06:00 +00:00
reporting source selection menu
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
8dcb94d3ae
commit
117da3335b
|
@ -3,6 +3,7 @@ import { ofetch } from "ofetch";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { ScrapingItems, ScrapingSegment } from "@/hooks/useProviderScrape";
|
import { ScrapingItems, ScrapingSegment } from "@/hooks/useProviderScrape";
|
||||||
|
import { PlayerMeta } from "@/stores/player/slices/source";
|
||||||
|
|
||||||
const metricsEndpoint = "https://backend.movie-web.app/metrics/providers";
|
const metricsEndpoint = "https://backend.movie-web.app/metrics/providers";
|
||||||
|
|
||||||
|
@ -46,6 +47,32 @@ const segmentStatusMap: Record<
|
||||||
waiting: null,
|
waiting: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function scrapeSourceOutputToProviderMetric(
|
||||||
|
media: PlayerMeta,
|
||||||
|
providerId: string,
|
||||||
|
embedId: string | null,
|
||||||
|
status: ProviderMetric["status"],
|
||||||
|
err: unknown | null
|
||||||
|
): ProviderMetric {
|
||||||
|
const episodeId = media.episode?.tmdbId;
|
||||||
|
const seasonId = media.season?.tmdbId;
|
||||||
|
let error: undefined | Error;
|
||||||
|
if (err instanceof Error) error = err;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
providerId,
|
||||||
|
title: media.title,
|
||||||
|
tmdbId: media.tmdbId,
|
||||||
|
type: media.type,
|
||||||
|
embedId: embedId ?? undefined,
|
||||||
|
episodeId,
|
||||||
|
seasonId,
|
||||||
|
errorMessage: error?.message,
|
||||||
|
fullError: error ? getStackTrace(error, 5) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function scrapeSegmentToProviderMetric(
|
export function scrapeSegmentToProviderMetric(
|
||||||
media: ScrapeMedia,
|
media: ScrapeMedia,
|
||||||
providerId: string,
|
providerId: string,
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { ReactNode, useEffect, useMemo, useRef } from "react";
|
import { ReactNode, useEffect, useMemo, useRef } from "react";
|
||||||
import { useAsyncFn } from "react-use";
|
|
||||||
|
|
||||||
import { Loading } from "@/components/layout/Loading";
|
import { Loading } from "@/components/layout/Loading";
|
||||||
|
import {
|
||||||
|
useEmbedScraping,
|
||||||
|
useSourceScraping,
|
||||||
|
} from "@/components/player/hooks/useSourceSelection";
|
||||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||||
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
|
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
|
||||||
import { convertRunoutputToSource } from "@/components/player/utils/convertRunoutputToSource";
|
|
||||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
import { metaToScrapeMedia } from "@/stores/player/slices/source";
|
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
import { providers } from "@/utils/providers";
|
import { providers } from "@/utils/providers";
|
||||||
|
|
||||||
|
@ -23,15 +24,9 @@ export interface EmbedSelectionViewProps {
|
||||||
export function EmbedOption(props: {
|
export function EmbedOption(props: {
|
||||||
embedId: string;
|
embedId: string;
|
||||||
url: string;
|
url: string;
|
||||||
sourceId: string | null;
|
sourceId: string;
|
||||||
routerId: string;
|
routerId: string;
|
||||||
}) {
|
}) {
|
||||||
const router = useOverlayRouter(props.routerId);
|
|
||||||
const meta = usePlayerStore((s) => s.meta);
|
|
||||||
const setSource = usePlayerStore((s) => s.setSource);
|
|
||||||
const setSourceId = usePlayerStore((s) => s.setSourceId);
|
|
||||||
const progress = usePlayerStore((s) => s.progress.time);
|
|
||||||
|
|
||||||
const unknownEmbedName = "Unknown";
|
const unknownEmbedName = "Unknown";
|
||||||
|
|
||||||
const embedName = useMemo(() => {
|
const embedName = useMemo(() => {
|
||||||
|
@ -40,22 +35,15 @@ export function EmbedOption(props: {
|
||||||
return sourceMeta?.name ?? unknownEmbedName;
|
return sourceMeta?.name ?? unknownEmbedName;
|
||||||
}, [props.embedId]);
|
}, [props.embedId]);
|
||||||
|
|
||||||
const [request, run] = useAsyncFn(async () => {
|
const { run, errored, loading } = useEmbedScraping(
|
||||||
const result = await providers.runEmbedScraper({
|
props.routerId,
|
||||||
id: props.embedId,
|
props.sourceId,
|
||||||
url: props.url,
|
props.url,
|
||||||
});
|
props.embedId
|
||||||
setSourceId(props.sourceId);
|
);
|
||||||
setSource(convertRunoutputToSource({ stream: result.stream }), progress);
|
|
||||||
router.close();
|
|
||||||
}, [props.embedId, props.sourceId, meta, router]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectableLink
|
<SelectableLink loading={loading} error={errored} onClick={run}>
|
||||||
loading={request.loading}
|
|
||||||
error={request.error}
|
|
||||||
onClick={run}
|
|
||||||
>
|
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span>{embedName}</span>
|
<span>{embedName}</span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -63,48 +51,16 @@ export function EmbedOption(props: {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor this file: cleanup + reporting
|
|
||||||
|
|
||||||
export function EmbedSelectionView({ sourceId, id }: EmbedSelectionViewProps) {
|
export function EmbedSelectionView({ sourceId, id }: EmbedSelectionViewProps) {
|
||||||
const router = useOverlayRouter(id);
|
const router = useOverlayRouter(id);
|
||||||
const meta = usePlayerStore((s) => s.meta);
|
const { run, watching, notfound, loading, items, errored } =
|
||||||
const setSource = usePlayerStore((s) => s.setSource);
|
useSourceScraping(sourceId, id);
|
||||||
const setSourceId = usePlayerStore((s) => s.setSourceId);
|
|
||||||
const progress = usePlayerStore((s) => s.progress.time);
|
|
||||||
|
|
||||||
const sourceName = useMemo(() => {
|
const sourceName = useMemo(() => {
|
||||||
if (!sourceId) return "...";
|
if (!sourceId) return "...";
|
||||||
const sourceMeta = providers.getMetadata(sourceId);
|
const sourceMeta = providers.getMetadata(sourceId);
|
||||||
return sourceMeta?.name ?? "...";
|
return sourceMeta?.name ?? "...";
|
||||||
}, [sourceId]);
|
}, [sourceId]);
|
||||||
const [request, run] = useAsyncFn(async () => {
|
|
||||||
if (!sourceId || !meta) return null;
|
|
||||||
const scrapeMedia = metaToScrapeMedia(meta);
|
|
||||||
const result = await providers.runSourceScraper({
|
|
||||||
id: sourceId,
|
|
||||||
media: scrapeMedia,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.stream) {
|
|
||||||
setSource(convertRunoutputToSource({ stream: result.stream }), progress);
|
|
||||||
setSourceId(sourceId);
|
|
||||||
router.close();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (result.embeds.length === 1) {
|
|
||||||
const embedResult = await providers.runEmbedScraper({
|
|
||||||
id: result.embeds[0].embedId,
|
|
||||||
url: result.embeds[0].url,
|
|
||||||
});
|
|
||||||
setSourceId(sourceId);
|
|
||||||
setSource(
|
|
||||||
convertRunoutputToSource({ stream: embedResult.stream }),
|
|
||||||
progress
|
|
||||||
);
|
|
||||||
router.close();
|
|
||||||
}
|
|
||||||
return result.embeds;
|
|
||||||
}, [sourceId, meta, router]);
|
|
||||||
|
|
||||||
const lastSourceId = useRef<string | null>(null);
|
const lastSourceId = useRef<string | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -115,27 +71,35 @@ export function EmbedSelectionView({ sourceId, id }: EmbedSelectionViewProps) {
|
||||||
}, [run, sourceId]);
|
}, [run, sourceId]);
|
||||||
|
|
||||||
let content: ReactNode = null;
|
let content: ReactNode = null;
|
||||||
if (request.loading)
|
if (loading)
|
||||||
content = (
|
content = (
|
||||||
<Menu.TextDisplay noIcon>
|
<Menu.TextDisplay noIcon>
|
||||||
<Loading />
|
<Loading />
|
||||||
</Menu.TextDisplay>
|
</Menu.TextDisplay>
|
||||||
);
|
);
|
||||||
else if (request.error)
|
else if (notfound)
|
||||||
|
content = (
|
||||||
|
<Menu.TextDisplay title="No stream">
|
||||||
|
This source has no streams for this movie or show.
|
||||||
|
</Menu.TextDisplay>
|
||||||
|
);
|
||||||
|
else if (items?.length === 0)
|
||||||
|
content = (
|
||||||
|
<Menu.TextDisplay title="No embeds found">
|
||||||
|
We were unable to find any embeds for this source, please try another.
|
||||||
|
</Menu.TextDisplay>
|
||||||
|
);
|
||||||
|
else if (errored)
|
||||||
content = (
|
content = (
|
||||||
<Menu.TextDisplay title="Failed to scrape">
|
<Menu.TextDisplay title="Failed to scrape">
|
||||||
We were unable to find any videos for this source. Don't come
|
We were unable to find any videos for this source. Don't come
|
||||||
bitchin' to us about it, just try another source.
|
bitchin' to us about it, just try another source.
|
||||||
</Menu.TextDisplay>
|
</Menu.TextDisplay>
|
||||||
);
|
);
|
||||||
else if (request.value && request.value.length === 0)
|
else if (watching)
|
||||||
content = (
|
content = null; // when it starts watching, empty the display
|
||||||
<Menu.TextDisplay title="No embeds found">
|
else if (items && sourceId)
|
||||||
We were unable to find any embeds for this source, please try another.
|
content = items.map((v) => (
|
||||||
</Menu.TextDisplay>
|
|
||||||
);
|
|
||||||
else if (request.value)
|
|
||||||
content = request.value.map((v) => (
|
|
||||||
<EmbedOption
|
<EmbedOption
|
||||||
key={`${v.embedId}-${v.url}`}
|
key={`${v.embedId}-${v.url}`}
|
||||||
embedId={v.embedId}
|
embedId={v.embedId}
|
||||||
|
|
156
src/components/player/hooks/useSourceSelection.ts
Normal file
156
src/components/player/hooks/useSourceSelection.ts
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import {
|
||||||
|
EmbedOutput,
|
||||||
|
NotFoundError,
|
||||||
|
SourcererOutput,
|
||||||
|
} from "@movie-web/providers";
|
||||||
|
import { useAsyncFn } from "react-use";
|
||||||
|
|
||||||
|
import {
|
||||||
|
scrapeSourceOutputToProviderMetric,
|
||||||
|
useReportProviders,
|
||||||
|
} from "@/backend/helpers/report";
|
||||||
|
import { convertRunoutputToSource } from "@/components/player/utils/convertRunoutputToSource";
|
||||||
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
|
import { metaToScrapeMedia } from "@/stores/player/slices/source";
|
||||||
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
import { providers } from "@/utils/providers";
|
||||||
|
|
||||||
|
export function useEmbedScraping(
|
||||||
|
routerId: string,
|
||||||
|
sourceId: string,
|
||||||
|
url: string,
|
||||||
|
embedId: string
|
||||||
|
) {
|
||||||
|
const setSource = usePlayerStore((s) => s.setSource);
|
||||||
|
const setSourceId = usePlayerStore((s) => s.setSourceId);
|
||||||
|
const progress = usePlayerStore((s) => s.progress.time);
|
||||||
|
const meta = usePlayerStore((s) => s.meta);
|
||||||
|
const router = useOverlayRouter(routerId);
|
||||||
|
const { report } = useReportProviders();
|
||||||
|
|
||||||
|
const [request, run] = useAsyncFn(async () => {
|
||||||
|
let result: EmbedOutput | undefined;
|
||||||
|
if (!meta) return;
|
||||||
|
try {
|
||||||
|
result = await providers.runEmbedScraper({
|
||||||
|
id: embedId,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to scrape ${embedId}`, err);
|
||||||
|
const notFound = err instanceof NotFoundError;
|
||||||
|
const status = notFound ? "notfound" : "failed";
|
||||||
|
report([
|
||||||
|
scrapeSourceOutputToProviderMetric(
|
||||||
|
meta,
|
||||||
|
sourceId,
|
||||||
|
embedId,
|
||||||
|
status,
|
||||||
|
err
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
report([
|
||||||
|
scrapeSourceOutputToProviderMetric(meta, sourceId, null, "success", null),
|
||||||
|
]);
|
||||||
|
setSourceId(sourceId);
|
||||||
|
setSource(convertRunoutputToSource({ stream: result.stream }), progress);
|
||||||
|
router.close();
|
||||||
|
}, [embedId, sourceId, meta, router, report]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
run,
|
||||||
|
loading: request.loading,
|
||||||
|
errored: !!request.error,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSourceScraping(sourceId: string | null, routerId: string) {
|
||||||
|
const meta = usePlayerStore((s) => s.meta);
|
||||||
|
const setSource = usePlayerStore((s) => s.setSource);
|
||||||
|
const setSourceId = usePlayerStore((s) => s.setSourceId);
|
||||||
|
const progress = usePlayerStore((s) => s.progress.time);
|
||||||
|
const router = useOverlayRouter(routerId);
|
||||||
|
const { report } = useReportProviders();
|
||||||
|
|
||||||
|
const [request, run] = useAsyncFn(async () => {
|
||||||
|
if (!sourceId || !meta) return null;
|
||||||
|
const scrapeMedia = metaToScrapeMedia(meta);
|
||||||
|
|
||||||
|
let result: SourcererOutput | undefined;
|
||||||
|
try {
|
||||||
|
result = await providers.runSourceScraper({
|
||||||
|
id: sourceId,
|
||||||
|
media: scrapeMedia,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to scrape ${sourceId}`, err);
|
||||||
|
const notFound = err instanceof NotFoundError;
|
||||||
|
const status = notFound ? "notfound" : "failed";
|
||||||
|
report([
|
||||||
|
scrapeSourceOutputToProviderMetric(meta, sourceId, null, status, err),
|
||||||
|
]);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
report([
|
||||||
|
scrapeSourceOutputToProviderMetric(meta, sourceId, null, "success", null),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (result.stream) {
|
||||||
|
setSource(convertRunoutputToSource({ stream: result.stream }), progress);
|
||||||
|
setSourceId(sourceId);
|
||||||
|
router.close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (result.embeds.length === 1) {
|
||||||
|
let embedResult: EmbedOutput | undefined;
|
||||||
|
if (!meta) return;
|
||||||
|
try {
|
||||||
|
embedResult = await providers.runEmbedScraper({
|
||||||
|
id: result.embeds[0].embedId,
|
||||||
|
url: result.embeds[0].url,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to scrape ${result.embeds[0].embedId}`, err);
|
||||||
|
const notFound = err instanceof NotFoundError;
|
||||||
|
const status = notFound ? "notfound" : "failed";
|
||||||
|
report([
|
||||||
|
scrapeSourceOutputToProviderMetric(
|
||||||
|
meta,
|
||||||
|
sourceId,
|
||||||
|
result.embeds[0].embedId,
|
||||||
|
status,
|
||||||
|
err
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
report([
|
||||||
|
scrapeSourceOutputToProviderMetric(
|
||||||
|
meta,
|
||||||
|
sourceId,
|
||||||
|
result.embeds[0].embedId,
|
||||||
|
"success",
|
||||||
|
null
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
setSourceId(sourceId);
|
||||||
|
setSource(
|
||||||
|
convertRunoutputToSource({ stream: embedResult.stream }),
|
||||||
|
progress
|
||||||
|
);
|
||||||
|
router.close();
|
||||||
|
}
|
||||||
|
return result.embeds;
|
||||||
|
}, [sourceId, meta, router]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
run,
|
||||||
|
watching: (request.value ?? null) === null,
|
||||||
|
loading: request.loading,
|
||||||
|
items: request.value,
|
||||||
|
notfound: !!(request.error instanceof NotFoundError),
|
||||||
|
errored: !!request.error,
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue