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"
|
"close": "Close"
|
||||||
},
|
},
|
||||||
"player": {
|
"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": {
|
"back": {
|
||||||
"default": "Back to home",
|
"default": "Back to home",
|
||||||
"short": "Back"
|
"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.",
|
"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"
|
"title": "Failed to load metadata"
|
||||||
},
|
},
|
||||||
|
"api": {
|
||||||
|
"text": "Could not load API metadata, please check your internet connection.",
|
||||||
|
"title": "Failed to load API metadata"
|
||||||
|
},
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"badge": "Not found",
|
"badge": "Not found",
|
||||||
"homeButton": "Back to home",
|
"homeButton": "Back to home",
|
||||||
|
@ -2,12 +2,14 @@ import classNames from "classnames";
|
|||||||
import FocusTrap from "focus-trap-react";
|
import FocusTrap from "focus-trap-react";
|
||||||
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Transition } from "@/components/utils/Transition";
|
import { Transition } from "@/components/utils/Transition";
|
||||||
import {
|
import {
|
||||||
useInternalOverlayRouter,
|
useInternalOverlayRouter,
|
||||||
useRouterAnchorUpdate,
|
useRouterAnchorUpdate,
|
||||||
} from "@/hooks/useOverlayRouter";
|
} from "@/hooks/useOverlayRouter";
|
||||||
|
import { TurnstileProvider } from "@/stores/turnstile";
|
||||||
|
|
||||||
export interface OverlayProps {
|
export interface OverlayProps {
|
||||||
id: string;
|
id: string;
|
||||||
@ -15,6 +17,34 @@ export interface OverlayProps {
|
|||||||
darken?: boolean;
|
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 }) {
|
export function OverlayDisplay(props: { children: ReactNode }) {
|
||||||
const router = useInternalOverlayRouter("hello world :)");
|
const router = useInternalOverlayRouter("hello world :)");
|
||||||
const refRouter = useRef(router);
|
const refRouter = useRef(router);
|
||||||
@ -27,7 +57,12 @@ export function OverlayDisplay(props: { children: ReactNode }) {
|
|||||||
r.close();
|
r.close();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
return <div className="popout-location">{props.children}</div>;
|
return (
|
||||||
|
<div className="popout-location">
|
||||||
|
<TurnstileInteractive />
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OverlayPortal(props: {
|
export function OverlayPortal(props: {
|
||||||
|
@ -43,7 +43,11 @@ export function MetaPart(props: MetaPartProps) {
|
|||||||
const { error, value, loading } = useAsync(async () => {
|
const { error, value, loading } = useAsync(async () => {
|
||||||
const providerApiUrl = getLoadbalancedProviderApiUrl();
|
const providerApiUrl = getLoadbalancedProviderApiUrl();
|
||||||
if (providerApiUrl) {
|
if (providerApiUrl) {
|
||||||
await fetchMetadata(providerApiUrl);
|
try {
|
||||||
|
await fetchMetadata(providerApiUrl);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error("failed-api-metadata");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setCachedMetadata([
|
setCachedMetadata([
|
||||||
...providers.listSources(),
|
...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) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<ErrorLayout>
|
<ErrorLayout>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ProviderControls, ScrapeMedia } from "@movie-web/providers";
|
import { ProviderControls, ScrapeMedia } from "@movie-web/providers";
|
||||||
import classNames from "classnames";
|
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 { useMountedState } from "react-use";
|
||||||
import type { AsyncReturnType } from "type-fest";
|
import type { AsyncReturnType } from "type-fest";
|
||||||
|
|
||||||
@ -8,6 +9,8 @@ import {
|
|||||||
scrapePartsToProviderMetric,
|
scrapePartsToProviderMetric,
|
||||||
useReportProviders,
|
useReportProviders,
|
||||||
} from "@/backend/helpers/report";
|
} from "@/backend/helpers/report";
|
||||||
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
|
import { Loading } from "@/components/layout/Loading";
|
||||||
import {
|
import {
|
||||||
ScrapeCard,
|
ScrapeCard,
|
||||||
ScrapeItem,
|
ScrapeItem,
|
||||||
@ -18,6 +21,7 @@ import {
|
|||||||
useListCenter,
|
useListCenter,
|
||||||
useScrape,
|
useScrape,
|
||||||
} from "@/hooks/useProviderScrape";
|
} from "@/hooks/useProviderScrape";
|
||||||
|
import { LargeTextPart } from "@/pages/parts/util/LargeTextPart";
|
||||||
|
|
||||||
export interface ScrapingProps {
|
export interface ScrapingProps {
|
||||||
media: ScrapeMedia;
|
media: ScrapeMedia;
|
||||||
@ -32,9 +36,11 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||||||
const { report } = useReportProviders();
|
const { report } = useReportProviders();
|
||||||
const { startScraping, sourceOrder, sources, currentSource } = useScrape();
|
const { startScraping, sourceOrder, sources, currentSource } = useScrape();
|
||||||
const isMounted = useMountedState();
|
const isMounted = useMountedState();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const listRef = useRef<HTMLDivElement | null>(null);
|
const listRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [failedStartScrape, setFailedStartScrape] = useState<boolean>(false);
|
||||||
const renderedOnce = useListCenter(
|
const renderedOnce = useListCenter(
|
||||||
containerRef,
|
containerRef,
|
||||||
listRef,
|
listRef,
|
||||||
@ -72,7 +78,7 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
props.onGetStream?.(output);
|
props.onGetStream?.(output);
|
||||||
})();
|
})().catch(() => setFailedStartScrape(true));
|
||||||
}, [startScraping, props, report, isMounted]);
|
}, [startScraping, props, report, isMounted]);
|
||||||
|
|
||||||
let currentProviderIndex = sourceOrder.findIndex(
|
let currentProviderIndex = sourceOrder.findIndex(
|
||||||
@ -81,11 +87,28 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||||||
if (currentProviderIndex === -1)
|
if (currentProviderIndex === -1)
|
||||||
currentProviderIndex = sourceOrder.length - 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-full w-full relative dir-neutral:origin-top-left flex"
|
className="h-full w-full relative dir-neutral:origin-top-left flex"
|
||||||
ref={containerRef}
|
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
|
<div
|
||||||
className={classNames({
|
className={classNames({
|
||||||
"absolute transition-[transform,opacity] opacity-0 dir-neutral:left-0":
|
"absolute transition-[transform,opacity] opacity-0 dir-neutral:left-0":
|
||||||
@ -97,7 +120,7 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||||||
{sourceOrder.map((order) => {
|
{sourceOrder.map((order) => {
|
||||||
const source = sources[order.id];
|
const source = sources[order.id];
|
||||||
const distance = Math.abs(
|
const distance = Math.abs(
|
||||||
sourceOrder.findIndex((t) => t.id === order.id) -
|
sourceOrder.findIndex((o) => o.id === order.id) -
|
||||||
currentProviderIndex,
|
currentProviderIndex,
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
@ -11,24 +11,32 @@ interface BannerInstance {
|
|||||||
interface BannerStore {
|
interface BannerStore {
|
||||||
banners: BannerInstance[];
|
banners: BannerInstance[];
|
||||||
isOnline: boolean;
|
isOnline: boolean;
|
||||||
|
isTurnstile: boolean;
|
||||||
location: string | null;
|
location: string | null;
|
||||||
updateHeight(id: string, height: number): void;
|
updateHeight(id: string, height: number): void;
|
||||||
showBanner(id: string): void;
|
showBanner(id: string): void;
|
||||||
hideBanner(id: string): void;
|
hideBanner(id: string): void;
|
||||||
setLocation(loc: string | null): void;
|
setLocation(loc: string | null): void;
|
||||||
updateOnline(isOnline: boolean): void;
|
updateOnline(isOnline: boolean): void;
|
||||||
|
updateTurnstile(isTurnstile: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useBannerStore = create(
|
export const useBannerStore = create(
|
||||||
immer<BannerStore>((set) => ({
|
immer<BannerStore>((set) => ({
|
||||||
banners: [],
|
banners: [],
|
||||||
isOnline: true,
|
isOnline: true,
|
||||||
|
isTurnstile: false,
|
||||||
location: null,
|
location: null,
|
||||||
updateOnline(isOnline) {
|
updateOnline(isOnline) {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.isOnline = isOnline;
|
s.isOnline = isOnline;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
updateTurnstile(isTurnstile) {
|
||||||
|
set((s) => {
|
||||||
|
s.isTurnstile = isTurnstile;
|
||||||
|
});
|
||||||
|
},
|
||||||
setLocation(loc) {
|
setLocation(loc) {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.location = loc;
|
s.location = loc;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import classNames from "classnames";
|
||||||
|
import { useRef } from "react";
|
||||||
import Turnstile, { BoundTurnstileObject } from "react-turnstile";
|
import Turnstile, { BoundTurnstileObject } from "react-turnstile";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { immer } from "zustand/middleware/immer";
|
import { immer } from "zustand/middleware/immer";
|
||||||
@ -6,19 +8,31 @@ import { reportCaptchaSolve } from "@/backend/helpers/report";
|
|||||||
import { conf } from "@/setup/config";
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
export interface TurnstileStore {
|
export interface TurnstileStore {
|
||||||
turnstile: BoundTurnstileObject | null;
|
isInWidget: boolean;
|
||||||
|
turnstiles: {
|
||||||
|
controls: BoundTurnstileObject;
|
||||||
|
isInPopout: boolean;
|
||||||
|
id: string;
|
||||||
|
}[];
|
||||||
cbs: ((token: string | null) => void)[];
|
cbs: ((token: string | null) => void)[];
|
||||||
setTurnstile(v: BoundTurnstileObject | null): void;
|
setTurnstile(
|
||||||
|
id: string,
|
||||||
|
v: BoundTurnstileObject | null,
|
||||||
|
isInPopout: boolean,
|
||||||
|
): void;
|
||||||
getToken(): Promise<string>;
|
getToken(): Promise<string>;
|
||||||
processToken(token: string | null): void;
|
processToken(token: string | null, widgetId: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTurnstileStore = create(
|
export const useTurnstileStore = create(
|
||||||
immer<TurnstileStore>((set, get) => ({
|
immer<TurnstileStore>((set, get) => ({
|
||||||
turnstile: null,
|
isInWidget: false,
|
||||||
|
turnstiles: [],
|
||||||
cbs: [],
|
cbs: [],
|
||||||
processToken(token) {
|
processToken(token, widgetId) {
|
||||||
const cbs = get().cbs;
|
const cbs = get().cbs;
|
||||||
|
const turnstile = get().turnstiles.find((v) => v.id === widgetId);
|
||||||
|
if (turnstile?.id !== widgetId) return;
|
||||||
cbs.forEach((fn) => fn(token));
|
cbs.forEach((fn) => fn(token));
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.cbs = [];
|
s.cbs = [];
|
||||||
@ -37,16 +51,26 @@ export const useTurnstileStore = create(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setTurnstile(v) {
|
setTurnstile(id, controls, isInPopout) {
|
||||||
set((s) => {
|
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() {
|
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() {
|
export function isTurnstileInitialized() {
|
||||||
@ -55,9 +79,12 @@ export function isTurnstileInitialized() {
|
|||||||
|
|
||||||
export async function getTurnstileToken() {
|
export async function getTurnstileToken() {
|
||||||
const turnstile = getTurnstile();
|
const turnstile = getTurnstile();
|
||||||
turnstile?.reset();
|
|
||||||
turnstile?.execute();
|
|
||||||
try {
|
try {
|
||||||
|
// I hate turnstile
|
||||||
|
(window as any).turnstile.execute(
|
||||||
|
document.querySelector(`#${turnstile.id}`),
|
||||||
|
{},
|
||||||
|
);
|
||||||
const token = await useTurnstileStore.getState().getToken();
|
const token = await useTurnstileStore.getState().getToken();
|
||||||
reportCaptchaSolve(true);
|
reportCaptchaSolve(true);
|
||||||
return token;
|
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 siteKey = conf().TURNSTILE_KEY;
|
||||||
|
const idRef = useRef<string | null>(null);
|
||||||
const setTurnstile = useTurnstileStore((s) => s.setTurnstile);
|
const setTurnstile = useTurnstileStore((s) => s.setTurnstile);
|
||||||
const processToken = useTurnstileStore((s) => s.processToken);
|
const processToken = useTurnstileStore((s) => s.processToken);
|
||||||
if (!siteKey) return null;
|
if (!siteKey) return null;
|
||||||
return (
|
return (
|
||||||
<Turnstile
|
<div
|
||||||
sitekey={siteKey}
|
className={classNames({
|
||||||
onLoad={(_widgetId, bound) => {
|
hidden: !props.isInPopout,
|
||||||
setTurnstile(bound);
|
})}
|
||||||
}}
|
>
|
||||||
onError={() => {
|
<Turnstile
|
||||||
processToken(null);
|
sitekey={siteKey}
|
||||||
}}
|
onLoad={(widgetId, bound) => {
|
||||||
onVerify={(token) => {
|
idRef.current = widgetId;
|
||||||
processToken(token);
|
setTurnstile(widgetId, bound, !!props.isInPopout);
|
||||||
}}
|
}}
|
||||||
/>
|
onError={() => {
|
||||||
|
const id = idRef.current;
|
||||||
|
if (!id) return;
|
||||||
|
processToken(null, id);
|
||||||
|
}}
|
||||||
|
onVerify={(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