VNEWS / app_main.py
bep40's picture
Upload app_main.py with huggingface_hub
01cd6d5 verified
Raw
History Blame
13.6 kB
"""VNEWS v2 - Clean frontend. CRITICAL: removes ALL old routes before registering new ones."""
from app_run import *
from app_run import app, f5, f6, rt, PATCH_INJECT, UNIFIED_INJECT_FIXED, HIGHLIGHT_FULL_OVERRIDE, EXTRA_WALL_FIX, FAST_HASHTAG_JS
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse, Response
from fastapi.staticfiles import StaticFiles
from fastapi import Query, Request
import requests as req
from urllib.parse import quote
from bs4 import BeautifulSoup
import re, html as html_lib, os, json, threading, time
from concurrent.futures import ThreadPoolExecutor, as_completed
def _clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or ""))).strip()
_STOP_WORDS=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ì'.split())
def _relevance_score(topic, title):
topic_lower = topic.lower().strip();title_lower = (title or '').lower()
if topic_lower in title_lower: return 10
topic_words = [w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+', topic_lower) if len(w) > 1 and w not in _STOP_WORDS]
if not topic_words: return 0
matched = sum(1 for w in topic_words if w in title_lower)
ratio = matched / len(topic_words) if topic_words else 0
return int(ratio * 8) if ratio >= 0.6 else 0
def _search_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'):items.append({'title':_clean(a.get('title','') or a.get_text(strip=True)),'url':a['href'],'via':'VnExpress'})
except:pass
return items
def _search_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:
if not href.startswith('http'):href='https://dantri.com.vn'+href
if 'dantri.com.vn' in href:items.append({'title':t,'url':href,'via':'Dân Trí'})
if len(items)>=limit:break
except:pass
return items
def _search_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], .horizontalPost__main-title a')[:limit*2]:
t=_clean(a.get_text(strip=True));href=a.get('href','')
if t and len(t)>15:
if not href.startswith('http'):href='https://vietnamnet.vn'+href
if 'vietnamnet.vn' in href:items.append({'title':t,'url':href,'via':'VietNamNet'})
if len(items)>=limit:break
except:pass
return items
def _search_all(topic, limit=40):
all_items=[]
with ThreadPoolExecutor(5) as ex:
futs=[ex.submit(_search_vnexpress,topic,10),ex.submit(_search_dantri,topic,10),ex.submit(_search_vietnamnet,topic,8)]
for f in as_completed(futs,timeout=12):
try:all_items.extend(f.result())
except:pass
seen=set();unique=[]
for i in all_items:
if i.get('url') and i['url'] not in seen:seen.add(i['url']);unique.append(i)
return unique[:limit]
# Remove old routes
app.router.routes = [r for r in app.router.routes if not (
(getattr(r, 'path', None) == '/' and 'GET' in getattr(r, 'methods', set())) or
(getattr(r, 'path', None) == '/api/hashtag/sources' and 'GET' in getattr(r, 'methods', set())) or
(getattr(r, 'path', None) in ('/api/short/comments', '/api/short/comment'))
)]
app.routes[:] = [r for r in app.routes if not (
hasattr(r, 'path') and getattr(r, 'path', None) == '/' and
hasattr(r, 'methods') and 'GET' in getattr(r, 'methods', set())
)]
STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
@app.get('/api/hashtag/sources')
def _ht(topic:str=Query(...), page:int=Query(default=0)):
all_items=_search_all(topic, 40)
scored = [(s,item) for item in all_items if (s:=_relevance_score(topic, item.get('title','')))>0]
scored.sort(key=lambda x: x[0], reverse=True)
filtered = [item for _, item in scored]
if len(filtered) < 3: filtered = all_items
per_page=6;start=page*per_page;end=start+per_page
return JSONResponse({'sources':filtered[start:end],'topic':topic,'page':page,'has_more':end<len(filtered),'total':len(filtered)})
@app.get('/api/categories')
def _categories():return JSONResponse([])
@app.get('/api/storage_status')
def _storage():return JSONResponse({'persistent':os.path.isdir('/data') and os.access('/data', os.W_OK)})
@app.get('/s')
async def _share(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>Redirecting...</body></html>')
@app.get('/api/proxy/page')
def proxy_page(url: str = Query(...)):
try:
r = req.get(url, headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36','Accept-Language':'vi-VN,vi;q=0.9','Referer':'https://hd.xemtv.net/'}, timeout=15)
return HTMLResponse(content=r.text)
except:
return HTMLResponse(content='', status_code=502)
@app.get('/api/proxy/hls')
def proxy_hls(url: str = Query(...)):
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': '*/*',
'Accept-Language': 'vi-VN,vi;q=0.9',
'Referer': 'https://fptplay.vn/',
'Origin': 'https://fptplay.vn',
}
r = req.get(url, headers=headers, timeout=15)
content_type = r.headers.get('Content-Type', 'application/vnd.apple.mpegurl')
text = r.text
base_url = url.rsplit('/', 1)[0] + '/'
def _rewrite_url(m):
seg_url = m.group(0)
if seg_url.startswith('http'):
return '/api/proxy/seg?url=' + quote(seg_url, safe='')
elif seg_url.startswith('/'):
return '/api/proxy/seg?url=' + quote(base_url.rsplit('/', 2)[0] + seg_url, safe='')
else:
return '/api/proxy/seg?url=' + quote(base_url + seg_url, safe='')
text = re.sub(r'https?://[^\s"\'<>]+\.(ts|m3u8)[^\s"\'<>]*', _rewrite_url, text)
return HTMLResponse(content=text, media_type=content_type)
except:
return HTMLResponse(content='', status_code=502)
@app.get('/api/proxy/seg')
def proxy_seg(url: str = Query(...)):
try:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': 'https://fptplay.vn/',
'Origin': 'https://fptplay.vn',
}
r = req.get(url, headers=headers, timeout=15)
content_type = r.headers.get('Content-Type', 'video/MP2T')
return Response(content=r.content, media_type=content_type)
except:
return Response(content=b'', status_code=502)
# Interactions
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)
INTERACTIONS_FILE = os.path.join(DATA_DIR, 'interactions_v2.json')
COMMENTS_FILE = os.path.join(DATA_DIR, 'comments_v2.json')
_interact_lock = threading.Lock()
_comment_lock = threading.Lock()
def _load_json(path):
try:
if os.path.exists(path):
with open(path,'r',encoding='utf-8') as f:return json.load(f)
except:pass
return {}
def _save_json(path, data):
try:
tmp=path+'.tmp'
with open(tmp,'w',encoding='utf-8') as f:json.dump(data,f,ensure_ascii=False)
os.replace(tmp,path)
except:pass
@app.post('/api/v2/interact')
async def api_interact(request:Request):
body=await request.json();vid=str(body.get('id','')).strip();itype=str(body.get('type','')).strip()
if not vid or itype not in('view','like'):return JSONResponse({'error':'invalid'},status_code=400)
with _interact_lock:
db=_load_json(INTERACTIONS_FILE)
if vid not in db:db[vid]={'views':0,'likes':0,'comments':0}
db[vid][itype+'s']=db[vid].get(itype+'s',0)+1
_save_json(INTERACTIONS_FILE,db);return JSONResponse(db[vid])
@app.get('/api/v2/interactions')
def api_get_interactions(id:str=Query(...)):
with _interact_lock:return JSONResponse(_load_json(INTERACTIONS_FILE).get(id.strip(),{'views':0,'likes':0,'comments':0}))
@app.get('/api/v2/comments')
def api_get_comments(id:str=Query(...)):
with _comment_lock:return JSONResponse({'comments':_load_json(COMMENTS_FILE).get(id.strip(),[])})
@app.post('/api/v2/comment')
async def api_post_comment(request:Request):
body=await request.json();vid=str(body.get('id','')).strip();text=str(body.get('text','')).strip()[:500]
if not vid or not text:return JSONResponse({'error':'invalid'},status_code=400)
comment={'text':text,'time':time.strftime('%H:%M %d/%m',time.localtime()),'ts':int(time.time())}
with _comment_lock:
db=_load_json(COMMENTS_FILE)
if vid not in db:db[vid]=[]
db[vid].append(comment)
if len(db[vid])>200:db[vid]=db[vid][-200:]
_save_json(COMMENTS_FILE,db);comments=db[vid]
with _interact_lock:
idb=_load_json(INTERACTIONS_FILE)
if vid not in idb:idb[vid]={'views':0,'likes':0,'comments':0}
idb[vid]['comments']=len(comments);_save_json(INTERACTIONS_FILE,idb)
return JSONResponse({'comments':comments})
# World Cup 2026 API
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
)
@app.get('/api/wc2026')
def api_wc2026_all():return JSONResponse(get_wc2026_all())
@app.get('/api/wc2026/summary')
def api_wc2026_summary():return JSONResponse(scrape_summary())
@app.get('/api/wc2026/fixtures')
def api_wc2026_fixtures():return JSONResponse(scrape_fixtures())
@app.get('/api/wc2026/standings')
def api_wc2026_standings():return JSONResponse(scrape_standings())
@app.get('/api/wc2026/stats')
def api_wc2026_stats():return JSONResponse(scrape_stats())
@app.get('/api/wc2026/history')
def api_wc2026_history():return JSONResponse(scrape_history())
@app.get('/api/wc2026/news')
def api_wc2026_news():return JSONResponse(scrape_wc_news())
@app.get('/api/wc2026/road')
def api_wc2026_road():return JSONResponse(scrape_road_to_wc())
@app.get('/api/wc2026/h2h/{event_id}')
def api_wc2026_h2h(event_id:int):return JSONResponse(scrape_h2h(event_id))
@app.get('/api/wc2026/lineups/{event_id}')
def api_wc2026_lineups(event_id:int):return JSONResponse(scrape_lineups(event_id))
@app.get('/api/wc2026/match/{event_id}')
def api_wc2026_match(event_id:int):return JSONResponse(scrape_match_detail(event_id))
# Match Detail API (for any match from bongda.com.vn)
from match_detail import fetch_match_detail, fetch_match_detail_by_url, _bongda_api
@app.get('/api/match/{event_id}/detail')
def api_match_detail(event_id: int, url: str = Query(default=None)):
"""Get complete match detail. Optional 'url' param with full bongda URL (with slug) for HTML scraping."""
if url:
return JSONResponse(fetch_match_detail_by_url(url))
return JSONResponse(fetch_match_detail(event_id))
@app.get('/api/match/{event_id}/commentaries')
def api_match_commentaries(event_id: int):
"""Get match commentaries from bongda API."""
comm = _bongda_api("/api/fixtures/commentaries", {"event_id": event_id})
if comm and comm.get("status") == "success":
html = comm.get("html", "")
if html and len(html.strip()) > 10:
return JSONResponse({"html": html})
return JSONResponse({"html": ""})
@app.get('/api/match/{event_id}/stats')
def api_match_stats(event_id: int):
"""Get match player performance stats from bongda API."""
perf = _bongda_api("/api/event-standing/player-performance", {"event_id": event_id})
if perf and perf.get("status") == "success":
html = perf.get("html", "")
if html and len(html.strip()) > 10:
return JSONResponse({"html": html})
return JSONResponse({"html": ""})
@app.get('/api/match/detail')
def api_match_detail_by_url(url: str = Query(...)):
"""Get match detail by full bongda.com.vn URL."""
return JSONResponse(fetch_match_detail_by_url(url))
def _wc2026_bg_refresh():
time.sleep(10)
while True:
try:get_wc2026_all()
except:pass
time.sleep(90)
threading.Thread(target=_wc2026_bg_refresh,daemon=True).start()
# Serve frontend
@app.get('/')
async def _index_v2():
index_path = os.path.join(STATIC_DIR, 'index_v2.html')
if os.path.exists(index_path):
return FileResponse(index_path, media_type='text/html')
return HTMLResponse('<html><body><h1>VNEWS v2</h1><p>index_v2.html not found</p></body></html>')
app.mount('/static', StaticFiles(directory=STATIC_DIR), name='vnews_static')