Spaces:
Running
Running
Add persistent shorts stats/comments API (views, likes, shares, comments)
Browse files
main.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""VNEWS - FastAPI backend with livescore + xemlaibongda highlights + YouTube FPT shorts"""
|
| 2 |
-
import hashlib, re, time, subprocess, json
|
| 3 |
import html as html_lib
|
| 4 |
from datetime import datetime
|
| 5 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
@@ -19,6 +19,23 @@ _cache = {}
|
|
| 19 |
_cache_ttl = 300
|
| 20 |
_cache_ttl_live = 60
|
| 21 |
_cache_ttl_yt = 1800
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
PRIORITY_LEAGUES = ["Ngoại Hạng Anh","FA Cup","Champions League","LaLiga","Copa del Rey","Serie A","Bundesliga","Ligue 1","V-League"]
|
| 23 |
LEAGUE_IDS = {"nha":27110,"laliga":27233,"seriea":27044,"bundesliga":26891,"ligue1":27212}
|
| 24 |
HL_LEAGUES = {"premier-league":{"path":"anh/premier-league","name":"Premier League","emoji":"🏴"},"fa-cup":{"path":"anh/fa-cup","name":"FA Cup","emoji":"🏆"},"bundesliga":{"path":"duc/bundesliga","name":"Bundesliga","emoji":"🇩🇪"},"serie-a":{"path":"italy/serie-a","name":"Serie A","emoji":"🇮🇹"},"la-liga":{"path":"tay-ban-nha/la-liga","name":"La Liga","emoji":"🇪🇸"},"champions-league":{"path":"cup-chau-au/uefa-champions-league","name":"Champions League","emoji":"⭐"},"europa-league":{"path":"cup-chau-au/uefa-europa-league","name":"Europa League","emoji":"🟠"},"world-cup":{"path":"the-gioi/world-cup-qualifiers","name":"World Cup 2026","emoji":"🌍"}}
|
|
@@ -246,6 +263,35 @@ def api_livescore_featured():
|
|
| 246 |
# ===== VIDEO APIs =====
|
| 247 |
@app.get("/api/shorts")
|
| 248 |
def api_shorts():return JSONResponse(_cached("yt_shorts",scrape_shorts,ttl=_cache_ttl_yt))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
@app.get("/api/highlights")
|
| 250 |
def api_highlights():return JSONResponse(_cached("xemlaibongda_hl",scrape_xemlaibongda,ttl=_cache_ttl))
|
| 251 |
@app.get("/api/highlights/leagues")
|
|
|
|
| 1 |
"""VNEWS - FastAPI backend with livescore + xemlaibongda highlights + YouTube FPT shorts"""
|
| 2 |
+
import hashlib, re, time, subprocess, json, os, threading
|
| 3 |
import html as html_lib
|
| 4 |
from datetime import datetime
|
| 5 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
| 19 |
_cache_ttl = 300
|
| 20 |
_cache_ttl_live = 60
|
| 21 |
_cache_ttl_yt = 1800
|
| 22 |
+
SHORT_STATS_FILE = "/data/short_stats.json" if os.path.isdir("/data") else "/app/short_stats.json"
|
| 23 |
+
_short_lock = threading.Lock()
|
| 24 |
+
def _load_short_db():
|
| 25 |
+
try:
|
| 26 |
+
if os.path.exists(SHORT_STATS_FILE):
|
| 27 |
+
with open(SHORT_STATS_FILE,"r",encoding="utf-8") as f:return json.load(f)
|
| 28 |
+
except:pass
|
| 29 |
+
return {}
|
| 30 |
+
def _save_short_db(db):
|
| 31 |
+
try:
|
| 32 |
+
os.makedirs(os.path.dirname(SHORT_STATS_FILE),exist_ok=True)
|
| 33 |
+
tmp=SHORT_STATS_FILE+".tmp"
|
| 34 |
+
with open(tmp,"w",encoding="utf-8") as f:json.dump(db,f,ensure_ascii=False)
|
| 35 |
+
os.replace(tmp,SHORT_STATS_FILE)
|
| 36 |
+
except:pass
|
| 37 |
+
|
| 38 |
+
def _short_default():return {"views":0,"likes":0,"shares":0,"comments":[]}
|
| 39 |
PRIORITY_LEAGUES = ["Ngoại Hạng Anh","FA Cup","Champions League","LaLiga","Copa del Rey","Serie A","Bundesliga","Ligue 1","V-League"]
|
| 40 |
LEAGUE_IDS = {"nha":27110,"laliga":27233,"seriea":27044,"bundesliga":26891,"ligue1":27212}
|
| 41 |
HL_LEAGUES = {"premier-league":{"path":"anh/premier-league","name":"Premier League","emoji":"🏴"},"fa-cup":{"path":"anh/fa-cup","name":"FA Cup","emoji":"🏆"},"bundesliga":{"path":"duc/bundesliga","name":"Bundesliga","emoji":"🇩🇪"},"serie-a":{"path":"italy/serie-a","name":"Serie A","emoji":"🇮🇹"},"la-liga":{"path":"tay-ban-nha/la-liga","name":"La Liga","emoji":"🇪🇸"},"champions-league":{"path":"cup-chau-au/uefa-champions-league","name":"Champions League","emoji":"⭐"},"europa-league":{"path":"cup-chau-au/uefa-europa-league","name":"Europa League","emoji":"🟠"},"world-cup":{"path":"the-gioi/world-cup-qualifiers","name":"World Cup 2026","emoji":"🌍"}}
|
|
|
|
| 263 |
# ===== VIDEO APIs =====
|
| 264 |
@app.get("/api/shorts")
|
| 265 |
def api_shorts():return JSONResponse(_cached("yt_shorts",scrape_shorts,ttl=_cache_ttl_yt))
|
| 266 |
+
@app.get("/api/short-stats")
|
| 267 |
+
def api_short_stats(ids:str=Query(default="")):
|
| 268 |
+
arr=[x for x in ids.split(",") if x]
|
| 269 |
+
with _short_lock:
|
| 270 |
+
db=_load_short_db();out={}
|
| 271 |
+
for vid in arr:
|
| 272 |
+
st=db.get(vid) or _short_default()
|
| 273 |
+
out[vid]={"views":int(st.get("views",0)),"likes":int(st.get("likes",0)),"shares":int(st.get("shares",0)),"comments":st.get("comments",[])[:80]}
|
| 274 |
+
return JSONResponse({"stats":out})
|
| 275 |
+
|
| 276 |
+
@app.post("/api/short-action")
|
| 277 |
+
async def api_short_action(request:Request):
|
| 278 |
+
try:body=await request.json()
|
| 279 |
+
except:body={}
|
| 280 |
+
vid=str(body.get("id","")).strip();action=str(body.get("action","")).strip();txt=str(body.get("text","")).strip()
|
| 281 |
+
if not vid:return JSONResponse({"error":"missing id"},status_code=400)
|
| 282 |
+
with _short_lock:
|
| 283 |
+
db=_load_short_db();st=db.get(vid) or _short_default()
|
| 284 |
+
if action=="view":st["views"]=int(st.get("views",0))+1
|
| 285 |
+
elif action=="like":st["likes"]=int(st.get("likes",0))+1
|
| 286 |
+
elif action=="share":st["shares"]=int(st.get("shares",0))+1
|
| 287 |
+
elif action=="comment" and txt:
|
| 288 |
+
comments=st.get("comments",[])
|
| 289 |
+
comments.insert(0,{"text":txt[:180],"ts":int(time.time())})
|
| 290 |
+
st["comments"]=comments[:80]
|
| 291 |
+
st["updated"]=int(time.time());db[vid]=st;_save_short_db(db)
|
| 292 |
+
out={"views":int(st.get("views",0)),"likes":int(st.get("likes",0)),"shares":int(st.get("shares",0)),"comments":st.get("comments",[])[:80]}
|
| 293 |
+
return JSONResponse({"stats":out})
|
| 294 |
+
|
| 295 |
@app.get("/api/highlights")
|
| 296 |
def api_highlights():return JSONResponse(_cached("xemlaibongda_hl",scrape_xemlaibongda,ttl=_cache_ttl))
|
| 297 |
@app.get("/api/highlights/leagues")
|