mirror of
https://github.com/p-stream/p-stream.git
synced 2026-01-11 20:10:32 +00:00
add m3u8 proxy test and toggle to admin page
This commit is contained in:
parent
e8c3f1f25f
commit
774237c537
3 changed files with 337 additions and 2 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
314
src/pages/parts/admin/M3U8TestPart.tsx
Normal file
314
src/pages/parts/admin/M3U8TestPart.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue