pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add m3u8 proxy test and toggle to admin page

Pas 774237c5 e8c3f1f2

+337 -2
+21 -2
src/backend/providers/fetchers.ts
··· 30 30 export const getLoadbalancedProxyUrl = makeLoadbalancedList(getProxyUrls); 31 31 export const getLoadbalancedProviderApiUrl = 32 32 makeLoadbalancedList(getProviderApiUrls); 33 - export const getLoadbalancedM3U8ProxyUrl = 34 - makeLoadbalancedList(getM3U8ProxyUrls); 33 + function getEnabledM3U8ProxyUrls() { 34 + const allM3U8ProxyUrls = getM3U8ProxyUrls(); 35 + const enabledProxies = localStorage.getItem("m3u8-proxy-enabled"); 36 + 37 + if (!enabledProxies) { 38 + return allM3U8ProxyUrls; 39 + } 40 + 41 + try { 42 + const enabled = JSON.parse(enabledProxies); 43 + return allM3U8ProxyUrls.filter( 44 + (_url, index) => enabled[index.toString()] !== false, 45 + ); 46 + } catch { 47 + return allM3U8ProxyUrls; 48 + } 49 + } 50 + 51 + export const getLoadbalancedM3U8ProxyUrl = makeLoadbalancedList( 52 + getEnabledM3U8ProxyUrls, 53 + ); 35 54 36 55 async function fetchButWithApiTokens( 37 56 input: RequestInfo | URL,
+2
src/pages/admin/AdminPage.tsx
··· 3 3 import { SubPageLayout } from "@/pages/layouts/SubPageLayout"; 4 4 import { ConfigValuesPart } from "@/pages/parts/admin/ConfigValuesPart"; 5 5 import { ExtensionOverridePart } from "@/pages/parts/admin/ExtensionOverridePart"; 6 + import { M3U8TestPart } from "@/pages/parts/admin/M3U8TestPart"; 6 7 import { RegionSelectorPart } from "@/pages/parts/admin/RegionSelectorPart"; 7 8 import { TMDBTestPart } from "@/pages/parts/admin/TMDBTestPart"; 8 9 import { WorkerTestPart } from "@/pages/parts/admin/WorkerTestPart"; ··· 20 21 <BackendTestPart /> 21 22 <WorkerTestPart /> 22 23 <TMDBTestPart /> 24 + <M3U8TestPart /> 23 25 <ExtensionOverridePart /> 24 26 <RegionSelectorPart /> 25 27 </ThinContainer>
+314
src/pages/parts/admin/M3U8TestPart.tsx
··· 1 + import classNames from "classnames"; 2 + import { useEffect, useMemo, useState } from "react"; 3 + import { useAsyncFn } from "react-use"; 4 + 5 + import { Button } from "@/components/buttons/Button"; 6 + import { Toggle } from "@/components/buttons/Toggle"; 7 + import { Icon, Icons } from "@/components/Icon"; 8 + import { Box } from "@/components/layout/Box"; 9 + import { Divider } from "@/components/utils/Divider"; 10 + import { Heading2 } from "@/components/utils/Text"; 11 + import { getM3U8ProxyUrls } from "@/utils/proxyUrls"; 12 + 13 + export function M3U8ProxyItem(props: { 14 + name: string; 15 + errored?: boolean; 16 + success?: boolean; 17 + questionable?: boolean; 18 + errorText?: string; 19 + url?: string; 20 + enabled?: boolean; 21 + onToggle?: (enabled: boolean) => void; 22 + }) { 23 + const urlWithoutProtocol = props.url ? new URL(props.url).host : null; 24 + 25 + return ( 26 + <div className="flex mb-2"> 27 + <Toggle 28 + enabled={props.enabled} 29 + onClick={() => props.onToggle?.(!props.enabled)} 30 + /> 31 + <Icon 32 + icon={ 33 + props.errored 34 + ? Icons.X 35 + : props.success 36 + ? Icons.CIRCLE_CHECK 37 + : props.questionable 38 + ? Icons.CIRCLE_QUESTION 39 + : Icons.EYE_SLASH 40 + } 41 + className={classNames({ 42 + "text-xl mr-2 mt-0.5 ml-3": true, 43 + "text-video-scraping-error": props.errored, 44 + "text-video-scraping-noresult": 45 + !props.errored && !props.success && !props.questionable, 46 + "text-video-scraping-success": props.success, 47 + "text-yellow-400": props.questionable, 48 + })} 49 + /> 50 + <div className="flex-1"> 51 + <p className="text-white font-bold">{props.name}</p> 52 + {props.errorText ? <p>{props.errorText}</p> : null} 53 + {urlWithoutProtocol ? <p>{urlWithoutProtocol}</p> : null} 54 + </div> 55 + </div> 56 + ); 57 + } 58 + 59 + export function M3U8TestPart() { 60 + const m3u8ProxyList = useMemo(() => { 61 + return getM3U8ProxyUrls().map((v, ind) => ({ 62 + id: ind.toString(), 63 + url: v, 64 + })); 65 + }, []); 66 + 67 + // Load enabled proxies from localStorage 68 + const [enabledProxies, setEnabledProxies] = useState<Record<string, boolean>>( 69 + () => { 70 + const saved = localStorage.getItem("m3u8-proxy-enabled"); 71 + if (saved) { 72 + try { 73 + return JSON.parse(saved); 74 + } catch { 75 + return {}; 76 + } 77 + } 78 + // Default: all enabled 79 + return Object.fromEntries(m3u8ProxyList.map((proxy) => [proxy.id, true])); 80 + }, 81 + ); 82 + 83 + // Save enabled proxies to localStorage 84 + useEffect(() => { 85 + localStorage.setItem("m3u8-proxy-enabled", JSON.stringify(enabledProxies)); 86 + }, [enabledProxies]); 87 + 88 + const [proxyState, setProxyState] = useState< 89 + { 90 + id: string; 91 + status: "error" | "success" | "questionable"; 92 + error?: Error; 93 + }[] 94 + >([]); 95 + 96 + const [buttonClicked, setButtonClicked] = useState(false); 97 + const [buttonDisabled, setButtonDisabled] = useState(false); 98 + 99 + const [testState, runTests] = useAsyncFn(async () => { 100 + setButtonDisabled(true); 101 + function updateProxy(id: string, data: (typeof proxyState)[number]) { 102 + setProxyState((s) => { 103 + return [...s.filter((v) => v.id !== id), data]; 104 + }); 105 + } 106 + setProxyState([]); 107 + 108 + const activeProxies = m3u8ProxyList.filter( 109 + (proxy) => enabledProxies[proxy.id], 110 + ); 111 + const proxyPromises = activeProxies.map(async (proxy) => { 112 + try { 113 + if (proxy.url.endsWith("/")) { 114 + updateProxy(proxy.id, { 115 + id: proxy.id, 116 + status: "error", 117 + error: new Error("URL ends with slash"), 118 + }); 119 + return; 120 + } 121 + 122 + // Test if it can do the same destination fetch as CORS proxy 123 + const testUrl = `${proxy.url}/?destination=${encodeURIComponent( 124 + "https://postman-echo.com/get", 125 + )}`; 126 + const response = await fetch(testUrl); 127 + 128 + if (response.ok) { 129 + updateProxy(proxy.id, { 130 + id: proxy.id, 131 + status: "success", 132 + }); 133 + } 134 + } catch (err) { 135 + const error = err as Error; 136 + error.message = error.message.replace(proxy.url, "M3U8_PROXY_URL"); 137 + updateProxy(proxy.id, { 138 + id: proxy.id, 139 + status: "questionable", 140 + error, 141 + }); 142 + } 143 + }); 144 + 145 + await Promise.all(proxyPromises); 146 + setTimeout(() => setButtonDisabled(false), 5000); 147 + }, [m3u8ProxyList, enabledProxies]); 148 + 149 + const handleToggleProxy = (proxyId: string, enabled: boolean) => { 150 + setEnabledProxies((prev) => ({ 151 + ...prev, 152 + [proxyId]: enabled, 153 + })); 154 + }; 155 + 156 + const allEnabled = m3u8ProxyList.every((proxy) => enabledProxies[proxy.id]); 157 + const noneEnabled = m3u8ProxyList.every((proxy) => !enabledProxies[proxy.id]); 158 + 159 + const handleToggleAll = () => { 160 + if (allEnabled) { 161 + // Disable all 162 + setEnabledProxies( 163 + Object.fromEntries(m3u8ProxyList.map((proxy) => [proxy.id, false])), 164 + ); 165 + } else { 166 + // Enable all 167 + setEnabledProxies( 168 + Object.fromEntries(m3u8ProxyList.map((proxy) => [proxy.id, true])), 169 + ); 170 + } 171 + }; 172 + 173 + const enabledCount = m3u8ProxyList.filter( 174 + (proxy) => enabledProxies[proxy.id], 175 + ).length; 176 + 177 + return ( 178 + <> 179 + <Heading2 className="!mb-0 mt-12">M3U8 Proxy tests</Heading2> 180 + <div className="flex items-center justify-between mb-8 mt-2"> 181 + <p> 182 + {m3u8ProxyList.length} M3U8 proxy(s) registered ({enabledCount}{" "} 183 + enabled) 184 + </p> 185 + <Button 186 + theme="secondary" 187 + onClick={handleToggleAll} 188 + disabled={m3u8ProxyList.length === 0} 189 + > 190 + {allEnabled ? "Disable All" : "Enable All"} 191 + </Button> 192 + </div> 193 + <Box> 194 + {m3u8ProxyList.map((v, i) => { 195 + const s = proxyState.find((segment) => segment.id === v.id); 196 + const name = `M3U8 Proxy ${i + 1}`; 197 + const enabled = enabledProxies[v.id]; 198 + 199 + if (!s) { 200 + return ( 201 + <M3U8ProxyItem 202 + name={name} 203 + key={v.id} 204 + enabled={enabled} 205 + onToggle={(isEnabled) => handleToggleProxy(v.id, isEnabled)} 206 + /> 207 + ); 208 + } 209 + 210 + if (s.status === "error") { 211 + return ( 212 + <M3U8ProxyItem 213 + name={name} 214 + errored 215 + key={v.id} 216 + errorText={s.error?.toString()} 217 + enabled={enabled} 218 + onToggle={(isEnabled) => handleToggleProxy(v.id, isEnabled)} 219 + /> 220 + ); 221 + } 222 + 223 + if (s.status === "success") { 224 + return ( 225 + <M3U8ProxyItem 226 + name={name} 227 + url={v.url} 228 + success 229 + key={v.id} 230 + enabled={enabled} 231 + onToggle={(isEnabled) => handleToggleProxy(v.id, isEnabled)} 232 + /> 233 + ); 234 + } 235 + 236 + if (s.status === "questionable") { 237 + return ( 238 + <M3U8ProxyItem 239 + name={name} 240 + questionable 241 + key={v.id} 242 + errorText={s.error?.toString()} 243 + enabled={enabled} 244 + onToggle={(isEnabled) => handleToggleProxy(v.id, isEnabled)} 245 + /> 246 + ); 247 + } 248 + 249 + return ( 250 + <M3U8ProxyItem 251 + name={name} 252 + key={v.id} 253 + enabled={enabled} 254 + onToggle={(isEnabled) => handleToggleProxy(v.id, isEnabled)} 255 + /> 256 + ); 257 + })} 258 + <Divider /> 259 + <div className="flex justify-end"> 260 + {buttonClicked ? ( 261 + proxyState 262 + .filter((p) => enabledProxies[p.id]) 263 + .every((proxy) => proxy.status === "success") ? ( 264 + <p> 265 + All enabled M3U8 proxies have passed the test!{" "} 266 + <span className="font-bold">٩(ˊᗜˋ*)و♡</span> 267 + </p> 268 + ) : ( 269 + <div> 270 + <div className="text-right"> 271 + <p className="pb-4"> 272 + Some M3U8 proxies have failed the test...{" "} 273 + <span className="font-bold">(•᷄∩•᷅ )</span> 274 + </p> 275 + <div className="flex justify-end"> 276 + <Button 277 + theme="purple" 278 + loading={testState.loading} 279 + onClick={async (event) => { 280 + event.preventDefault(); 281 + setButtonDisabled(true); 282 + await runTests(); 283 + setButtonClicked(true); 284 + setTimeout(() => setButtonDisabled(false), 250); 285 + }} 286 + disabled={buttonDisabled || noneEnabled} 287 + > 288 + Test M3U8 proxies 289 + </Button> 290 + </div> 291 + </div> 292 + </div> 293 + ) 294 + ) : ( 295 + <Button 296 + theme="purple" 297 + loading={testState.loading} 298 + onClick={async (event) => { 299 + event.preventDefault(); 300 + setButtonDisabled(true); 301 + await runTests(); 302 + setButtonClicked(true); 303 + setTimeout(() => setButtonDisabled(false), 5000); 304 + }} 305 + disabled={buttonDisabled || noneEnabled} 306 + > 307 + Test M3U8 proxies 308 + </Button> 309 + )} 310 + </div> 311 + </Box> 312 + </> 313 + ); 314 + }