add m3u8 proxy test and toggle to admin page

This commit is contained in:
Pas 2025-07-12 16:58:33 -06:00
parent e8c3f1f25f
commit 774237c537
3 changed files with 337 additions and 2 deletions

View file

@ -30,8 +30,27 @@ function makeLoadbalancedList(getter: () => string[]) {
export const getLoadbalancedProxyUrl = makeLoadbalancedList(getProxyUrls);
export const getLoadbalancedProviderApiUrl =
makeLoadbalancedList(getProviderApiUrls);
export const getLoadbalancedM3U8ProxyUrl =
makeLoadbalancedList(getM3U8ProxyUrls);
function getEnabledM3U8ProxyUrls() {
const allM3U8ProxyUrls = getM3U8ProxyUrls();
const enabledProxies = localStorage.getItem("m3u8-proxy-enabled");
if (!enabledProxies) {
return allM3U8ProxyUrls;
}
try {
const enabled = JSON.parse(enabledProxies);
return allM3U8ProxyUrls.filter(
(_url, index) => enabled[index.toString()] !== false,
);
} catch {
return allM3U8ProxyUrls;
}
}
export const getLoadbalancedM3U8ProxyUrl = makeLoadbalancedList(
getEnabledM3U8ProxyUrls,
);
async function fetchButWithApiTokens(
input: RequestInfo | URL,

View file

@ -3,6 +3,7 @@ import { Heading1, Paragraph } from "@/components/utils/Text";
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
import { ConfigValuesPart } from "@/pages/parts/admin/ConfigValuesPart";
import { ExtensionOverridePart } from "@/pages/parts/admin/ExtensionOverridePart";
import { M3U8TestPart } from "@/pages/parts/admin/M3U8TestPart";
import { RegionSelectorPart } from "@/pages/parts/admin/RegionSelectorPart";
import { TMDBTestPart } from "@/pages/parts/admin/TMDBTestPart";
import { WorkerTestPart } from "@/pages/parts/admin/WorkerTestPart";
@ -20,6 +21,7 @@ export function AdminPage() {
<BackendTestPart />
<WorkerTestPart />
<TMDBTestPart />
<M3U8TestPart />
<ExtensionOverridePart />
<RegionSelectorPart />
</ThinContainer>

View file

@ -0,0 +1,314 @@
import classNames from "classnames";
import { useEffect, useMemo, useState } from "react";
import { useAsyncFn } from "react-use";
import { Button } from "@/components/buttons/Button";
import { Toggle } from "@/components/buttons/Toggle";
import { Icon, Icons } from "@/components/Icon";
import { Box } from "@/components/layout/Box";
import { Divider } from "@/components/utils/Divider";
import { Heading2 } from "@/components/utils/Text";
import { getM3U8ProxyUrls } from "@/utils/proxyUrls";
export function M3U8ProxyItem(props: {
name: string;
errored?: boolean;
success?: boolean;
questionable?: boolean;
errorText?: string;
url?: string;
enabled?: boolean;
onToggle?: (enabled: boolean) => void;
}) {
const urlWithoutProtocol = props.url ? new URL(props.url).host : null;
return (
<div className="flex mb-2">
<Toggle
enabled={props.enabled}
onClick={() => props.onToggle?.(!props.enabled)}
/>
<Icon
icon={
props.errored
? Icons.X
: props.success
? Icons.CIRCLE_CHECK
: props.questionable
? Icons.CIRCLE_QUESTION
: Icons.EYE_SLASH
}
className={classNames({
"text-xl mr-2 mt-0.5 ml-3": true,
"text-video-scraping-error": props.errored,
"text-video-scraping-noresult":
!props.errored && !props.success && !props.questionable,
"text-video-scraping-success": props.success,
"text-yellow-400": props.questionable,
})}
/>
<div className="flex-1">
<p className="text-white font-bold">{props.name}</p>
{props.errorText ? <p>{props.errorText}</p> : null}
{urlWithoutProtocol ? <p>{urlWithoutProtocol}</p> : null}
</div>
</div>
);
}
export function M3U8TestPart() {
const m3u8ProxyList = useMemo(() => {
return getM3U8ProxyUrls().map((v, ind) => ({
id: ind.toString(),
url: v,
}));
}, []);
// Load enabled proxies from localStorage
const [enabledProxies, setEnabledProxies] = useState<Record<string, boolean>>(
() => {
const saved = localStorage.getItem("m3u8-proxy-enabled");
if (saved) {
try {
return JSON.parse(saved);
} catch {
return {};
}
}
// Default: all enabled
return Object.fromEntries(m3u8ProxyList.map((proxy) => [proxy.id, true]));
},
);
// Save enabled proxies to localStorage
useEffect(() => {
localStorage.setItem("m3u8-proxy-enabled", JSON.stringify(enabledProxies));
}, [enabledProxies]);
const [proxyState, setProxyState] = useState<
{
id: string;
status: "error" | "success" | "questionable";
error?: Error;
}[]
>([]);
const [buttonClicked, setButtonClicked] = useState(false);
const [buttonDisabled, setButtonDisabled] = useState(false);
const [testState, runTests] = useAsyncFn(async () => {
setButtonDisabled(true);
function updateProxy(id: string, data: (typeof proxyState)[number]) {
setProxyState((s) => {
return [...s.filter((v) => v.id !== id), data];
});
}
setProxyState([]);
const activeProxies = m3u8ProxyList.filter(
(proxy) => enabledProxies[proxy.id],
);
const proxyPromises = activeProxies.map(async (proxy) => {
try {
if (proxy.url.endsWith("/")) {
updateProxy(proxy.id, {
id: proxy.id,
status: "error",
error: new Error("URL ends with slash"),
});
return;
}
// Test if it can do the same destination fetch as CORS proxy
const testUrl = `${proxy.url}/?destination=${encodeURIComponent(
"https://postman-echo.com/get",
)}`;
const response = await fetch(testUrl);
if (response.ok) {
updateProxy(proxy.id, {
id: proxy.id,
status: "success",
});
}
} catch (err) {
const error = err as Error;
error.message = error.message.replace(proxy.url, "M3U8_PROXY_URL");
updateProxy(proxy.id, {
id: proxy.id,
status: "questionable",
error,
});
}
});
await Promise.all(proxyPromises);
setTimeout(() => setButtonDisabled(false), 5000);
}, [m3u8ProxyList, enabledProxies]);
const handleToggleProxy = (proxyId: string, enabled: boolean) => {
setEnabledProxies((prev) => ({
...prev,
[proxyId]: enabled,
}));
};
const allEnabled = m3u8ProxyList.every((proxy) => enabledProxies[proxy.id]);
const noneEnabled = m3u8ProxyList.every((proxy) => !enabledProxies[proxy.id]);
const handleToggleAll = () => {
if (allEnabled) {
// Disable all
setEnabledProxies(
Object.fromEntries(m3u8ProxyList.map((proxy) => [proxy.id, false])),
);
} else {
// Enable all
setEnabledProxies(
Object.fromEntries(m3u8ProxyList.map((proxy) => [proxy.id, true])),
);
}
};
const enabledCount = m3u8ProxyList.filter(
(proxy) => enabledProxies[proxy.id],
).length;
return (
<>
<Heading2 className="!mb-0 mt-12">M3U8 Proxy tests</Heading2>
<div className="flex items-center justify-between mb-8 mt-2">
<p>
{m3u8ProxyList.length} M3U8 proxy(s) registered ({enabledCount}{" "}
enabled)
</p>
<Button
theme="secondary"
onClick={handleToggleAll}
disabled={m3u8ProxyList.length === 0}
>
{allEnabled ? "Disable All" : "Enable All"}
</Button>
</div>
<Box>
{m3u8ProxyList.map((v, i) => {
const s = proxyState.find((segment) => segment.id === v.id);
const name = `M3U8 Proxy ${i + 1}`;
const enabled = enabledProxies[v.id];
if (!s) {
return (
<M3U8ProxyItem
name={name}
key={v.id}
enabled={enabled}
onToggle={(isEnabled) => handleToggleProxy(v.id, isEnabled)}
/>
);
}
if (s.status === "error") {
return (
<M3U8ProxyItem
name={name}
errored
key={v.id}
errorText={s.error?.toString()}
enabled={enabled}
onToggle={(isEnabled) => handleToggleProxy(v.id, isEnabled)}
/>
);
}
if (s.status === "success") {
return (
<M3U8ProxyItem
name={name}
url={v.url}
success
key={v.id}
enabled={enabled}
onToggle={(isEnabled) => handleToggleProxy(v.id, isEnabled)}
/>
);
}
if (s.status === "questionable") {
return (
<M3U8ProxyItem
name={name}
questionable
key={v.id}
errorText={s.error?.toString()}
enabled={enabled}
onToggle={(isEnabled) => handleToggleProxy(v.id, isEnabled)}
/>
);
}
return (
<M3U8ProxyItem
name={name}
key={v.id}
enabled={enabled}
onToggle={(isEnabled) => handleToggleProxy(v.id, isEnabled)}
/>
);
})}
<Divider />
<div className="flex justify-end">
{buttonClicked ? (
proxyState
.filter((p) => enabledProxies[p.id])
.every((proxy) => proxy.status === "success") ? (
<p>
All enabled M3U8 proxies have passed the test!{" "}
<span className="font-bold">٩(ˊˋ*)و</span>
</p>
) : (
<div>
<div className="text-right">
<p className="pb-4">
Some M3U8 proxies have failed the test...{" "}
<span className="font-bold">( )</span>
</p>
<div className="flex justify-end">
<Button
theme="purple"
loading={testState.loading}
onClick={async (event) => {
event.preventDefault();
setButtonDisabled(true);
await runTests();
setButtonClicked(true);
setTimeout(() => setButtonDisabled(false), 250);
}}
disabled={buttonDisabled || noneEnabled}
>
Test M3U8 proxies
</Button>
</div>
</div>
</div>
)
) : (
<Button
theme="purple"
loading={testState.loading}
onClick={async (event) => {
event.preventDefault();
setButtonDisabled(true);
await runTests();
setButtonClicked(true);
setTimeout(() => setButtonDisabled(false), 5000);
}}
disabled={buttonDisabled || noneEnabled}
>
Test M3U8 proxies
</Button>
)}
</div>
</Box>
</>
);
}