Spaces:
Running
Running
Fix ROOT CAUSE: segments are PNG-wrapped TS - proxy strips 188-byte PNG header, rewrites m3u8 segment URLs through server
Browse files
main.py
CHANGED
|
@@ -20,8 +20,6 @@ _cache_ttl_live = 60
|
|
| 20 |
_cache_ttl_yt = 1800
|
| 21 |
PRIORITY_LEAGUES = ["Ngoại Hạng Anh","FA Cup","Champions League","LaLiga","Copa del Rey","Serie A","Bundesliga","Ligue 1","V-League"]
|
| 22 |
LEAGUE_IDS = {"nha":27110,"laliga":27233,"seriea":27044,"bundesliga":26891,"ligue1":27212}
|
| 23 |
-
|
| 24 |
-
# ===== HIGHLIGHT LEAGUE CONFIG =====
|
| 25 |
HL_LEAGUES = {
|
| 26 |
"premier-league": {"path": "anh/premier-league", "name": "Premier League", "emoji": "🏴"},
|
| 27 |
"fa-cup": {"path": "anh/fa-cup", "name": "FA Cup", "emoji": "🏆"},
|
|
@@ -32,7 +30,6 @@ HL_LEAGUES = {
|
|
| 32 |
"europa-league": {"path": "cup-chau-au/uefa-europa-league", "name": "Europa League", "emoji": "🟠"},
|
| 33 |
"world-cup": {"path": "the-gioi/world-cup-qualifiers", "name": "World Cup 2026", "emoji": "🌍"},
|
| 34 |
}
|
| 35 |
-
|
| 36 |
def _cached(key, fn, ttl=None):
|
| 37 |
now=time.time();t=ttl or _cache_ttl
|
| 38 |
if key in _cache and now-_cache[key]["t"]<t:return _cache[key]["d"]
|
|
@@ -74,22 +71,52 @@ def _parse_match_from_li(li, status_type="live"):
|
|
| 74 |
league=league_el.get_text(strip=True) if league_el else ""
|
| 75 |
return{"home":home_el.get_text(strip=True),"away":away_el.get_text(strip=True),"score":score or"VS","minute":minute,"league":league,"time":time_el.get_text(strip=True) if time_el else "","event_id":event_id,"home_logo":home_logo.get("src","") if home_logo else "","away_logo":away_logo.get("src","") if away_logo else "","status":status_type}
|
| 76 |
|
| 77 |
-
# ===== VIDEO PROXY (fix CORS) =====
|
| 78 |
@app.get("/api/proxy/m3u8")
|
| 79 |
def proxy_m3u8(url: str = Query(...)):
|
| 80 |
-
"""Proxy m3u8 manifest
|
| 81 |
try:
|
| 82 |
r = requests.get(url, headers=HEADERS, timeout=15)
|
| 83 |
if r.status_code != 200:
|
| 84 |
return Response(status_code=502, content="upstream error")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
return Response(
|
| 86 |
-
content=
|
| 87 |
media_type="application/vnd.apple.mpegurl",
|
| 88 |
headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "public, max-age=300"}
|
| 89 |
)
|
| 90 |
except:
|
| 91 |
return Response(status_code=502, content="proxy error")
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
@app.get("/api/proxy/video")
|
| 94 |
def proxy_video(url: str = Query(...), request: Request = None):
|
| 95 |
"""Proxy MP4/video for sources that block cross-origin (BDP). Supports range requests."""
|
|
@@ -117,7 +144,6 @@ def proxy_video(url: str = Query(...), request: Request = None):
|
|
| 117 |
|
| 118 |
# ===== XEMLAIBONGDA HIGHLIGHTS =====
|
| 119 |
def _scrape_xemlaibongda_page(page_path):
|
| 120 |
-
"""Scrape highlights from a specific xemlaibongda page (league or homepage)."""
|
| 121 |
try:
|
| 122 |
url = f"https://xemlaibongda.top/{page_path}" if page_path else "https://xemlaibongda.top/"
|
| 123 |
r=requests.get(url,headers=HEADERS,timeout=15)
|
|
@@ -147,16 +173,13 @@ def _scrape_xemlaibongda_page(page_path):
|
|
| 147 |
except:return[]
|
| 148 |
|
| 149 |
def scrape_xemlaibongda():
|
| 150 |
-
"""Get all highlight videos from homepage."""
|
| 151 |
return _scrape_xemlaibongda_page("")
|
| 152 |
|
| 153 |
def scrape_highlights_by_league(league_key):
|
| 154 |
-
"""Get highlights for a specific league."""
|
| 155 |
if league_key not in HL_LEAGUES:return[]
|
| 156 |
return _scrape_xemlaibongda_page(HL_LEAGUES[league_key]["path"])
|
| 157 |
|
| 158 |
def scrape_all_league_highlights():
|
| 159 |
-
"""Fetch highlights for all leagues in parallel."""
|
| 160 |
results = {}
|
| 161 |
def _fetch(key):
|
| 162 |
return key, scrape_highlights_by_league(key)
|
|
@@ -165,13 +188,11 @@ def scrape_all_league_highlights():
|
|
| 165 |
for f in as_completed(futs):
|
| 166 |
try:
|
| 167 |
key, vids = f.result()
|
| 168 |
-
if vids:
|
| 169 |
-
results[key] = vids[:6]
|
| 170 |
except:pass
|
| 171 |
return results
|
| 172 |
|
| 173 |
def extract_xemlaibongda_video(url):
|
| 174 |
-
"""Extract HLS m3u8 URL from xemlaibongda article page."""
|
| 175 |
try:
|
| 176 |
r=requests.get(url,headers=HEADERS,timeout=15)
|
| 177 |
if r.status_code!=200:return None
|
|
@@ -282,17 +303,14 @@ def api_highlights():
|
|
| 282 |
return JSONResponse(_cached("xemlaibongda_hl",scrape_xemlaibongda,ttl=_cache_ttl))
|
| 283 |
@app.get("/api/highlights/leagues")
|
| 284 |
def api_highlights_leagues():
|
| 285 |
-
"""Get highlights grouped by league for homepage display."""
|
| 286 |
return JSONResponse(_cached("hl_leagues",scrape_all_league_highlights,ttl=_cache_ttl))
|
| 287 |
@app.get("/api/highlights/{league}")
|
| 288 |
def api_highlights_league(league:str):
|
| 289 |
-
"""Get highlights for a specific league."""
|
| 290 |
if league not in HL_LEAGUES:
|
| 291 |
return JSONResponse({"error":"league not found"})
|
| 292 |
return JSONResponse(_cached(f"hl_{league}",lambda:scrape_highlights_by_league(league),ttl=_cache_ttl))
|
| 293 |
@app.get("/api/highlights_config")
|
| 294 |
def api_highlights_config():
|
| 295 |
-
"""Return league config for frontend."""
|
| 296 |
return JSONResponse(HL_LEAGUES)
|
| 297 |
@app.get("/api/video_url")
|
| 298 |
def api_video_url(url:str=Query(...)):
|
|
@@ -340,7 +358,7 @@ def api_bdp_videos():
|
|
| 340 |
return arts[:20]
|
| 341 |
except:return[]
|
| 342 |
return JSONResponse(_cached("bdp_videos",_f))
|
| 343 |
-
# ===== NEWS =====
|
| 344 |
def scrape_vne(cat_url):
|
| 345 |
try:
|
| 346 |
soup=_get(cat_url);arts=[]
|
|
|
|
| 20 |
_cache_ttl_yt = 1800
|
| 21 |
PRIORITY_LEAGUES = ["Ngoại Hạng Anh","FA Cup","Champions League","LaLiga","Copa del Rey","Serie A","Bundesliga","Ligue 1","V-League"]
|
| 22 |
LEAGUE_IDS = {"nha":27110,"laliga":27233,"seriea":27044,"bundesliga":26891,"ligue1":27212}
|
|
|
|
|
|
|
| 23 |
HL_LEAGUES = {
|
| 24 |
"premier-league": {"path": "anh/premier-league", "name": "Premier League", "emoji": "🏴"},
|
| 25 |
"fa-cup": {"path": "anh/fa-cup", "name": "FA Cup", "emoji": "🏆"},
|
|
|
|
| 30 |
"europa-league": {"path": "cup-chau-au/uefa-europa-league", "name": "Europa League", "emoji": "🟠"},
|
| 31 |
"world-cup": {"path": "the-gioi/world-cup-qualifiers", "name": "World Cup 2026", "emoji": "🌍"},
|
| 32 |
}
|
|
|
|
| 33 |
def _cached(key, fn, ttl=None):
|
| 34 |
now=time.time();t=ttl or _cache_ttl
|
| 35 |
if key in _cache and now-_cache[key]["t"]<t:return _cache[key]["d"]
|
|
|
|
| 71 |
league=league_el.get_text(strip=True) if league_el else ""
|
| 72 |
return{"home":home_el.get_text(strip=True),"away":away_el.get_text(strip=True),"score":score or"VS","minute":minute,"league":league,"time":time_el.get_text(strip=True) if time_el else "","event_id":event_id,"home_logo":home_logo.get("src","") if home_logo else "","away_logo":away_logo.get("src","") if away_logo else "","status":status_type}
|
| 73 |
|
| 74 |
+
# ===== VIDEO PROXY (fix CORS + PNG-wrapped TS segments) =====
|
| 75 |
@app.get("/api/proxy/m3u8")
|
| 76 |
def proxy_m3u8(url: str = Query(...)):
|
| 77 |
+
"""Proxy m3u8 manifest: add CORS + rewrite segment URLs to go through /api/proxy/seg"""
|
| 78 |
try:
|
| 79 |
r = requests.get(url, headers=HEADERS, timeout=15)
|
| 80 |
if r.status_code != 200:
|
| 81 |
return Response(status_code=502, content="upstream error")
|
| 82 |
+
# Rewrite segment URLs to proxy through our server (strips PNG header)
|
| 83 |
+
lines = r.text.strip().split('\n')
|
| 84 |
+
rewritten = []
|
| 85 |
+
for line in lines:
|
| 86 |
+
if line.startswith('#') or not line.strip():
|
| 87 |
+
rewritten.append(line)
|
| 88 |
+
else:
|
| 89 |
+
# This is a segment URL - rewrite to our proxy
|
| 90 |
+
rewritten.append("/api/proxy/seg?url=" + quote(line.strip(), safe=""))
|
| 91 |
+
content = '\n'.join(rewritten)
|
| 92 |
return Response(
|
| 93 |
+
content=content.encode('utf-8'),
|
| 94 |
media_type="application/vnd.apple.mpegurl",
|
| 95 |
headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "public, max-age=300"}
|
| 96 |
)
|
| 97 |
except:
|
| 98 |
return Response(status_code=502, content="proxy error")
|
| 99 |
|
| 100 |
+
@app.get("/api/proxy/seg")
|
| 101 |
+
def proxy_segment(url: str = Query(...)):
|
| 102 |
+
"""Proxy HLS segment: strips 188-byte PNG header wrapper, returns clean MPEG-TS."""
|
| 103 |
+
try:
|
| 104 |
+
r = requests.get(url, headers=HEADERS, timeout=30)
|
| 105 |
+
if r.status_code != 200:
|
| 106 |
+
return Response(status_code=502, content="upstream error")
|
| 107 |
+
data = r.content
|
| 108 |
+
# TikTok CDN wraps TS in PNG: first 188 bytes = 1x1 PNG, rest = MPEG-TS
|
| 109 |
+
# Detect and strip: check if starts with PNG magic + byte 188 is 0x47 (TS sync)
|
| 110 |
+
if len(data) > 188 and data[0:4] == b'\x89PNG' and data[188] == 0x47:
|
| 111 |
+
data = data[188:]
|
| 112 |
+
return Response(
|
| 113 |
+
content=data,
|
| 114 |
+
media_type="video/mp2t",
|
| 115 |
+
headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "public, max-age=3600"}
|
| 116 |
+
)
|
| 117 |
+
except:
|
| 118 |
+
return Response(status_code=502, content="proxy error")
|
| 119 |
+
|
| 120 |
@app.get("/api/proxy/video")
|
| 121 |
def proxy_video(url: str = Query(...), request: Request = None):
|
| 122 |
"""Proxy MP4/video for sources that block cross-origin (BDP). Supports range requests."""
|
|
|
|
| 144 |
|
| 145 |
# ===== XEMLAIBONGDA HIGHLIGHTS =====
|
| 146 |
def _scrape_xemlaibongda_page(page_path):
|
|
|
|
| 147 |
try:
|
| 148 |
url = f"https://xemlaibongda.top/{page_path}" if page_path else "https://xemlaibongda.top/"
|
| 149 |
r=requests.get(url,headers=HEADERS,timeout=15)
|
|
|
|
| 173 |
except:return[]
|
| 174 |
|
| 175 |
def scrape_xemlaibongda():
|
|
|
|
| 176 |
return _scrape_xemlaibongda_page("")
|
| 177 |
|
| 178 |
def scrape_highlights_by_league(league_key):
|
|
|
|
| 179 |
if league_key not in HL_LEAGUES:return[]
|
| 180 |
return _scrape_xemlaibongda_page(HL_LEAGUES[league_key]["path"])
|
| 181 |
|
| 182 |
def scrape_all_league_highlights():
|
|
|
|
| 183 |
results = {}
|
| 184 |
def _fetch(key):
|
| 185 |
return key, scrape_highlights_by_league(key)
|
|
|
|
| 188 |
for f in as_completed(futs):
|
| 189 |
try:
|
| 190 |
key, vids = f.result()
|
| 191 |
+
if vids:results[key] = vids[:6]
|
|
|
|
| 192 |
except:pass
|
| 193 |
return results
|
| 194 |
|
| 195 |
def extract_xemlaibongda_video(url):
|
|
|
|
| 196 |
try:
|
| 197 |
r=requests.get(url,headers=HEADERS,timeout=15)
|
| 198 |
if r.status_code!=200:return None
|
|
|
|
| 303 |
return JSONResponse(_cached("xemlaibongda_hl",scrape_xemlaibongda,ttl=_cache_ttl))
|
| 304 |
@app.get("/api/highlights/leagues")
|
| 305 |
def api_highlights_leagues():
|
|
|
|
| 306 |
return JSONResponse(_cached("hl_leagues",scrape_all_league_highlights,ttl=_cache_ttl))
|
| 307 |
@app.get("/api/highlights/{league}")
|
| 308 |
def api_highlights_league(league:str):
|
|
|
|
| 309 |
if league not in HL_LEAGUES:
|
| 310 |
return JSONResponse({"error":"league not found"})
|
| 311 |
return JSONResponse(_cached(f"hl_{league}",lambda:scrape_highlights_by_league(league),ttl=_cache_ttl))
|
| 312 |
@app.get("/api/highlights_config")
|
| 313 |
def api_highlights_config():
|
|
|
|
| 314 |
return JSONResponse(HL_LEAGUES)
|
| 315 |
@app.get("/api/video_url")
|
| 316 |
def api_video_url(url:str=Query(...)):
|
|
|
|
| 358 |
return arts[:20]
|
| 359 |
except:return[]
|
| 360 |
return JSONResponse(_cached("bdp_videos",_f))
|
| 361 |
+
# ===== NEWS (unchanged) =====
|
| 362 |
def scrape_vne(cat_url):
|
| 363 |
try:
|
| 364 |
soup=_get(cat_url);arts=[]
|