VNEWS / app_v2_entry.py
bep40's picture
Add: xemlaibongda.top proxy endpoint (CORS workaround for WC highlights)
246bd47 verified
Raw
History Blame
27.1 kB
"""VNEWS v2 Entry Point - with fast bongda proxy"""
import sys, os
from main import app, HEADERS, BONGDA_HEADERS, fetch_bongda_api, HL_LEAGUES
try:
import ai_ext
except Exception as e:
print(f"[WARN] ai_ext import failed: {e}")
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, Response
from fastapi.staticfiles import StaticFiles
from starlette.routing import Mount
from fastapi import Query, Request
import requests as req
from bs4 import BeautifulSoup
import re, html as html_lib, json, threading, time
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.parse import quote
HL_LEAGUES['friendly'] = {"path": "giai-khac/friendly", "name": "Giao hữu", "emoji": "🤝"}
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
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()))]
app.routes[:]=[r for r in app.routes if not isinstance(r, Mount)]
app.router.routes=[r for r in app.router.routes if not isinstance(r, Mount)]
def _clean(s): return re.sub(r"\s+"," ",html_lib.unescape(str(s or""))).strip()
# Cache for match details (5 min TTL)
_match_cache = {}
# === FAST BONGDA PROXY ENDPOINT ===
def _get_match_detail(event_id, slug=None):
"""Internal function to scrape match detail from bongda.com.vn"""
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html", "Referer": "https://bongda.com.vn/"}
if slug:
url = f"https://bongda.com.vn/tran-dau/{event_id}/centre/{slug}"
else:
url = f"https://bongda.com.vn/tran-dau/{event_id}"
resp = req.get(url, headers=headers, timeout=15, allow_redirects=True)
if resp.status_code != 200:
return None
soup = BeautifulSoup(resp.text, 'html.parser')
result = {"event_id": event_id, "found": False, "sections": []}
info = {}
tel = soup.select_one('.teams')
if tel:
he = tel.select_one('.team.home')
if he:
p_tags = [p for p in he.select('p') if not p.get('class') or 'logo' not in p.get('class', [])]
if p_tags: info['home_team'] = _clean(p_tags[0].get_text())
lo = he.select_one('img')
if lo: info['home_logo'] = lo.get('src', '')
ae = tel.select_one('.team.away')
if ae:
p_tags = ae.select('p')
team_ps = [p for p in p_tags if not p.get('class') or 'logo' not in p.get('class', [])]
if team_ps: info['away_team'] = _clean(team_ps[-1].get_text())
lo = ae.select_one('img')
if lo: info['away_logo'] = lo.get('src', '')
sc = tel.select_one('.score')
if sc:
parts = [_clean(p.get_text()) for p in sc.select('p')]
if len(parts) >= 2: info['score'] = f"{parts[0]} - {parts[1]}"
lb = sc.select_one('.label')
if lb: info['status_label'] = _clean(lb.get_text())
if info.get('home_team') and info.get('away_team'):
result['info'] = info
result['found'] = True
result['sections'].append('info')
events = []
for ev in soup.select('.events .period .event'):
ev_cls = ' '.join(ev.get('class', []))
ev_data = {'team': 'home' if 'home' in ev_cls else 'away', 'period': '', 'type': 'unknown', 'time': '', 'players': ''}
parent = ev.parent
if parent:
h2 = parent.find('h2')
if h2: ev_data['period'] = _clean(h2.get_text())
if ev.select_one('[class*="goal"]'): ev_data['type'] = 'goal'
elif ev.select_one('[class*="redcard"]'): ev_data['type'] = 'redcard'
elif ev.select_one('[class*="yellowcard"]'): ev_data['type'] = 'yellowcard'
elif ev.select_one('[class*="substitution"]'): ev_data['type'] = 'substitution'
players_el = ev.select_one('.players')
if players_el:
pl_text = _clean(players_el.get_text(' ', strip=True))
m = re.match(r"(\d+)'(.*)", pl_text)
if m:
ev_data['time'] = f"{m.group(1)}'"
ev_data['players'] = m.group(2)
else:
ev_data['players'] = pl_text
events.append(ev_data)
if events:
result['events'] = events
result['sections'].append('events')
pred = soup.select_one('.prediction-card')
if pred:
team_info = pred.select_one('.team-info')
if team_info:
teams = team_info.select('.team')
pred_data = {}
if len(teams) >= 2:
pred_data['home_name'] = _clean(teams[0].select_one('.team-name').get_text()) if teams[0].select_one('.team-name') else ''
pred_data['away_name'] = _clean(teams[1].select_one('.team-name').get_text()) if teams[1].select_one('.team-name') else ''
divider = team_info.select_one('.divider')
if divider: pred_data['result'] = _clean(divider.get_text())
vc = pred.select_one('.vote-count')
if vc: pred_data['vote_count'] = _clean(vc.get_text())
result['prediction'] = pred_data
recent = []
ml = soup.select_one('.matches-list')
if ml:
for item in ml.select('.match-detail, .match-item, li'):
de = item.select_one('.date, .time')
le = item.select_one('.league')
he_item = item.select_one('.home, .team-home')
ae_item = item.select_one('.away, .team-away')
se = item.select_one('.score, .result')
if he_item or ae_item:
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'})
if recent:
result['recent_matches'] = recent
result['sections'].append('recent')
try:
api_h = {"User-Agent": "Mozilla/5.0", "Accept": "application/json", "X-Requested-With": "XMLHttpRequest", "Referer": "https://bongda.com.vn/"}
ar = req.get(f"https://bongda.com.vn/api/fixtures/h2h-stats?event_id={event_id}", headers=api_h, timeout=10)
if ar.status_code == 200:
ad = ar.json()
if ad.get('status') == 'success' and ad.get('html'):
asp = BeautifulSoup(ad['html'], 'html.parser')
ast = {}
for row in asp.select('li, tr'):
cells = row.select('td, span, p')
if len(cells) >= 3:
lb = _clean(cells[0].get_text())
if lb: ast[lb] = {'home': _clean(cells[1].get_text()), 'away': _clean(cells[2].get_text())}
if ast:
result['h2h_stats_parsed'] = ast
result['sections'].append('h2h_stats')
except: pass
return result
@app.get('/api/proxy/bongda')
def proxy_bongda(event_id: int = Query(default=None), slug: str = Query(default=None)):
if event_id is None:
return JSONResponse({'error': 'event_id required'}, status_code=400)
cache_key = f"{event_id}_{slug}"
now = time.time()
cached = _match_cache.get(cache_key)
if cached and now - cached.get('_ts', 0) < 300:
return JSONResponse(cached)
try:
result = _get_match_detail(event_id, slug)
if result:
result['_ts'] = now
_match_cache[cache_key] = result
return JSONResponse(result)
except Exception as e:
err = {"event_id": event_id, "found": False, "error": str(e), "_ts": now}
_match_cache[cache_key] = err
return JSONResponse(err)
return JSONResponse({"event_id": event_id, "found": False})
@app.get('/api/match/{event_id}/detail')
def api_match_detail(event_id: int, url: str = Query(default=None)):
# Try to extract slug from url if provided
slug = None
if url:
m = re.match(r'.+/tran-dau/\d+/(?:centre|preview)/(.+)', url)
if m:
slug = m.group(1)
cache_key = f"{event_id}_{slug or ''}"
now = time.time()
cached = _match_cache.get(cache_key)
if cached and now - cached.get('_ts', 0) < 300:
return JSONResponse(cached)
try:
# If no slug, try to find it from homepage
if not slug:
try:
home_r = req.get("https://bongda.com.vn/", headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
if home_r.status_code == 200:
home_soup = BeautifulSoup(home_r.text, 'html.parser')
for a in home_soup.select(f'a[href*="/tran-dau/{event_id}/"]'):
href = a.get('href', '')
m = re.match(r'/tran-dau/\d+/(?:centre|preview)/(.+)', href)
if m:
slug = m.group(1)
cache_key = f"{event_id}_{slug}"
break
except: pass
result = _get_match_detail(event_id, slug)
if result:
result['_ts'] = now
_match_cache[cache_key] = result
return JSONResponse(result)
except Exception as e:
err = {"event_id": event_id, "found": False, "error": str(e), "_ts": now}
_match_cache[cache_key] = err
return JSONResponse(err)
return JSONResponse({"event_id": event_id, "found": False})
# === Rest of endpoints (existing) ===
_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())
def _has_kw(topic,title):
tl=topic.lower();tt=(title or'').lower()
if tl in tt:return True
words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',tl) if len(w)>2 and w not in _STOP]
if not words:return True
return any(w in tt for w in words)
def _s_vnexpress(topic,limit=8):
items=[]
try:
r=req.get(f"https://timkiem.vnexpress.net/?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
for art in soup.select('article.item-news')[:limit]:
a=art.select_one('h2 a, h3 a')
if a and a.get('href'):
t=_clean(a.get('title','') or a.get_text(strip=True))
if _has_kw(topic,t):items.append({'title':t,'url':a['href'],'via':'VnExpress'})
except:pass
return items
def _s_dantri(topic,limit=8):
items=[]
try:
r=req.get(f"https://dantri.com.vn/tim-kiem/{quote(topic)}.htm",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
for a in soup.select('h3 a[href], .article-title a[href]')[:limit*2]:
t=_clean(a.get_text(strip=True));href=a.get('href','')
if t and len(t)>15 and _has_kw(topic,t):
if not href.startswith('http'):href='https://dantri.com.vn'+href
items.append({'title':t,'url':href,'via':'Dân Trí'})
if len(items)>=limit:break
except:pass
return items
def _s_vietnamnet(topic,limit=6):
items=[]
try:
r=req.get(f"https://vietnamnet.vn/tim-kiem?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
for a in soup.select('h3 a[href], .vnn-title a')[:limit*2]:
t=_clean(a.get_text(strip=True));href=a.get('href','')
if t and len(t)>15 and _has_kw(topic,t):
if not href.startswith('http'):href='https://vietnamnet.vn'+href
items.append({'title':t,'url':href,'via':'VietNamNet'})
if len(items)>=limit:break
except:pass
return items
def _s_bongda(topic,limit=5):
items=[]
try:
r=req.get(f"https://bongda.com.vn/tim-kiem.html?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=8);soup=BeautifulSoup(r.text,'lxml')
for a in soup.select('h3 a[href], .title a[href]')[:limit*2]:
t=_clean(a.get_text(strip=True));href=a.get('href','')
if t and len(t)>15 and _has_kw(topic,t):
if not href.startswith('http'):href='https://bongda.com.vn'+href
items.append({'title':t,'url':href,'via':'Bóng Đá'})
if len(items)>=limit:break
except:pass
return items
def _s_genk(topic,limit=5):
items=[]
try:
r=req.get(f"https://genk.vn/tim-kiem?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=8);soup=BeautifulSoup(r.text,'lxml')
for a in soup.select('a[href$=".chn"]')[:limit*3]:
t=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
if t and len(t)>15 and _has_kw(topic,t):
if href.startswith('/'):href='https://genk.vn'+href
items.append({'title':t,'url':href,'via':'GenK'})
if len(items)>=limit:break
except:pass
return items
def _s_thanhnien(topic,limit=6):
items=[]
try:
r=req.get(f"https://thanhnien.vn/tim-kiem?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
for a in soup.select('h3 a[href], .box-title a')[:limit*2]:
t=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
if t and len(t)>15 and _has_kw(topic,t):
if not href.startswith('http'):href='https://thanhnien.vn'+href
items.append({'title':t,'url':href,'via':'Thanh Niên'})
if len(items)>=limit:break
except:pass
return items
def _s_tuoitre(topic,limit=6):
items=[]
try:
r=req.get(f"https://tuoitre.vn/tim-kiem.htm?keywords={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
for a in soup.select('h3 a[href], .box-title-text a')[:limit*2]:
t=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
if t and len(t)>15 and _has_kw(topic,t):
if not href.startswith('http'):href='https://tuoitre.vn'+href
items.append({'title':t,'url':href,'via':'Tuổi Trẻ'})
if len(items)>=limit:break
except:pass
return items
def _s_thethaovanhoa(topic,limit=5):
items=[]
try:
r=req.get(f"https://thethaovanhoa.vn/tim-kiem.htm?keyword={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=8);soup=BeautifulSoup(r.text,'lxml')
for a in soup.select('h3 a[href], .title a[href]')[:limit*2]:
t=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
if t and len(t)>15 and _has_kw(topic,t):
if not href.startswith('http'):href='https://thethaovanhoa.vn'+href
items.append({'title':t,'url':href,'via':'TT&VH'})
if len(items)>=limit:break
except:pass
return items
def _search_all(topic,limit=36):
results={}
with ThreadPoolExecutor(8) as ex:
futs={ex.submit(_s_vnexpress,topic,8):'vne',ex.submit(_s_dantri,topic,8):'dt',ex.submit(_s_vietnamnet,topic,6):'vnn',ex.submit(_s_bongda,topic,5):'bd',ex.submit(_s_genk,topic,5):'gk',ex.submit(_s_thanhnien,topic,6):'tn',ex.submit(_s_tuoitre,topic,6):'tt',ex.submit(_s_thethaovanhoa,topic,5):'tvh'}
for f in as_completed(futs,timeout=14):
try:results[futs[f]]=f.result()
except:results[futs[f]]=[]
srcs=list(results.values());out=[];seen=set()
for i in range(max((len(s) for s in srcs),default=0)):
for s in srcs:
if i<len(s) and s[i].get('url') and s[i]['url'] not in seen:seen.add(s[i]['url']);out.append(s[i])
return out[:limit]
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/api/article' and 'GET' in getattr(r,'methods',set()))]
def _scrape_generic(url):
try:
r=req.get(url,headers={'User-Agent':'Mozilla/5.0','Accept-Language':'vi-VN,vi;q=0.9'},timeout=15,allow_redirects=True);r.encoding='utf-8';soup=BeautifulSoup(r.text,'lxml')
for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript']):tag.decompose()
h1=soup.find('h1');ogt=soup.find('meta',property='og:title');title=(h1.get_text(strip=True) if h1 else '')or(ogt.get('content','') if ogt else '')
ogd=soup.find('meta',property='og:description');summary=ogd.get('content','') if ogd else ''
ogi=soup.find('meta',property='og:image');og_img=ogi.get('content','') if ogi else ''
if og_img and og_img.startswith('//'):og_img='https:'+og_img
block=None
for sel in['article','.singular-content','.detail-content','.fck_detail','.content-detail','.knc-content','main','.cms-body','.article__body']:
el=soup.select_one(sel)
if el and len(el.find_all('p'))>=2:block=el;break
if not block:block=soup.body or soup
body=[]
for el in block.find_all(['p','h2','h3','figure','img'],recursive=True):
if el.name=='p':t=el.get_text(strip=True);(body.append({'type':'p','text':t}) if t and len(t)>30 else None)
elif el.name in('h2','h3'):t=el.get_text(strip=True);(body.append({'type':'heading','text':t}) if t else None)
elif el.name in('figure','img'):
im=el if el.name=='img' else el.find('img')
if im:src=im.get('data-src') or im.get('src') or'';(body.append({'type':'img','src':'https:'+src if src.startswith('//') else src}) if src and'base64' not in src else None)
if not body and summary:body=[{'type':'p','text':summary}]
return{'title':_clean(title),'summary':_clean(summary),'og_image':og_img,'body':body[:50],'source':'generic','url':url}
except:return None
@app.get('/api/article')
def api_article_v2(url:str=Query(...)):
from main import scrape_vne_article,scrape_bbc_article,scrape_dantri_article,scrape_genk_article,scrape_ttvh_article
if 'vnexpress.net' in url:data=scrape_vne_article(url)
elif 'bbc.com' in url:data=scrape_bbc_article(url)
elif 'dantri.com.vn' in url:data=scrape_dantri_article(url)
elif 'genk.vn' in url:data=scrape_genk_article(url)
elif 'thethaovanhoa.vn' in url:data=scrape_ttvh_article(url)
else:data=_scrape_generic(url)
if data and data.get('body'):return JSONResponse(data)
return JSONResponse(data if data else{'error':'Không đọc được','url':url})
_hot_cache={'t':0,'d':[]}
def _get_hot_topics():
now=time.time()
if _hot_cache['d'] and now-_hot_cache['t']<600:return _hot_cache['d']
freq={};display={}
feeds=['https://vnexpress.net/rss/tin-moi-nhat.rss','https://dantri.com.vn/rss/home.rss','https://vietnamnet.vn/rss/tin-moi-nhat.rss','https://thanhnien.vn/rss/home.rss','https://tuoitre.vn/rss/tin-moi-nhat.rss','https://genk.vn/rss','https://vnexpress.net/rss/the-thao.rss','https://thethaovanhoa.vn/rss/tin-nong.rss']
for feed_url in feeds:
try:
r=req.get(feed_url,headers={'User-Agent':'Mozilla/5.0'},timeout=6);r.encoding='utf-8';soup=BeautifulSoup(r.text,'xml')
for item in soup.find_all('item')[:12]:
title=_clean(item.find('title').get_text() if item.find('title') else '')
if not title:continue
title=re.sub(r'\s*[-|].*$','',title);words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',title) if len(w)>2 and w.lower() not in _STOP]
if len(words)<2:continue
for n in(3,4,2):
for i in range(max(0,len(words)-n+1)):
phrase=' '.join(words[i:i+n])
if 8<=len(phrase)<=45:key=phrase.lower();freq[key]=freq.get(key,0)+1;display[key]=phrase
except:continue
ranked=sorted(freq.items(),key=lambda x:x[1],reverse=True);topics=[];seen=set()
for key,count in ranked:
is_dup=any(len(set(e.split())&set(key.split()))/max(len(set(e.split())),len(set(key.split())),1)>0.6 for e in seen)
if is_dup:continue
seen.add(key);topics.append({'label':'#'+re.sub(r'\s+','',display[key].title()),'topic':display[key],'count':count})
if len(topics)>=20:break
for kw in['World Cup 2026','Kinh tế Việt Nam','Bóng đá châu Âu','Công nghệ AI','Giá vàng','Thời tiết']:
if len(topics)>=24:break
if not any(kw.lower() in s for s in seen):topics.append({'label':'#'+re.sub(r'\s+','',kw.title()),'topic':kw,'count':0})
_hot_cache.update({'t':now,'d':topics[:24]});return topics[:24]
@app.get('/api/hot_topics')
def api_hot_topics():return JSONResponse({'topics':_get_hot_topics()})
@app.get('/')
async def serve_index():
p=os.path.join(STATIC_DIR,'index_v2.html')
if os.path.exists(p):return FileResponse(p,media_type='text/html')
return HTMLResponse('<h1>VNEWS</h1>')
@app.get('/api/hashtag/sources')
def _ht(topic:str=Query(...),page:int=Query(default=0)):
items=_search_all(topic,36);per_page=8;start=page*per_page;end=start+per_page
return JSONResponse({'sources':items[start:end],'topic':topic,'page':page,'has_more':end<len(items),'total':len(items)})
@app.get('/api/categories')
def _cat():return JSONResponse([])
@app.get('/api/storage_status')
def _st():return JSONResponse({'persistent':os.path.isdir('/data') and os.access('/data',os.W_OK)})
@app.get('/s')
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>')
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)
# === XEMLAIBONGDA.PROXY (CORS workaround for WC highlights) ===
_xlb_cache = {}
_xlb_lock = threading.Lock()
def _xlb_scrape(path):
"""Scrape xemlaibongda.top and extract video articles."""
url = f"https://xemlaibongda.top/{path}"
r = req.get(url, headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"}, timeout=15, allow_redirects=True)
if r.status_code != 200:
return []
soup = BeautifulSoup(r.text, 'lxml')
vids = []
seen = set()
for a in soup.select('a[href*="/video/"]'):
href = a.get('href', '')
if not href or href in seen:
continue
seen.add(href)
if not href.startswith('http'):
href = 'https://xemlaibongda.top' + href
img = ''
img_el = a.select_one('img')
if img_el:
img = img_el.get('src', '') or img_el.get('data-src', '')
if img.startswith('//'):
img = 'https:' + img
elif img.startswith('/'):
img = 'https://xemlaibongda.top' + img
title = ''
for sel in ['.title', 'h3', 'h2', '.name', '.post-title']:
t = a.select_one(sel)
if t:
title = _clean(t.get_text())
break
if not title:
title = _clean(a.get('title', '') or a.get_text())
if title and len(title) > 3:
vids.append({"link": href, "img": img, "title": title})
if len(vids) >= 30:
break
return vids
@app.get('/api/proxy/xlb')
def proxy_xlb(path: str = Query(default="")):
"""Proxy for xemlaibongda.top to avoid CORS in browser."""
now = time.time()
cache_key = f"xlb:{path}"
with _xlb_lock:
cached = _xlb_cache.get(cache_key)
if cached and now - cached['t'] < 120: # 2 min cache
return JSONResponse(cached['d'])
try:
vids = _xlb_scrape(path)
result = {"videos": vids, "count": len(vids)}
with _xlb_lock:
_xlb_cache[cache_key] = {'t': now, 'd': result}
return JSONResponse(result)
except Exception as e:
return JSONResponse({"videos": [], "count": 0, "error": str(e)}, status_code=500)
@app.get('/api/wc2026')
def _w():return JSONResponse(get_wc2026_all())
@app.get('/api/wc2026/fixtures')
def _wf():return JSONResponse(scrape_fixtures())
@app.get('/api/wc2026/standings')
def _ws():return JSONResponse(scrape_standings())
@app.get('/api/wc2026/stats')
def _wst():return JSONResponse(scrape_stats())
@app.get('/api/wc2026/history')
def _whi():return JSONResponse(scrape_history())
@app.get('/api/wc2026/news')
def _wn():return JSONResponse(scrape_wc_news())
@app.get('/api/wc2026/road')
def _wr():return JSONResponse(scrape_road_to_wc())
@app.get('/api/wc2026/h2h/{eid}')
def _wh2(eid:int):return JSONResponse(scrape_h2h(eid))
@app.get('/api/wc2026/lineups/{eid}')
def _wl(eid:int):return JSONResponse(scrape_lineups(eid))
@app.get('/api/wc2026/match/{eid}')
def _wm(eid:int):return JSONResponse(scrape_match_detail(eid))
DATA_DIR='/data' if os.path.isdir('/data') else os.path.join(os.path.dirname(os.path.abspath(__file__)),'data')
os.makedirs(DATA_DIR,exist_ok=True);IF=os.path.join(DATA_DIR,'interactions_v2.json');CF=os.path.join(DATA_DIR,'comments_v2.json')
_il=threading.Lock();_cl=threading.Lock()
def _lj(p):
try:
if os.path.exists(p):return json.load(open(p,'r',encoding='utf-8'))
except:pass
return{}
def _sj(p,d):
try:open(p+'.tmp','w',encoding='utf-8').write(json.dumps(d,ensure_ascii=False));os.replace(p+'.tmp',p)
except:pass
@app.post('/api/v2/interact')
async def _int(request:Request):
b=await request.json();v=str(b.get('id','')).strip();t=str(b.get('type','')).strip()
if not v or t not in('view','like'):return JSONResponse({'error':'x'},status_code=400)
with _il:db=_lj(IF);db.setdefault(v,{'views':0,'likes':0,'comments':0});db[v][t+'s']+=1;_sj(IF,db);return JSONResponse(db[v])
@app.get('/api/v2/interactions')
def _gi(id:str=Query(...)):
with _il:return JSONResponse(_lj(IF).get(id.strip(),{'views':0,'likes':0,'comments':0}))
@app.get('/api/v2/comments')
def _gc(id:str=Query(...)):
with _cl:return JSONResponse({'comments':_lj(CF).get(id.strip(),[])})
@app.post('/api/v2/comment')
async def _pc(request:Request):
b=await request.json();v=str(b.get('id','')).strip();tx=str(b.get('text','')).strip()[:500]
if not v or not tx:return JSONResponse({'error':'x'},status_code=400)
c={'text':tx,'time':time.strftime('%H:%M %d/%m',time.localtime()),'ts':int(time.time())}
with _cl:db=_lj(CF);db.setdefault(v,[]);db[v].append(c);db[v]=db[v][-200:];_sj(CF,db);cms=db[v]
with _il:idb=_lj(IF);idb.setdefault(v,{'views':0,'likes':0,'comments':0});idb[v]['comments']=len(cms);_sj(IF,idb)
return JSONResponse({'comments':cms})
def _bg():
time.sleep(15)
while True:
try:get_wc2026_all()
except:pass
time.sleep(90)
threading.Thread(target=_bg,daemon=True).start()
app.mount('/static',StaticFiles(directory=STATIC_DIR),name='vnews_static')