bep40 commited on
Commit
06a7c10
·
verified ·
1 Parent(s): 6ec7529

Fix ROOT CAUSE: segments are PNG-wrapped TS - proxy strips 188-byte PNG header, rewrites m3u8 segment URLs through server

Browse files
Files changed (1) hide show
  1. main.py +35 -17
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 to add CORS headers. Segments have CORS already."""
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=r.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=[]