Spaces:
Running
Running
Update app_v2_entry.py
Browse files- app_v2_entry.py +46 -28
app_v2_entry.py
CHANGED
|
@@ -17,7 +17,12 @@ import re, html as html_lib, json, threading, time
|
|
| 17 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 18 |
from urllib.parse import quote
|
| 19 |
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
|
| 23 |
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/' and hasattr(r,'methods') and 'GET' in getattr(r,'methods',set()))]
|
|
@@ -26,27 +31,55 @@ app.router.routes=[r for r in app.router.routes if not isinstance(r, Mount)]
|
|
| 26 |
|
| 27 |
def _clean(s): return re.sub(r"\s+"," ",html_lib.unescape(str(s or""))).strip()
|
| 28 |
|
| 29 |
-
# Cache for match details (5 min TTL)
|
| 30 |
_match_cache = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
# === FAST BONGDA PROXY ENDPOINT ===
|
| 33 |
def _get_match_detail(event_id, slug=None):
|
| 34 |
-
"""Internal function to scrape match detail from bongda.com.vn"""
|
| 35 |
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html", "Referer": "https://bongda.com.vn/"}
|
| 36 |
-
|
| 37 |
if slug:
|
| 38 |
url = f"https://bongda.com.vn/tran-dau/{event_id}/centre/{slug}"
|
| 39 |
else:
|
| 40 |
url = f"https://bongda.com.vn/tran-dau/{event_id}"
|
| 41 |
-
|
| 42 |
resp = req.get(url, headers=headers, timeout=15, allow_redirects=True)
|
| 43 |
if resp.status_code != 200:
|
| 44 |
return None
|
| 45 |
-
|
| 46 |
soup = BeautifulSoup(resp.text, 'html.parser')
|
| 47 |
result = {"event_id": event_id, "found": False, "sections": []}
|
| 48 |
info = {}
|
| 49 |
-
|
| 50 |
tel = soup.select_one('.teams')
|
| 51 |
if tel:
|
| 52 |
he = tel.select_one('.team.home')
|
|
@@ -68,27 +101,22 @@ def _get_match_detail(event_id, slug=None):
|
|
| 68 |
if len(parts) >= 2: info['score'] = f"{parts[0]} - {parts[1]}"
|
| 69 |
lb = sc.select_one('.label')
|
| 70 |
if lb: info['status_label'] = _clean(lb.get_text())
|
| 71 |
-
|
| 72 |
if info.get('home_team') and info.get('away_team'):
|
| 73 |
result['info'] = info
|
| 74 |
result['found'] = True
|
| 75 |
result['sections'].append('info')
|
| 76 |
-
|
| 77 |
events = []
|
| 78 |
for ev in soup.select('.events .period .event'):
|
| 79 |
ev_cls = ' '.join(ev.get('class', []))
|
| 80 |
ev_data = {'team': 'home' if 'home' in ev_cls else 'away', 'period': '', 'type': 'unknown', 'time': '', 'players': ''}
|
| 81 |
-
|
| 82 |
parent = ev.parent
|
| 83 |
if parent:
|
| 84 |
h2 = parent.find('h2')
|
| 85 |
if h2: ev_data['period'] = _clean(h2.get_text())
|
| 86 |
-
|
| 87 |
if ev.select_one('[class*="goal"]'): ev_data['type'] = 'goal'
|
| 88 |
elif ev.select_one('[class*="redcard"]'): ev_data['type'] = 'redcard'
|
| 89 |
elif ev.select_one('[class*="yellowcard"]'): ev_data['type'] = 'yellowcard'
|
| 90 |
elif ev.select_one('[class*="substitution"]'): ev_data['type'] = 'substitution'
|
| 91 |
-
|
| 92 |
players_el = ev.select_one('.players')
|
| 93 |
if players_el:
|
| 94 |
pl_text = _clean(players_el.get_text(' ', strip=True))
|
|
@@ -99,11 +127,9 @@ def _get_match_detail(event_id, slug=None):
|
|
| 99 |
else:
|
| 100 |
ev_data['players'] = pl_text
|
| 101 |
events.append(ev_data)
|
| 102 |
-
|
| 103 |
if events:
|
| 104 |
result['events'] = events
|
| 105 |
result['sections'].append('events')
|
| 106 |
-
|
| 107 |
pred = soup.select_one('.prediction-card')
|
| 108 |
if pred:
|
| 109 |
team_info = pred.select_one('.team-info')
|
|
@@ -118,7 +144,6 @@ def _get_match_detail(event_id, slug=None):
|
|
| 118 |
vc = pred.select_one('.vote-count')
|
| 119 |
if vc: pred_data['vote_count'] = _clean(vc.get_text())
|
| 120 |
result['prediction'] = pred_data
|
| 121 |
-
|
| 122 |
recent = []
|
| 123 |
ml = soup.select_one('.matches-list')
|
| 124 |
if ml:
|
|
@@ -133,7 +158,6 @@ def _get_match_detail(event_id, slug=None):
|
|
| 133 |
if recent:
|
| 134 |
result['recent_matches'] = recent
|
| 135 |
result['sections'].append('recent')
|
| 136 |
-
|
| 137 |
try:
|
| 138 |
api_h = {"User-Agent": "Mozilla/5.0", "Accept": "application/json", "X-Requested-With": "XMLHttpRequest", "Referer": "https://bongda.com.vn/"}
|
| 139 |
ar = req.get(f"https://bongda.com.vn/api/fixtures/h2h-stats?event_id={event_id}", headers=api_h, timeout=10)
|
|
@@ -151,20 +175,17 @@ def _get_match_detail(event_id, slug=None):
|
|
| 151 |
result['h2h_stats_parsed'] = ast
|
| 152 |
result['sections'].append('h2h_stats')
|
| 153 |
except: pass
|
| 154 |
-
|
| 155 |
return result
|
| 156 |
|
| 157 |
@app.get('/api/proxy/bongda')
|
| 158 |
def proxy_bongda(event_id: int = Query(default=None), slug: str = Query(default=None)):
|
| 159 |
if event_id is None:
|
| 160 |
return JSONResponse({'error': 'event_id required'}, status_code=400)
|
| 161 |
-
|
| 162 |
cache_key = f"{event_id}_{slug}"
|
| 163 |
now = time.time()
|
| 164 |
cached = _match_cache.get(cache_key)
|
| 165 |
if cached and now - cached.get('_ts', 0) < 300:
|
| 166 |
return JSONResponse(cached)
|
| 167 |
-
|
| 168 |
try:
|
| 169 |
result = _get_match_detail(event_id, slug)
|
| 170 |
if result:
|
|
@@ -175,26 +196,21 @@ def proxy_bongda(event_id: int = Query(default=None), slug: str = Query(default=
|
|
| 175 |
err = {"event_id": event_id, "found": False, "error": str(e), "_ts": now}
|
| 176 |
_match_cache[cache_key] = err
|
| 177 |
return JSONResponse(err)
|
| 178 |
-
|
| 179 |
return JSONResponse({"event_id": event_id, "found": False})
|
| 180 |
|
| 181 |
@app.get('/api/match/{event_id}/detail')
|
| 182 |
def api_match_detail(event_id: int, url: str = Query(default=None)):
|
| 183 |
-
# Try to extract slug from url if provided
|
| 184 |
slug = None
|
| 185 |
if url:
|
| 186 |
m = re.match(r'.+/tran-dau/\d+/(?:centre|preview)/(.+)', url)
|
| 187 |
if m:
|
| 188 |
slug = m.group(1)
|
| 189 |
-
|
| 190 |
cache_key = f"{event_id}_{slug or ''}"
|
| 191 |
now = time.time()
|
| 192 |
cached = _match_cache.get(cache_key)
|
| 193 |
if cached and now - cached.get('_ts', 0) < 300:
|
| 194 |
return JSONResponse(cached)
|
| 195 |
-
|
| 196 |
try:
|
| 197 |
-
# If no slug, try to find it from homepage
|
| 198 |
if not slug:
|
| 199 |
try:
|
| 200 |
home_r = req.get("https://bongda.com.vn/", headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
|
|
@@ -208,7 +224,6 @@ def api_match_detail(event_id: int, url: str = Query(default=None)):
|
|
| 208 |
cache_key = f"{event_id}_{slug}"
|
| 209 |
break
|
| 210 |
except: pass
|
| 211 |
-
|
| 212 |
result = _get_match_detail(event_id, slug)
|
| 213 |
if result:
|
| 214 |
result['_ts'] = now
|
|
@@ -218,10 +233,13 @@ def api_match_detail(event_id: int, url: str = Query(default=None)):
|
|
| 218 |
err = {"event_id": event_id, "found": False, "error": str(e), "_ts": now}
|
| 219 |
_match_cache[cache_key] = err
|
| 220 |
return JSONResponse(err)
|
| 221 |
-
|
| 222 |
return JSONResponse({"event_id": event_id, "found": False})
|
| 223 |
|
| 224 |
-
# ===
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
_STOP=set('và của các những một được trong với cho tại sau trước khi không người việt nam hôm nay mới nhất nóng tin tức cập nhật theo từ đến là có thì này đã để'.split())
|
| 226 |
|
| 227 |
def _has_kw(topic,title):
|
|
@@ -498,4 +516,4 @@ def _bg():
|
|
| 498 |
time.sleep(90)
|
| 499 |
threading.Thread(target=_bg,daemon=True).start()
|
| 500 |
|
| 501 |
-
app.mount('/static',StaticFiles(directory=STATIC_DIR),name='vnews_static')
|
|
|
|
| 17 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 18 |
from urllib.parse import quote
|
| 19 |
|
| 20 |
+
# Update world-cup path to the actual xemlaibongda page (not qualifiers)
|
| 21 |
+
HL_LEAGUES['world-cup'] = {"path": "the-gioi/world-cup", "name": "World Cup", "emoji": "🌍"}
|
| 22 |
+
|
| 23 |
+
# Add friendly back
|
| 24 |
+
if 'friendly' not in HL_LEAGUES:
|
| 25 |
+
HL_LEAGUES['friendly'] = {"path": "giai-khac/friendly", "name": "Giao hữu", "emoji": "🤝"}
|
| 26 |
|
| 27 |
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
|
| 28 |
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/' and hasattr(r,'methods') and 'GET' in getattr(r,'methods',set()))]
|
|
|
|
| 31 |
|
| 32 |
def _clean(s): return re.sub(r"\s+"," ",html_lib.unescape(str(s or""))).strip()
|
| 33 |
|
|
|
|
| 34 |
_match_cache = {}
|
| 35 |
+
_vtvnambo_cache = {'t': 0, 'd': []}
|
| 36 |
+
_VTVNAMBO_CACHE_TTL = 1800 # 30 min
|
| 37 |
+
|
| 38 |
+
def _yt_channel_shorts_fast(channel, count=15):
|
| 39 |
+
"""Fast scrape YouTube shorts without yt-dlp."""
|
| 40 |
+
try:
|
| 41 |
+
url=f"https://www.youtube.com/@{channel}/shorts"
|
| 42 |
+
r=req.get(url,headers={**HEADERS,"Accept-Language":"vi,en;q=0.8"},timeout=15)
|
| 43 |
+
if r.status_code!=200:return[]
|
| 44 |
+
html=r.text
|
| 45 |
+
ids=[];items=[]
|
| 46 |
+
for m in re.finditer(r'"videoId":"([A-Za-z0-9_-]{11})"',html):
|
| 47 |
+
vid=m.group(1)
|
| 48 |
+
if vid in ids:continue
|
| 49 |
+
ids.append(vid)
|
| 50 |
+
snip=html[max(0,m.start()-900):m.start()+1600]
|
| 51 |
+
title=""
|
| 52 |
+
mt=re.search(r'"title":\{"runs":\[\{"text":"([^"]+)"',snip)
|
| 53 |
+
if mt:title=html_lib.unescape(mt.group(1)).replace('\n',' ').strip()
|
| 54 |
+
if not title:title="YouTube Short"
|
| 55 |
+
items.append({"title":title,"link":f"https://www.youtube.com/watch?v={vid}","img":f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg","source":"yt","id":vid,"channel":channel})
|
| 56 |
+
if len(items)>=count:break
|
| 57 |
+
return items
|
| 58 |
+
except:return[]
|
| 59 |
+
|
| 60 |
+
def _get_vtvnambo_shorts():
|
| 61 |
+
"""Cached VTVNamBo shorts."""
|
| 62 |
+
now = time.time()
|
| 63 |
+
if _vtvnambo_cache['d'] and now - _vtvnambo_cache['t'] < _VTVNAMBO_CACHE_TTL:
|
| 64 |
+
return _vtvnambo_cache['d']
|
| 65 |
+
items = _yt_channel_shorts_fast("vtvnambo", 24)
|
| 66 |
+
_vtvnambo_cache['t'] = now
|
| 67 |
+
_vtvnambo_cache['d'] = items
|
| 68 |
+
return items
|
| 69 |
|
| 70 |
# === FAST BONGDA PROXY ENDPOINT ===
|
| 71 |
def _get_match_detail(event_id, slug=None):
|
|
|
|
| 72 |
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html", "Referer": "https://bongda.com.vn/"}
|
|
|
|
| 73 |
if slug:
|
| 74 |
url = f"https://bongda.com.vn/tran-dau/{event_id}/centre/{slug}"
|
| 75 |
else:
|
| 76 |
url = f"https://bongda.com.vn/tran-dau/{event_id}"
|
|
|
|
| 77 |
resp = req.get(url, headers=headers, timeout=15, allow_redirects=True)
|
| 78 |
if resp.status_code != 200:
|
| 79 |
return None
|
|
|
|
| 80 |
soup = BeautifulSoup(resp.text, 'html.parser')
|
| 81 |
result = {"event_id": event_id, "found": False, "sections": []}
|
| 82 |
info = {}
|
|
|
|
| 83 |
tel = soup.select_one('.teams')
|
| 84 |
if tel:
|
| 85 |
he = tel.select_one('.team.home')
|
|
|
|
| 101 |
if len(parts) >= 2: info['score'] = f"{parts[0]} - {parts[1]}"
|
| 102 |
lb = sc.select_one('.label')
|
| 103 |
if lb: info['status_label'] = _clean(lb.get_text())
|
|
|
|
| 104 |
if info.get('home_team') and info.get('away_team'):
|
| 105 |
result['info'] = info
|
| 106 |
result['found'] = True
|
| 107 |
result['sections'].append('info')
|
|
|
|
| 108 |
events = []
|
| 109 |
for ev in soup.select('.events .period .event'):
|
| 110 |
ev_cls = ' '.join(ev.get('class', []))
|
| 111 |
ev_data = {'team': 'home' if 'home' in ev_cls else 'away', 'period': '', 'type': 'unknown', 'time': '', 'players': ''}
|
|
|
|
| 112 |
parent = ev.parent
|
| 113 |
if parent:
|
| 114 |
h2 = parent.find('h2')
|
| 115 |
if h2: ev_data['period'] = _clean(h2.get_text())
|
|
|
|
| 116 |
if ev.select_one('[class*="goal"]'): ev_data['type'] = 'goal'
|
| 117 |
elif ev.select_one('[class*="redcard"]'): ev_data['type'] = 'redcard'
|
| 118 |
elif ev.select_one('[class*="yellowcard"]'): ev_data['type'] = 'yellowcard'
|
| 119 |
elif ev.select_one('[class*="substitution"]'): ev_data['type'] = 'substitution'
|
|
|
|
| 120 |
players_el = ev.select_one('.players')
|
| 121 |
if players_el:
|
| 122 |
pl_text = _clean(players_el.get_text(' ', strip=True))
|
|
|
|
| 127 |
else:
|
| 128 |
ev_data['players'] = pl_text
|
| 129 |
events.append(ev_data)
|
|
|
|
| 130 |
if events:
|
| 131 |
result['events'] = events
|
| 132 |
result['sections'].append('events')
|
|
|
|
| 133 |
pred = soup.select_one('.prediction-card')
|
| 134 |
if pred:
|
| 135 |
team_info = pred.select_one('.team-info')
|
|
|
|
| 144 |
vc = pred.select_one('.vote-count')
|
| 145 |
if vc: pred_data['vote_count'] = _clean(vc.get_text())
|
| 146 |
result['prediction'] = pred_data
|
|
|
|
| 147 |
recent = []
|
| 148 |
ml = soup.select_one('.matches-list')
|
| 149 |
if ml:
|
|
|
|
| 158 |
if recent:
|
| 159 |
result['recent_matches'] = recent
|
| 160 |
result['sections'].append('recent')
|
|
|
|
| 161 |
try:
|
| 162 |
api_h = {"User-Agent": "Mozilla/5.0", "Accept": "application/json", "X-Requested-With": "XMLHttpRequest", "Referer": "https://bongda.com.vn/"}
|
| 163 |
ar = req.get(f"https://bongda.com.vn/api/fixtures/h2h-stats?event_id={event_id}", headers=api_h, timeout=10)
|
|
|
|
| 175 |
result['h2h_stats_parsed'] = ast
|
| 176 |
result['sections'].append('h2h_stats')
|
| 177 |
except: pass
|
|
|
|
| 178 |
return result
|
| 179 |
|
| 180 |
@app.get('/api/proxy/bongda')
|
| 181 |
def proxy_bongda(event_id: int = Query(default=None), slug: str = Query(default=None)):
|
| 182 |
if event_id is None:
|
| 183 |
return JSONResponse({'error': 'event_id required'}, status_code=400)
|
|
|
|
| 184 |
cache_key = f"{event_id}_{slug}"
|
| 185 |
now = time.time()
|
| 186 |
cached = _match_cache.get(cache_key)
|
| 187 |
if cached and now - cached.get('_ts', 0) < 300:
|
| 188 |
return JSONResponse(cached)
|
|
|
|
| 189 |
try:
|
| 190 |
result = _get_match_detail(event_id, slug)
|
| 191 |
if result:
|
|
|
|
| 196 |
err = {"event_id": event_id, "found": False, "error": str(e), "_ts": now}
|
| 197 |
_match_cache[cache_key] = err
|
| 198 |
return JSONResponse(err)
|
|
|
|
| 199 |
return JSONResponse({"event_id": event_id, "found": False})
|
| 200 |
|
| 201 |
@app.get('/api/match/{event_id}/detail')
|
| 202 |
def api_match_detail(event_id: int, url: str = Query(default=None)):
|
|
|
|
| 203 |
slug = None
|
| 204 |
if url:
|
| 205 |
m = re.match(r'.+/tran-dau/\d+/(?:centre|preview)/(.+)', url)
|
| 206 |
if m:
|
| 207 |
slug = m.group(1)
|
|
|
|
| 208 |
cache_key = f"{event_id}_{slug or ''}"
|
| 209 |
now = time.time()
|
| 210 |
cached = _match_cache.get(cache_key)
|
| 211 |
if cached and now - cached.get('_ts', 0) < 300:
|
| 212 |
return JSONResponse(cached)
|
|
|
|
| 213 |
try:
|
|
|
|
| 214 |
if not slug:
|
| 215 |
try:
|
| 216 |
home_r = req.get("https://bongda.com.vn/", headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
|
|
|
|
| 224 |
cache_key = f"{event_id}_{slug}"
|
| 225 |
break
|
| 226 |
except: pass
|
|
|
|
| 227 |
result = _get_match_detail(event_id, slug)
|
| 228 |
if result:
|
| 229 |
result['_ts'] = now
|
|
|
|
| 233 |
err = {"event_id": event_id, "found": False, "error": str(e), "_ts": now}
|
| 234 |
_match_cache[cache_key] = err
|
| 235 |
return JSONResponse(err)
|
|
|
|
| 236 |
return JSONResponse({"event_id": event_id, "found": False})
|
| 237 |
|
| 238 |
+
# === VTVNamBo Shorts API ===
|
| 239 |
+
@app.get('/api/shorts/vtvnambo')
|
| 240 |
+
def api_shorts_vtvnambo():
|
| 241 |
+
return JSONResponse(_get_vtvnambo_shorts())
|
| 242 |
+
|
| 243 |
_STOP=set('và của các những một được trong với cho tại sau trước khi không người việt nam hôm nay mới nhất nóng tin tức cập nhật theo từ đến là có thì này đã để'.split())
|
| 244 |
|
| 245 |
def _has_kw(topic,title):
|
|
|
|
| 516 |
time.sleep(90)
|
| 517 |
threading.Thread(target=_bg,daemon=True).start()
|
| 518 |
|
| 519 |
+
app.mount('/static',StaticFiles(directory=STATIC_DIR),name='vnews_static')
|