Spaces:
Running
Running
Upload app_v2_entry.py
Browse files- app_v2_entry.py +158 -152
app_v2_entry.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""VNEWS v2 Entry Point - with
|
| 2 |
import sys, os
|
| 3 |
from main import app, HEADERS, BONGDA_HEADERS, fetch_bongda_api, HL_LEAGUES
|
| 4 |
|
|
@@ -12,10 +12,10 @@ from fastapi.staticfiles import StaticFiles
|
|
| 12 |
from starlette.routing import Mount
|
| 13 |
from fastapi import Query, Request
|
| 14 |
import requests as req
|
| 15 |
-
from urllib.parse import quote, urlparse
|
| 16 |
from bs4 import BeautifulSoup
|
| 17 |
import re, html as html_lib, json, threading, time
|
| 18 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
| 19 |
|
| 20 |
HL_LEAGUES['friendly'] = {"path": "giai-khac/friendly", "name": "Giao hữu", "emoji": "🤝"}
|
| 21 |
|
|
@@ -25,7 +25,162 @@ app.routes[:]=[r for r in app.routes if not isinstance(r, Mount)]
|
|
| 25 |
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 |
_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())
|
|
|
|
| 29 |
def _has_kw(topic,title):
|
| 30 |
tl=topic.lower();tt=(title or'').lower()
|
| 31 |
if tl in tt:return True
|
|
@@ -33,7 +188,6 @@ def _has_kw(topic,title):
|
|
| 33 |
if not words:return True
|
| 34 |
return any(w in tt for w in words)
|
| 35 |
|
| 36 |
-
# Search functions...
|
| 37 |
def _s_vnexpress(topic,limit=8):
|
| 38 |
items=[]
|
| 39 |
try:
|
|
@@ -236,149 +390,6 @@ def _st():return JSONResponse({'persistent':os.path.isdir('/data') and os.access
|
|
| 236 |
@app.get('/s')
|
| 237 |
async def _sh(url:str='',title:str='',img:str=''):return HTMLResponse(f'<!DOCTYPE html><html><head><meta property="og:title" content="{_clean(title)}"><meta property="og:image" content="{_clean(img)}"><meta http-equiv="refresh" content="0;url={_clean(url) or "/"}"></head><body></body></html>')
|
| 238 |
|
| 239 |
-
# === BONGDA PROXY (v5-fast) ===
|
| 240 |
-
@app.get('/api/proxy/bongda')
|
| 241 |
-
def proxy_bongda(event_id: int = Query(default=None)):
|
| 242 |
-
"""Fast server-side proxy for bongda.com.vn match detail."""
|
| 243 |
-
if event_id is None:
|
| 244 |
-
return JSONResponse({'error': 'event_id required'}, status_code=400)
|
| 245 |
-
|
| 246 |
-
result = {"event_id": event_id, "found": False, "sections": []}
|
| 247 |
-
|
| 248 |
-
try:
|
| 249 |
-
url = f"https://bongda.com.vn/tran-dau/{event_id}/centre/"
|
| 250 |
-
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html", "Referer": "https://bongda.com.vn/"}
|
| 251 |
-
resp = req.get(url, headers=headers, timeout=15, allow_redirects=True)
|
| 252 |
-
|
| 253 |
-
if resp.status_code != 200:
|
| 254 |
-
return JSONResponse(result)
|
| 255 |
-
|
| 256 |
-
soup = BeautifulSoup(resp.text, 'html.parser')
|
| 257 |
-
info = {}
|
| 258 |
-
|
| 259 |
-
tel = soup.select_one('.teams')
|
| 260 |
-
if tel:
|
| 261 |
-
he = tel.select_one('.team.home')
|
| 262 |
-
if he:
|
| 263 |
-
ne = he.select_one('p:not(.logo)') or he.find('p')
|
| 264 |
-
if ne: info['home_team'] = _clean(ne.get_text())
|
| 265 |
-
lo = he.select_one('img')
|
| 266 |
-
if lo: info['home_logo'] = lo.get('src', '')
|
| 267 |
-
ae = tel.select_one('.team.away')
|
| 268 |
-
if ae:
|
| 269 |
-
ne = ae.select_one('p:not(.logo)') or ae.find('p')
|
| 270 |
-
if ne: info['away_team'] = _clean(ne.get_text())
|
| 271 |
-
lo = ae.select_one('img')
|
| 272 |
-
if lo: info['away_logo'] = lo.get('src', '')
|
| 273 |
-
sc = tel.select_one('.score')
|
| 274 |
-
if sc:
|
| 275 |
-
parts = [_clean(p.get_text()) for p in sc.select('p')]
|
| 276 |
-
if len(parts) >= 2: info['score'] = f"{parts[0]} - {parts[1]}"
|
| 277 |
-
lb = sc.select_one('.label')
|
| 278 |
-
if lb: info['status_label'] = _clean(lb.get_text())
|
| 279 |
-
|
| 280 |
-
if info.get('home_team') and info.get('away_team'):
|
| 281 |
-
result['info'] = info
|
| 282 |
-
result['found'] = True
|
| 283 |
-
result['sections'].append('info')
|
| 284 |
-
|
| 285 |
-
events = []
|
| 286 |
-
ev_div = soup.select_one('.events')
|
| 287 |
-
if ev_div:
|
| 288 |
-
period = ''
|
| 289 |
-
for child in ev_div.children:
|
| 290 |
-
if not hasattr(child, 'name') or not child.name: continue
|
| 291 |
-
cls = ' '.join(child.get('class', []))
|
| 292 |
-
if 'period' in cls:
|
| 293 |
-
h2 = child.find('h2')
|
| 294 |
-
if h2: period = _clean(h2.get_text())
|
| 295 |
-
for ev in child.children:
|
| 296 |
-
if not hasattr(ev, 'name') or not ev.name: continue
|
| 297 |
-
ev_cls = ' '.join(ev.get('class', []))
|
| 298 |
-
if 'event' not in ev_cls: continue
|
| 299 |
-
|
| 300 |
-
ev_data = {
|
| 301 |
-
'team': 'home' if 'home' in ev_cls else 'away',
|
| 302 |
-
'period': period,
|
| 303 |
-
'type': 'unknown',
|
| 304 |
-
'time': '',
|
| 305 |
-
'players': '',
|
| 306 |
-
}
|
| 307 |
-
|
| 308 |
-
type_el = ev.select_one('.event-type')
|
| 309 |
-
if type_el:
|
| 310 |
-
if type_el.select_one('[class*="redcard"]'): ev_data['type'] = 'redcard'
|
| 311 |
-
elif type_el.select_one('[class*="yellowcard"]'): ev_data['type'] = 'yellowcard'
|
| 312 |
-
elif type_el.select_one('[class*="goal"]'): ev_data['type'] = 'goal'
|
| 313 |
-
elif type_el.select_one('[class*="substitution"]'): ev_data['type'] = 'substitution'
|
| 314 |
-
|
| 315 |
-
players_el = ev.select_one('.players')
|
| 316 |
-
if players_el:
|
| 317 |
-
time_el = players_el.select_one('.event-time')
|
| 318 |
-
if time_el: ev_data['time'] = _clean(time_el.get_text())
|
| 319 |
-
ev_data['players'] = _clean(players_el.get_text(' ', strip=True))
|
| 320 |
-
|
| 321 |
-
events.append(ev_data)
|
| 322 |
-
|
| 323 |
-
if events:
|
| 324 |
-
result['events'] = events
|
| 325 |
-
result['sections'].append('events')
|
| 326 |
-
|
| 327 |
-
pred = soup.select_one('.prediction-card')
|
| 328 |
-
if pred:
|
| 329 |
-
team_info = pred.select_one('.team-info')
|
| 330 |
-
if team_info:
|
| 331 |
-
teams = team_info.select('.team')
|
| 332 |
-
if len(teams) >= 2:
|
| 333 |
-
pred_data = {}
|
| 334 |
-
pred_data['home_name'] = _clean(teams[0].select_one('.team-name').get_text()) if teams[0].select_one('.team-name') else ''
|
| 335 |
-
pred_data['away_name'] = _clean(teams[1].select_one('.team-name').get_text()) if teams[1].select_one('.team-name') else ''
|
| 336 |
-
divider = team_info.select_one('.divider')
|
| 337 |
-
if divider: pred_data['result'] = _clean(divider.get_text())
|
| 338 |
-
vc = pred.select_one('.vote-count')
|
| 339 |
-
if vc: pred_data['vote_count'] = _clean(vc.get_text())
|
| 340 |
-
result['prediction'] = pred_data
|
| 341 |
-
|
| 342 |
-
recent = []
|
| 343 |
-
ml = soup.select_one('.matches-list')
|
| 344 |
-
if ml:
|
| 345 |
-
for item in ml.select('.match-detail, .match-item, li'):
|
| 346 |
-
de = item.select_one('.date, .time')
|
| 347 |
-
le = item.select_one('.league')
|
| 348 |
-
he_item = item.select_one('.home, .team-home')
|
| 349 |
-
ae_item = item.select_one('.away, .team-away')
|
| 350 |
-
se = item.select_one('.score, .result')
|
| 351 |
-
if he_item or ae_item:
|
| 352 |
-
recent.append({
|
| 353 |
-
'date': _clean(de.get_text()) if de else '', 'league': _clean(le.get_text()) if le else '',
|
| 354 |
-
'home': _clean(he_item.get_text()) if he_item else '', 'away': _clean(ae_item.get_text()) if ae_item else '',
|
| 355 |
-
'score': _clean(se.get_text()) if se else 'vs',
|
| 356 |
-
})
|
| 357 |
-
if recent:
|
| 358 |
-
result['recent_matches'] = recent
|
| 359 |
-
result['sections'].append('recent')
|
| 360 |
-
|
| 361 |
-
try:
|
| 362 |
-
api_h = {"User-Agent": "Mozilla/5.0", "Accept": "application/json", "X-Requested-With": "XMLHttpRequest", "Referer": "https://bongda.com.vn/"}
|
| 363 |
-
ar = req.get(f"https://bongda.com.vn/api/fixtures/h2h-stats?event_id={event_id}", headers=api_h, timeout=10)
|
| 364 |
-
if ar.status_code == 200:
|
| 365 |
-
ad = ar.json()
|
| 366 |
-
if ad.get('status') == 'success' and ad.get('html'):
|
| 367 |
-
asp = BeautifulSoup(ad['html'], 'html.parser')
|
| 368 |
-
ast = {}
|
| 369 |
-
for row in asp.select('li, tr'):
|
| 370 |
-
cells = row.select('td, span, p')
|
| 371 |
-
if len(cells) >= 3:
|
| 372 |
-
lb = _clean(cells[0].get_text())
|
| 373 |
-
if lb: ast[lb] = {'home': _clean(cells[1].get_text()), 'away': _clean(cells[2].get_text())}
|
| 374 |
-
if ast: result['h2h_stats_parsed'] = ast; result['sections'].append('h2h_stats')
|
| 375 |
-
except: pass
|
| 376 |
-
|
| 377 |
-
return JSONResponse(result)
|
| 378 |
-
except Exception as e:
|
| 379 |
-
return JSONResponse({"event_id": event_id, "found": False, "error": str(e)})
|
| 380 |
-
|
| 381 |
-
# === WC2026 endpoints ===
|
| 382 |
from wc2026_scraper import(scrape_summary,scrape_fixtures,scrape_standings,scrape_stats,scrape_wc_news,scrape_road_to_wc,get_wc2026_all,scrape_history,scrape_h2h,scrape_lineups,scrape_match_detail)
|
| 383 |
@app.get('/api/wc2026')
|
| 384 |
def _w():return JSONResponse(get_wc2026_all())
|
|
@@ -444,9 +455,4 @@ def _bg():
|
|
| 444 |
time.sleep(90)
|
| 445 |
threading.Thread(target=_bg,daemon=True).start()
|
| 446 |
|
| 447 |
-
app.mount('/static',StaticFiles(directory=STATIC_DIR),name='vnews_static')
|
| 448 |
-
|
| 449 |
-
# _run.py helper
|
| 450 |
-
RUN_PY_CONTENT = 'from app_v2_entry import app'
|
| 451 |
-
with open('/app/_run.py', 'w') as f:
|
| 452 |
-
f.write(RUN_PY_CONTENT)
|
|
|
|
| 1 |
+
"""VNEWS v2 Entry Point - with fast bongda proxy"""
|
| 2 |
import sys, os
|
| 3 |
from main import app, HEADERS, BONGDA_HEADERS, fetch_bongda_api, HL_LEAGUES
|
| 4 |
|
|
|
|
| 12 |
from starlette.routing import Mount
|
| 13 |
from fastapi import Query, Request
|
| 14 |
import requests as req
|
|
|
|
| 15 |
from bs4 import BeautifulSoup
|
| 16 |
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 |
HL_LEAGUES['friendly'] = {"path": "giai-khac/friendly", "name": "Giao hữu", "emoji": "🤝"}
|
| 21 |
|
|
|
|
| 25 |
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')
|
| 53 |
+
if he:
|
| 54 |
+
p_tags = [p for p in he.select('p') if not p.get('class') or 'logo' not in p.get('class', [])]
|
| 55 |
+
if p_tags: info['home_team'] = _clean(p_tags[0].get_text())
|
| 56 |
+
lo = he.select_one('img')
|
| 57 |
+
if lo: info['home_logo'] = lo.get('src', '')
|
| 58 |
+
ae = tel.select_one('.team.away')
|
| 59 |
+
if ae:
|
| 60 |
+
p_tags = ae.select('p')
|
| 61 |
+
team_ps = [p for p in p_tags if not p.get('class') or 'logo' not in p.get('class', [])]
|
| 62 |
+
if team_ps: info['away_team'] = _clean(team_ps[-1].get_text())
|
| 63 |
+
lo = ae.select_one('img')
|
| 64 |
+
if lo: info['away_logo'] = lo.get('src', '')
|
| 65 |
+
sc = tel.select_one('.score')
|
| 66 |
+
if sc:
|
| 67 |
+
parts = [_clean(p.get_text()) for p in sc.select('p')]
|
| 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))
|
| 95 |
+
m = re.match(r"(\d+)'(.*)", pl_text)
|
| 96 |
+
if m:
|
| 97 |
+
ev_data['time'] = f"{m.group(1)}'"
|
| 98 |
+
ev_data['players'] = m.group(2)
|
| 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')
|
| 110 |
+
if team_info:
|
| 111 |
+
teams = team_info.select('.team')
|
| 112 |
+
pred_data = {}
|
| 113 |
+
if len(teams) >= 2:
|
| 114 |
+
pred_data['home_name'] = _clean(teams[0].select_one('.team-name').get_text()) if teams[0].select_one('.team-name') else ''
|
| 115 |
+
pred_data['away_name'] = _clean(teams[1].select_one('.team-name').get_text()) if teams[1].select_one('.team-name') else ''
|
| 116 |
+
divider = team_info.select_one('.divider')
|
| 117 |
+
if divider: pred_data['result'] = _clean(divider.get_text())
|
| 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:
|
| 125 |
+
for item in ml.select('.match-detail, .match-item, li'):
|
| 126 |
+
de = item.select_one('.date, .time')
|
| 127 |
+
le = item.select_one('.league')
|
| 128 |
+
he_item = item.select_one('.home, .team-home')
|
| 129 |
+
ae_item = item.select_one('.away, .team-away')
|
| 130 |
+
se = item.select_one('.score, .result')
|
| 131 |
+
if he_item or ae_item:
|
| 132 |
+
recent.append({'date': _clean(de.get_text()) if de else '', 'league': _clean(le.get_text()) if le else '', 'home': _clean(he_item.get_text()) if he_item else '', 'away': _clean(ae_item.get_text()) if ae_item else '', 'score': _clean(se.get_text()) if se else 'vs'})
|
| 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)
|
| 140 |
+
if ar.status_code == 200:
|
| 141 |
+
ad = ar.json()
|
| 142 |
+
if ad.get('status') == 'success' and ad.get('html'):
|
| 143 |
+
asp = BeautifulSoup(ad['html'], 'html.parser')
|
| 144 |
+
ast = {}
|
| 145 |
+
for row in asp.select('li, tr'):
|
| 146 |
+
cells = row.select('td, span, p')
|
| 147 |
+
if len(cells) >= 3:
|
| 148 |
+
lb = _clean(cells[0].get_text())
|
| 149 |
+
if lb: ast[lb] = {'home': _clean(cells[1].get_text()), 'away': _clean(cells[2].get_text())}
|
| 150 |
+
if ast:
|
| 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:
|
| 171 |
+
result['_ts'] = now
|
| 172 |
+
_match_cache[cache_key] = result
|
| 173 |
+
return JSONResponse(result)
|
| 174 |
+
except Exception as e:
|
| 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 |
+
# === Rest of endpoints (existing) ===
|
| 182 |
_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())
|
| 183 |
+
|
| 184 |
def _has_kw(topic,title):
|
| 185 |
tl=topic.lower();tt=(title or'').lower()
|
| 186 |
if tl in tt:return True
|
|
|
|
| 188 |
if not words:return True
|
| 189 |
return any(w in tt for w in words)
|
| 190 |
|
|
|
|
| 191 |
def _s_vnexpress(topic,limit=8):
|
| 192 |
items=[]
|
| 193 |
try:
|
|
|
|
| 390 |
@app.get('/s')
|
| 391 |
async def _sh(url:str='',title:str='',img:str=''):return HTMLResponse(f'<!DOCTYPE html><html><head><meta property="og:title" content="{_clean(title)}"><meta property="og:image" content="{_clean(img)}"><meta http-equiv="refresh" content="0;url={_clean(url) or "/"}"></head><body></body></html>')
|
| 392 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
from wc2026_scraper import(scrape_summary,scrape_fixtures,scrape_standings,scrape_stats,scrape_wc_news,scrape_road_to_wc,get_wc2026_all,scrape_history,scrape_h2h,scrape_lineups,scrape_match_detail)
|
| 394 |
@app.get('/api/wc2026')
|
| 395 |
def _w():return JSONResponse(get_wc2026_all())
|
|
|
|
| 455 |
time.sleep(90)
|
| 456 |
threading.Thread(target=_bg,daemon=True).start()
|
| 457 |
|
| 458 |
+
app.mount('/static',StaticFiles(directory=STATIC_DIR),name='vnews_static')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|