amdakil/beebee
0
1// server.ts2// TNT-Sports-1 Proxy untuk Hugging Face Spaces (Deno Docker)3// Output: { "player": "https://...", "link": "https://..." }4 5import { serve } from "https://deno.land/std@0.224.0/http/server.ts";6 7// ────────────────────────────────────────8// Konfigurasi9// ────────────────────────────────────────10 11const BEESPORT = "https://beesport.global";12const CHANNEL = "TNT-Sports-1";13const PORT = 8000; // Fix port untuk HF Spaces Docker14 15const CACHE_TTL = 30_000; // 30 detik16const SESSION_TTL = 4 * 60_000; // 4 menit17const UA_ROTATE_TTL = 10_000; // 10 detik18const MAX_RETRIES = 3;19const MIN_RETRY_DELAY = 3000;20const MAX_RETRY_DELAY = 8000;21 22// UA Pool (2026-friendly)23const UA_POOL = [24 {25 ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",26 ch: `"Google Chrome";v="134", "Chromium";v="134", "Not=A?Brand";v="24"`,27 platform: "Windows",28 mobile: "?0",29 },30 {31 ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0",32 ch: `"Not)A;Brand";v="99", "Chromium";v="134"`,33 platform: "Windows",34 mobile: "?0",35 },36 {37 ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 15_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",38 ch: `"Safari";v="18", "Not_A Brand";v="99"`,39 platform: "macOS",40 mobile: "?0",41 },42];43 44// Cache45const tokenCache = new Map<string, { token: string; serverUrl: string; time: number }>();46const sessionCache = new Map<string, { cookies: string; xsrf: string; time: number; failCount?: number }>();47const uaCache = new Map<string, { profile: typeof UA_POOL[number]; time: number }>();48 49// ────────────────────────────────────────50// Utilitas51// ────────────────────────────────────────52 53function getUA(forceNew = false) {54 const now = Date.now();55 let cached = uaCache.get(CHANNEL);56 57 if (!forceNew && cached && now - cached.time < UA_ROTATE_TTL) {58 return cached.profile;59 }60 61 const exclude = cached?.profile.ua;62 const pool = exclude ? UA_POOL.filter(p => p.ua !== exclude) : UA_POOL;63 const profile = pool[Math.floor(Math.random() * pool.length)] ?? UA_POOL[0];64 65 uaCache.set(CHANNEL, { profile, time: now });66 return profile;67}68 69function getHeaders(type: "html" | "api" = "api") {70 const p = getUA();71 const headers: Record<string, string> = {72 "User-Agent": p.ua,73 "Accept-Language": "en-US,en;q=0.9,id;q=0.8",74 "Accept-Encoding": "gzip, deflate, br",75 "Connection": "keep-alive",76 };77 78 if (p.ch) {79 headers["sec-ch-ua"] = p.ch;80 headers["sec-ch-ua-mobile"] = p.mobile;81 headers["sec-ch-ua-platform"] = `"${p.platform}"`;82 headers["sec-ch-ua-full-version-list"] = p.ch;83 }84 85 if (type === "html") {86 headers.Accept = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8";87 headers["Sec-Fetch-Dest"] = "document";88 headers["Sec-Fetch-Mode"] = "navigate";89 headers["Sec-Fetch-Site"] = "none";90 headers["Sec-Fetch-User"] = "?1";91 headers["Upgrade-Insecure-Requests"] = "1";92 } else {93 headers.Accept = "application/json, text/plain, */*";94 headers["Sec-Fetch-Dest"] = "empty";95 headers["Sec-Fetch-Mode"] = "cors";96 headers["Sec-Fetch-Site"] = "same-origin";97 }98 99 return headers;100}101 102// ────────────────────────────────────────103// Session + XSRF104// ────────────────────────────────────────105 106async function getSession() {107 const now = Date.now();108 let session = sessionCache.get(CHANNEL);109 110 if (session && now - session.time < SESSION_TTL && (session.failCount ?? 0) < 3) {111 console.log("Session cache HIT");112 return { cookies: session.cookies, xsrf: session.xsrf };113 }114 115 await new Promise(r => setTimeout(r, 1200 + Math.random() * 2800)); // jitter116 117 if ((session?.failCount ?? 0) >= 2) getUA(true);118 119 const headers = getHeaders("html");120 121 let res;122 try {123 res = await fetch(`${BEESPORT}/live-tv`, {124 headers,125 redirect: "follow",126 signal: AbortSignal.timeout(15000),127 });128 } catch (err) {129 console.error("Fetch /live-tv failed:", err);130 throw new Error("NETWORK_ERROR");131 }132 133 if (!res.ok) {134 if ([429, 403, 418, 419, 503].includes(res.status)) throw new Error("RATE_LIMITED");135 throw new Error(`Session HTTP ${res.status}`);136 }137 138 const setCookies = res.headers.getSetCookie() ?? [];139 let xsrfCookie = setCookies.find(c => /XSRF-TOKEN/i.test(c)) ?? setCookies.find(c => /xsrf/i.test(c));140 141 if (!xsrfCookie) {142 const text = await res.text();143 const match = text.match(/name=["']_token["']\s+value=["']([^"']+)["']/i) ||144 text.match(/xsrf-token["']\s*:\s*["']([^"']+)["']/i);145 if (match?.[1]) xsrfCookie = `XSRF-TOKEN=${encodeURIComponent(match[1])};`;146 }147 148 if (!xsrfCookie) throw new Error("XSRF token not found");149 150 const xsrfMatch = xsrfCookie.match(/=(.*?)(?:;|$)/);151 if (!xsrfMatch) throw new Error("Invalid XSRF format");152 153 const xsrf = decodeURIComponent(xsrfMatch[1]);154 const cookies = setCookies.map(c => c.split(";")[0]).filter(Boolean).join("; ");155 156 sessionCache.set(CHANNEL, { cookies, xsrf, time: now, failCount: 0 });157 console.log("New session cached");158 return { cookies, xsrf };159}160 161// ────────────────────────────────────────162// Authorize163// ────────────────────────────────────────164 165async function authorize() {166 let session;167 try {168 session = await getSession();169 } catch (err) {170 const e = err as Error;171 if (e.message.includes("XSRF") || e.message === "NETWORK_ERROR") {172 const s = sessionCache.get(CHANNEL);173 if (s) {174 s.failCount = (s.failCount ?? 0) + 1;175 sessionCache.set(CHANNEL, s);176 }177 }178 throw err;179 }180 181 const { cookies, xsrf } = session;182 183 const payload = {184 channel: `https://pri8ahnsass.twinspeed.space/${CHANNEL}/index.m3u8`,185 };186 187 const body = JSON.stringify(payload);188 const apiHeaders = getHeaders("api");189 190 const headers = {191 ...apiHeaders,192 "Content-Type": "application/json",193 "Content-Length": body.length.toString(),194 "Origin": BEESPORT,195 "Referer": `${BEESPORT}/live-tv`,196 "X-XSRF-TOKEN": xsrf,197 "X-Requested-With": "XMLHttpRequest",198 "Cookie": cookies,199 };200 201 console.log(`Authorizing ${CHANNEL}...`);202 203 let res;204 try {205 res = await fetch(`${BEESPORT}/authorize-channel`, {206 method: "POST",207 headers,208 body,209 signal: AbortSignal.timeout(20000),210 });211 } catch (err) {212 console.error("Authorize fetch failed:", err);213 throw new Error("NETWORK_ERROR");214 }215 216 if (!res.ok) {217 const text = await res.text().catch(() => "");218 if ([419, 429, 403, 503].includes(res.status) || /limited|rate|block/i.test(text)) {219 throw new Error("RATE_LIMITED");220 }221 throw new Error(`Authorize failed: ${res.status} - ${text.substring(0, 100)}`);222 }223 224 let data;225 try {226 data = await res.json();227 } catch {228 throw new Error("Invalid JSON from authorize");229 }230 231 const serverUrl = data?.server;232 if (!serverUrl) throw new Error("No server URL in response");233 234 // Token extraction (untuk validasi)235 const tokenPatterns = [236 /fastoken=([a-f0-9\-]+?n?)(?:&|$)/i,237 /fastoken=([a-f0-9\-]+)/i,238 /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i,239 ];240 241 let token = "";242 for (const pat of tokenPatterns) {243 const m = serverUrl.match(pat);244 if (m?.[1]) {245 token = m[1];246 break;247 }248 }249 250 if (!token) throw new Error("No token found in server URL");251 252 return { token, serverUrl };253}254 255async function authorizeWithRetry() {256 let lastError: Error | undefined;257 258 for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {259 try {260 return await authorize();261 } catch (err) {262 lastError = err as Error;263 console.log(`Attempt ${attempt + 1}/${MAX_RETRIES + 1} failed: ${lastError.message}`);264 265 if (lastError.message === "RATE_LIMITED") throw lastError;266 267 if (attempt >= 1) {268 getUA(true);269 sessionCache.delete(CHANNEL);270 }271 272 if (attempt < MAX_RETRIES) {273 const delay = MIN_RETRY_DELAY + Math.random() * (MAX_RETRY_DELAY - MIN_RETRY_DELAY);274 console.log(`Retry in ~${Math.round(delay / 1000)}s...`);275 await new Promise(r => setTimeout(r, delay));276 }277 }278 }279 280 throw lastError!;281}282 283async function getData() {284 const now = Date.now();285 const cached = tokenCache.get(CHANNEL);286 287 if (cached?.token && now - cached.time < CACHE_TTL) {288 console.log("Token cache HIT");289 return { token: cached.token, serverUrl: cached.serverUrl };290 }291 292 const result = await authorizeWithRetry();293 tokenCache.set(CHANNEL, { token: result.token, serverUrl: result.serverUrl, time: now });294 console.log(`Token cached: ${result.token.substring(0, 12)}...`);295 return result;296}297 298// ────────────────────────────────────────299// Server handler300// ────────────────────────────────────────301 302serve(async (req) => {303 const url = new URL(req.url);304 console.log(`Request: ${req.method} ${url.pathname}`);305 306 if (url.pathname !== "/") {307 return new Response("Not Found\nOnly / supported", { status: 404 });308 }309 310 if (req.method !== "GET") {311 return new Response("Method Not Allowed", { status: 405 });312 }313 314 try {315 const { serverUrl } = await getData();316 317 const playerMatch = serverUrl.match(/^(https:\/\/[^/]+\/)/i);318 const player = playerMatch ? playerMatch[1] : null;319 320 const linkParamMatch = serverUrl.match(/link=(https:\/\/[^&]+)/i);321 const linkFull = linkParamMatch ? decodeURIComponent(linkParamMatch[1]) : null;322 const linkMatch = linkFull ? linkFull.match(/^(https:\/\/[^/]+\/)/i) : null;323 const link = linkMatch ? linkMatch[1] : null;324 325 if (!player || !link) {326 throw new Error("Gagal parse host dari server URL");327 }328 329 const response = { player, link };330 331 return new Response(JSON.stringify(response, null, 2), {332 status: 200,333 headers: {334 "Content-Type": "application/json",335 "Access-Control-Allow-Origin": "*",336 "Cache-Control": "no-store, no-cache",337 },338 });339 } catch (err) {340 const e = err as Error;341 console.error("Error:", e.message);342 343 let status = 500;344 let msg = "Internal server error - coba lagi";345 346 if (e.message === "RATE_LIMITED") {347 status = 429;348 msg = "Rate limited - tunggu 10-20 menit";349 } else if (e.message.includes("XSRF") || e.message.includes("token") || e.message.includes("parse")) {350 status = 503;351 msg = "Gagal ambil data server - mungkin maintenance atau format berubah";352 }353 354 return new Response(JSON.stringify({ error: msg }), {355 status,356 headers: { "Content-Type": "application/json" },357 });358 }359}, {360 port: PORT,361 onListen({ hostname, port }) {362 console.log(`363TNT-Sports-1 Proxy - HF Spaces Edition364--------------------------------------365Channel : ${CHANNEL}366Endpoint : GET /367Output : { "player": "...", "link": "..." }368Port : ${port}369Session : ~4 menit370Token : ~30 detik371 372Server running on http://${hostname}:${port}373`);374 },375});