mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-21 05:02:24 +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 getLoadbalancedProxyUrl = makeLoadbalancedList(getProxyUrls);
|
||||||
export const getLoadbalancedProviderApiUrl =
|
export const getLoadbalancedProviderApiUrl =
|
||||||
makeLoadbalancedList(getProviderApiUrls);
|
makeLoadbalancedList(getProviderApiUrls);
|
||||||
export const getLoadbalancedM3U8ProxyUrl =
|
function getEnabledM3U8ProxyUrls() {
|
||||||
makeLoadbalancedList(getM3U8ProxyUrls);
|
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(
|
async function fetchButWithApiTokens(
|
||||||
input: RequestInfo | URL,
|
input: RequestInfo | URL,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Heading1, Paragraph } from "@/components/utils/Text";
|
||||||
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
||||||
import { ConfigValuesPart } from "@/pages/parts/admin/ConfigValuesPart";
|
import { ConfigValuesPart } from "@/pages/parts/admin/ConfigValuesPart";
|
||||||
import { ExtensionOverridePart } from "@/pages/parts/admin/ExtensionOverridePart";
|
import { ExtensionOverridePart } from "@/pages/parts/admin/ExtensionOverridePart";
|
||||||
|
import { M3U8TestPart } from "@/pages/parts/admin/M3U8TestPart";
|
||||||
import { RegionSelectorPart } from "@/pages/parts/admin/RegionSelectorPart";
|
import { RegionSelectorPart } from "@/pages/parts/admin/RegionSelectorPart";
|
||||||
import { TMDBTestPart } from "@/pages/parts/admin/TMDBTestPart";
|
import { TMDBTestPart } from "@/pages/parts/admin/TMDBTestPart";
|
||||||
import { WorkerTestPart } from "@/pages/parts/admin/WorkerTestPart";
|
import { WorkerTestPart } from "@/pages/parts/admin/WorkerTestPart";
|
||||||
|
|
@ -20,6 +21,7 @@ export function AdminPage() {
|
||||||
<BackendTestPart />
|
<BackendTestPart />
|
||||||
<WorkerTestPart />
|
<WorkerTestPart />
|
||||||
<TMDBTestPart />
|
<TMDBTestPart />
|
||||||
|
<M3U8TestPart />
|
||||||
<ExtensionOverridePart />
|
<ExtensionOverridePart />
|
||||||
<RegionSelectorPart />
|
<RegionSelectorPart />
|
||||||
</ThinContainer>
|
</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