mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-25 19:01:52 +01:00
Handle more turnstile errors + show interactive prompt + handle provider api metadata errors
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
0fec65ea7b
commit
1091253392
@ -165,6 +165,12 @@
|
||||
"close": "Close"
|
||||
},
|
||||
"player": {
|
||||
"turnstile": {
|
||||
"verifyingHumanity": "Verifying your humanity...",
|
||||
"title": "We need to verify that you're human.",
|
||||
"description": "Please verify that you are human by completing the Captcha on the right. This is to keep movie-web safe!",
|
||||
"error": "Failed to verify your humanity. Please try again."
|
||||
},
|
||||
"back": {
|
||||
"default": "Back to home",
|
||||
"short": "Back"
|
||||
@ -261,6 +267,10 @@
|
||||
"text": "Could not load the media's metadata from TMDB. Please check whether TMDB is down or blocked on your internet connection.",
|
||||
"title": "Failed to load metadata"
|
||||
},
|
||||
"api": {
|
||||
"text": "Could not load API metadata, please check your internet connection.",
|
||||
"title": "Failed to load API metadata"
|
||||
},
|
||||
"notFound": {
|
||||
"badge": "Not found",
|
||||
"homeButton": "Back to home",
|
||||
|
@ -2,12 +2,14 @@ import classNames from "classnames";
|
||||
import FocusTrap from "focus-trap-react";
|
||||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Transition } from "@/components/utils/Transition";
|
||||
import {
|
||||
useInternalOverlayRouter,
|
||||
useRouterAnchorUpdate,
|
||||
} from "@/hooks/useOverlayRouter";
|
||||
import { TurnstileProvider } from "@/stores/turnstile";
|
||||
|
||||
export interface OverlayProps {
|
||||
id: string;
|
||||
@ -15,6 +17,34 @@ export interface OverlayProps {
|
||||
darken?: boolean;
|
||||
}
|
||||
|
||||
function TurnstileInteractive() {
|
||||
const { t } = useTranslation();
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
// this may not rerender with different dom structure, must be exactly the same always
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"absolute w-10/12 max-w-[800px] bg-background-main p-20 rounded-lg select-none z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform",
|
||||
show ? "" : "hidden",
|
||||
)}
|
||||
>
|
||||
<div className="w-full grid lg:grid-cols-[1fr,auto] gap-12 items-center">
|
||||
<div className="text-left">
|
||||
<h2 className="text-type-emphasis font-bold text-xl mb-6">
|
||||
{t("player.turnstile.title")}
|
||||
</h2>
|
||||
<p>{t("player.turnstile.description")}</p>
|
||||
</div>
|
||||
<TurnstileProvider
|
||||
isInPopout
|
||||
onUpdateShow={(shouldShow) => setShow(shouldShow)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OverlayDisplay(props: { children: ReactNode }) {
|
||||
const router = useInternalOverlayRouter("hello world :)");
|
||||
const refRouter = useRef(router);
|
||||
@ -27,7 +57,12 @@ export function OverlayDisplay(props: { children: ReactNode }) {
|
||||
r.close();
|
||||
};
|
||||
}, []);
|
||||
return <div className="popout-location">{props.children}</div>;
|
||||
return (
|
||||
<div className="popout-location">
|
||||
<TurnstileInteractive />
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OverlayPortal(props: {
|
||||
|
@ -43,7 +43,11 @@ export function MetaPart(props: MetaPartProps) {
|
||||
const { error, value, loading } = useAsync(async () => {
|
||||
const providerApiUrl = getLoadbalancedProviderApiUrl();
|
||||
if (providerApiUrl) {
|
||||
try {
|
||||
await fetchMetadata(providerApiUrl);
|
||||
} catch (err) {
|
||||
throw new Error("failed-api-metadata");
|
||||
}
|
||||
} else {
|
||||
setCachedMetadata([
|
||||
...providers.listSources(),
|
||||
@ -117,6 +121,28 @@ export function MetaPart(props: MetaPartProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (error && error.message === "failed-api-metadata") {
|
||||
return (
|
||||
<ErrorLayout>
|
||||
<ErrorContainer>
|
||||
<IconPill icon={Icons.WAND}>
|
||||
{t("player.metadata.failed.badge")}
|
||||
</IconPill>
|
||||
<Title>{t("player.metadata.api.text")}</Title>
|
||||
<Paragraph>{t("player.metadata.api.title")}</Paragraph>
|
||||
<Button
|
||||
href="/"
|
||||
theme="purple"
|
||||
padding="md:px-12 p-2.5"
|
||||
className="mt-6"
|
||||
>
|
||||
{t("player.metadata.failed.homeButton")}
|
||||
</Button>
|
||||
</ErrorContainer>
|
||||
</ErrorLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorLayout>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ProviderControls, ScrapeMedia } from "@movie-web/providers";
|
||||
import classNames from "classnames";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMountedState } from "react-use";
|
||||
import type { AsyncReturnType } from "type-fest";
|
||||
|
||||
@ -8,6 +9,8 @@ import {
|
||||
scrapePartsToProviderMetric,
|
||||
useReportProviders,
|
||||
} from "@/backend/helpers/report";
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { Loading } from "@/components/layout/Loading";
|
||||
import {
|
||||
ScrapeCard,
|
||||
ScrapeItem,
|
||||
@ -18,6 +21,7 @@ import {
|
||||
useListCenter,
|
||||
useScrape,
|
||||
} from "@/hooks/useProviderScrape";
|
||||
import { LargeTextPart } from "@/pages/parts/util/LargeTextPart";
|
||||
|
||||
export interface ScrapingProps {
|
||||
media: ScrapeMedia;
|
||||
@ -32,9 +36,11 @@ export function ScrapingPart(props: ScrapingProps) {
|
||||
const { report } = useReportProviders();
|
||||
const { startScraping, sourceOrder, sources, currentSource } = useScrape();
|
||||
const isMounted = useMountedState();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const [failedStartScrape, setFailedStartScrape] = useState<boolean>(false);
|
||||
const renderedOnce = useListCenter(
|
||||
containerRef,
|
||||
listRef,
|
||||
@ -72,7 +78,7 @@ export function ScrapingPart(props: ScrapingProps) {
|
||||
),
|
||||
);
|
||||
props.onGetStream?.(output);
|
||||
})();
|
||||
})().catch(() => setFailedStartScrape(true));
|
||||
}, [startScraping, props, report, isMounted]);
|
||||
|
||||
let currentProviderIndex = sourceOrder.findIndex(
|
||||
@ -81,11 +87,28 @@ export function ScrapingPart(props: ScrapingProps) {
|
||||
if (currentProviderIndex === -1)
|
||||
currentProviderIndex = sourceOrder.length - 1;
|
||||
|
||||
if (failedStartScrape)
|
||||
return (
|
||||
<LargeTextPart
|
||||
iconSlot={
|
||||
<Icon className="text-type-danger text-2xl" icon={Icons.WARNING} />
|
||||
}
|
||||
>
|
||||
{t("player.turnstile.error")}
|
||||
</LargeTextPart>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="h-full w-full relative dir-neutral:origin-top-left flex"
|
||||
ref={containerRef}
|
||||
>
|
||||
{!sourceOrder || sourceOrder.length === 0 ? (
|
||||
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-center flex flex-col justify-center z-0">
|
||||
<Loading className="mb-8" />
|
||||
<p>{t("player.turnstile.verifyingHumanity")}</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={classNames({
|
||||
"absolute transition-[transform,opacity] opacity-0 dir-neutral:left-0":
|
||||
@ -97,7 +120,7 @@ export function ScrapingPart(props: ScrapingProps) {
|
||||
{sourceOrder.map((order) => {
|
||||
const source = sources[order.id];
|
||||
const distance = Math.abs(
|
||||
sourceOrder.findIndex((t) => t.id === order.id) -
|
||||
sourceOrder.findIndex((o) => o.id === order.id) -
|
||||
currentProviderIndex,
|
||||
);
|
||||
return (
|
||||
|
@ -11,24 +11,32 @@ interface BannerInstance {
|
||||
interface BannerStore {
|
||||
banners: BannerInstance[];
|
||||
isOnline: boolean;
|
||||
isTurnstile: boolean;
|
||||
location: string | null;
|
||||
updateHeight(id: string, height: number): void;
|
||||
showBanner(id: string): void;
|
||||
hideBanner(id: string): void;
|
||||
setLocation(loc: string | null): void;
|
||||
updateOnline(isOnline: boolean): void;
|
||||
updateTurnstile(isTurnstile: boolean): void;
|
||||
}
|
||||
|
||||
export const useBannerStore = create(
|
||||
immer<BannerStore>((set) => ({
|
||||
banners: [],
|
||||
isOnline: true,
|
||||
isTurnstile: false,
|
||||
location: null,
|
||||
updateOnline(isOnline) {
|
||||
set((s) => {
|
||||
s.isOnline = isOnline;
|
||||
});
|
||||
},
|
||||
updateTurnstile(isTurnstile) {
|
||||
set((s) => {
|
||||
s.isTurnstile = isTurnstile;
|
||||
});
|
||||
},
|
||||
setLocation(loc) {
|
||||
set((s) => {
|
||||
s.location = loc;
|
||||
|
@ -1,3 +1,5 @@
|
||||
import classNames from "classnames";
|
||||
import { useRef } from "react";
|
||||
import Turnstile, { BoundTurnstileObject } from "react-turnstile";
|
||||
import { create } from "zustand";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
@ -6,19 +8,31 @@ import { reportCaptchaSolve } from "@/backend/helpers/report";
|
||||
import { conf } from "@/setup/config";
|
||||
|
||||
export interface TurnstileStore {
|
||||
turnstile: BoundTurnstileObject | null;
|
||||
isInWidget: boolean;
|
||||
turnstiles: {
|
||||
controls: BoundTurnstileObject;
|
||||
isInPopout: boolean;
|
||||
id: string;
|
||||
}[];
|
||||
cbs: ((token: string | null) => void)[];
|
||||
setTurnstile(v: BoundTurnstileObject | null): void;
|
||||
setTurnstile(
|
||||
id: string,
|
||||
v: BoundTurnstileObject | null,
|
||||
isInPopout: boolean,
|
||||
): void;
|
||||
getToken(): Promise<string>;
|
||||
processToken(token: string | null): void;
|
||||
processToken(token: string | null, widgetId: string): void;
|
||||
}
|
||||
|
||||
export const useTurnstileStore = create(
|
||||
immer<TurnstileStore>((set, get) => ({
|
||||
turnstile: null,
|
||||
isInWidget: false,
|
||||
turnstiles: [],
|
||||
cbs: [],
|
||||
processToken(token) {
|
||||
processToken(token, widgetId) {
|
||||
const cbs = get().cbs;
|
||||
const turnstile = get().turnstiles.find((v) => v.id === widgetId);
|
||||
if (turnstile?.id !== widgetId) return;
|
||||
cbs.forEach((fn) => fn(token));
|
||||
set((s) => {
|
||||
s.cbs = [];
|
||||
@ -37,16 +51,26 @@ export const useTurnstileStore = create(
|
||||
});
|
||||
});
|
||||
},
|
||||
setTurnstile(v) {
|
||||
setTurnstile(id, controls, isInPopout) {
|
||||
set((s) => {
|
||||
s.turnstile = v;
|
||||
s.turnstiles = s.turnstiles.filter((v) => v.id !== id);
|
||||
if (controls) {
|
||||
s.turnstiles.push({
|
||||
controls,
|
||||
isInPopout,
|
||||
id,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
||||
export function getTurnstile() {
|
||||
return useTurnstileStore.getState().turnstile;
|
||||
const turnstiles = useTurnstileStore.getState().turnstiles;
|
||||
const inPopout = turnstiles.find((v) => v.isInPopout);
|
||||
if (inPopout) return inPopout;
|
||||
return turnstiles[0];
|
||||
}
|
||||
|
||||
export function isTurnstileInitialized() {
|
||||
@ -55,9 +79,12 @@ export function isTurnstileInitialized() {
|
||||
|
||||
export async function getTurnstileToken() {
|
||||
const turnstile = getTurnstile();
|
||||
turnstile?.reset();
|
||||
turnstile?.execute();
|
||||
try {
|
||||
// I hate turnstile
|
||||
(window as any).turnstile.execute(
|
||||
document.querySelector(`#${turnstile.id}`),
|
||||
{},
|
||||
);
|
||||
const token = await useTurnstileStore.getState().getToken();
|
||||
reportCaptchaSolve(true);
|
||||
return token;
|
||||
@ -67,23 +94,44 @@ export async function getTurnstileToken() {
|
||||
}
|
||||
}
|
||||
|
||||
export function TurnstileProvider() {
|
||||
export function TurnstileProvider(props: {
|
||||
isInPopout?: boolean;
|
||||
onUpdateShow?: (show: boolean) => void;
|
||||
}) {
|
||||
const siteKey = conf().TURNSTILE_KEY;
|
||||
const idRef = useRef<string | null>(null);
|
||||
const setTurnstile = useTurnstileStore((s) => s.setTurnstile);
|
||||
const processToken = useTurnstileStore((s) => s.processToken);
|
||||
if (!siteKey) return null;
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
hidden: !props.isInPopout,
|
||||
})}
|
||||
>
|
||||
<Turnstile
|
||||
sitekey={siteKey}
|
||||
onLoad={(_widgetId, bound) => {
|
||||
setTurnstile(bound);
|
||||
onLoad={(widgetId, bound) => {
|
||||
idRef.current = widgetId;
|
||||
setTurnstile(widgetId, bound, !!props.isInPopout);
|
||||
}}
|
||||
onError={() => {
|
||||
processToken(null);
|
||||
const id = idRef.current;
|
||||
if (!id) return;
|
||||
processToken(null, id);
|
||||
}}
|
||||
onVerify={(token) => {
|
||||
processToken(token);
|
||||
const id = idRef.current;
|
||||
if (!id) return;
|
||||
processToken(token, id);
|
||||
props.onUpdateShow?.(false);
|
||||
}}
|
||||
onBeforeInteractive={() => {
|
||||
props.onUpdateShow?.(true);
|
||||
}}
|
||||
refreshExpired="never"
|
||||
execution="render"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user