CoolFace
Apppublic

amdakil/beebee

sourceHugging Faceapache-2.0updated 7mo agoView on Hugging Face
0likes
server.ts375 linesDownload Raw Back to root
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});