p-stream/src/pages/parts/settings/SetupPart.tsx
2025-04-17 08:22:51 -06:00

350 lines
9.9 KiB
TypeScript

/* eslint-disable no-console */
import classNames from "classnames";
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { useAsync } from "react-use";
import { isExtensionActive } from "@/backend/extension/messaging";
import { singularProxiedFetch } from "@/backend/helpers/fetch";
import { Button } from "@/components/buttons/Button";
import { Icon, Icons } from "@/components/Icon";
import { Loading } from "@/components/layout/Loading";
import { SettingsCard } from "@/components/layout/SettingsCard";
import {
StatusCircle,
StatusCircleProps,
} from "@/components/player/internals/StatusCircle";
import { Heading3 } from "@/components/utils/Text";
import { conf } from "@/setup/config";
import { useAuthStore } from "@/stores/auth";
const getRegion = (): string | null => {
if (typeof window === "undefined") return null;
try {
const regionData = window.localStorage.getItem("__MW::region");
if (!regionData) return null;
const parsed = JSON.parse(regionData);
return parsed?.state?.region || null;
} catch {
return null;
}
};
const getBaseUrl = (): string => {
const region = getRegion();
switch (region) {
case "us-east":
return "https://fed-api-east.pstream.org";
case "us-west":
return "https://fed-api-west.pstream.org";
case "south-america":
return "https://fed-api-south.pstream.org";
case "asia":
return "https://fed-api-asia.pstream.org";
case "europe":
return "https://fed-api-europe.pstream.org";
default:
return "https://fed-api-east.pstream.org";
}
};
const BASE_URL = getBaseUrl();
const testUrl = "https://postman-echo.com/get";
const febboxApiTestUrl = `${BASE_URL}/movie//tt0325980`;
const sleep = (ms: number): Promise<void> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
};
export type Status = "success" | "unset" | "error";
type SetupData = {
extension: Status;
proxy: Status;
defaultProxy: Status;
febboxTokenTest?: Status;
};
function testProxy(url: string) {
return new Promise<void>((resolve, reject) => {
setTimeout(() => reject(new Error("Timed out!")), 3000);
singularProxiedFetch(url, testUrl, {})
.then((res) => {
if (res.url !== testUrl) return reject(new Error("Not a proxy"));
resolve();
})
.catch(reject);
});
}
export async function testFebboxToken(
febboxToken: string | null,
): Promise<Status> {
if (!febboxToken) {
return "unset";
}
let attempts = 0;
const maxAttempts = 3;
while (attempts < maxAttempts) {
console.log(
`Attempt ${attempts + 1} of ${maxAttempts} to check Febbox token`,
);
try {
const response = await fetch(febboxApiTestUrl, {
headers: {
"ui-token": febboxToken,
},
});
if (!response.ok) {
console.error("Febbox API test failed with status:", response.status);
attempts += 1;
if (attempts === maxAttempts) {
console.log("Max attempts reached, returning error");
return "error";
}
console.log("Retrying after failed response...");
await sleep(3000);
continue;
}
const data = (await response.json()) as any;
if (!data || !data.streams) {
console.error("Invalid response format from Febbox API:", data);
attempts += 1;
if (attempts === maxAttempts) {
console.log("Max attempts reached, returning error");
return "error";
}
console.log("Retrying after invalid response format...");
await sleep(3000);
continue;
}
const isVIPLink = Object.values(data.streams).some((link: any) => {
if (typeof link === "string") {
return link.toLowerCase().includes("vip");
}
return false;
});
if (isVIPLink) {
console.log("VIP link found, returning success");
return "success";
}
console.log("No VIP link found in attempt", attempts + 1);
attempts += 1;
if (attempts === maxAttempts) {
console.log("Max attempts reached, returning error");
return "error";
}
console.log("Retrying after no VIP link found...");
await sleep(3000);
} catch (error: any) {
console.error("Error testing Febbox token:", error);
attempts += 1;
if (attempts === maxAttempts) {
console.log("Max attempts reached, returning error");
return "error";
}
console.log("Retrying after error...");
await sleep(3000);
}
}
console.log("All attempts exhausted, returning error");
return "error";
}
function useIsSetup() {
const proxyUrls = useAuthStore((s) => s.proxySet);
const febboxToken = useAuthStore((s) => s.febboxToken);
const { loading, value } = useAsync(async (): Promise<SetupData> => {
const extensionStatus: Status = (await isExtensionActive())
? "success"
: "unset";
let proxyStatus: Status = "unset";
if (proxyUrls && proxyUrls.length > 0) {
try {
await testProxy(proxyUrls[0]);
proxyStatus = "success";
} catch {
proxyStatus = "error";
}
}
const febboxTokenStatus: Status = await testFebboxToken(febboxToken);
return {
extension: extensionStatus,
proxy: proxyStatus,
defaultProxy: "success",
...(conf().ALLOW_FEBBOX_KEY && {
febboxTokenTest: febboxTokenStatus,
}),
};
}, [proxyUrls, febboxToken]);
let globalState: Status = "unset";
if (
value?.extension === "success" ||
value?.proxy === "success" ||
value?.febboxTokenTest === "success"
)
globalState = "success";
if (
value?.proxy === "error" ||
value?.extension === "error" ||
value?.febboxTokenTest === "error"
)
globalState = "error";
return {
setupStates: value,
globalState,
loading,
};
}
function SetupCheckList(props: {
status: Status;
grey?: boolean;
highlight?: boolean;
children?: ReactNode;
}) {
const { t } = useTranslation();
const statusMap: Record<Status, StatusCircleProps["type"]> = {
error: "error",
success: "success",
unset: "noresult",
};
return (
<div className="flex items-start text-type-dimmed my-4">
<StatusCircle
type={statusMap[props.status]}
className={classNames({
"!text-video-scraping-noresult !bg-video-scraping-noresult opacity-50":
props.grey,
"scale-90 mr-3": true,
})}
/>
<div>
<p
className={classNames({
"!text-white": props.grey && props.highlight,
"!text-type-dimmed opacity-75": props.grey && !props.highlight,
"text-type-danger": props.status === "error",
"text-white": props.status === "success",
})}
>
{props.children}
</p>
{props.status === "error" ? (
<p className="max-w-96">
{t("settings.connections.setup.itemError")}
</p>
) : null}
</div>
</div>
);
}
export function SetupPart() {
const { t } = useTranslation();
const navigate = useNavigate();
const { loading, setupStates, globalState } = useIsSetup();
if (loading || !setupStates) {
return (
<SettingsCard>
<div className="flex py-6 items-center justify-center">
<Loading />
</div>
</SettingsCard>
);
}
const textLookupMap: Record<
Status,
{ title: string; desc: string; button: string }
> = {
error: {
title: "settings.connections.setup.errorStatus.title",
desc: "settings.connections.setup.errorStatus.description",
button: "settings.connections.setup.redoSetup",
},
success: {
title: "settings.connections.setup.successStatus.title",
desc: "settings.connections.setup.successStatus.description",
button: "settings.connections.setup.redoSetup",
},
unset: {
title: "settings.connections.setup.unsetStatus.title",
desc: "settings.connections.setup.unsetStatus.description",
button: "settings.connections.setup.doSetup",
},
};
return (
<SettingsCard>
<div className="flex flex-col md:flex-row items-start gap-4">
<div>
<div
className={classNames({
"rounded-full h-12 w-12 flex bg-opacity-15 justify-center items-center":
true,
"text-type-success bg-type-success": globalState === "success",
"text-type-danger bg-type-danger":
globalState === "error" || globalState === "unset",
})}
>
<Icon
icon={globalState === "success" ? Icons.CHECKMARK : Icons.X}
className="text-xl"
/>
</div>
</div>
<div className="flex-1">
<Heading3 className="!mb-3">
{t(textLookupMap[globalState].title)}
</Heading3>
<p className="max-w-[20rem] font-medium mb-6">
{t(textLookupMap[globalState].desc)}
</p>
<SetupCheckList status={setupStates.extension}>
{t("settings.connections.setup.items.extension")}
</SetupCheckList>
<SetupCheckList status={setupStates.proxy}>
{t("settings.connections.setup.items.proxy")}
</SetupCheckList>
<SetupCheckList
grey
highlight={globalState === "unset"}
status={setupStates.defaultProxy}
>
{t("settings.connections.setup.items.default")}
</SetupCheckList>
{conf().ALLOW_FEBBOX_KEY && (
<SetupCheckList status={setupStates.febboxTokenTest || "unset"}>
Febbox UI token
</SetupCheckList>
)}
</div>
<div className="md:mt-5">
<Button theme="purple" onClick={() => navigate("/onboarding")}>
{t(textLookupMap[globalState].button)}
</Button>
</div>
</div>
</SettingsCard>
);
}