Spaces:
Running
Running
Fix: sort YouTube by date (newest first) + stop iframe audio on slide change
Browse files
main.py
CHANGED
|
@@ -17,7 +17,7 @@ SPACE_URL = "https://bep40-vnews.hf.space"
|
|
| 17 |
_cache = {}
|
| 18 |
_cache_ttl = 300
|
| 19 |
_cache_ttl_live = 60
|
| 20 |
-
_cache_ttl_yt = 1800
|
| 21 |
PRIORITY_LEAGUES = ["Ngoại Hạng Anh","FA Cup","Champions League","LaLiga","Copa del Rey","Serie A","Bundesliga","Ligue 1","V-League"]
|
| 22 |
LEAGUE_IDS = {"nha":27110,"laliga":27233,"seriea":27044,"bundesliga":26891,"ligue1":27212}
|
| 23 |
def _cached(key, fn, ttl=None):
|
|
@@ -63,9 +63,9 @@ def _parse_match_from_li(li, status_type="live"):
|
|
| 63 |
|
| 64 |
# ===== YOUTUBE FPT BÓNG ĐÁ =====
|
| 65 |
def _yt_playlist(channel_path, count=15):
|
| 66 |
-
"""Get
|
| 67 |
try:
|
| 68 |
-
result=subprocess.run(["yt-dlp","--flat-playlist","--dump-json","--no-download","--playlist-end",str(count),f"https://www.youtube.com/@fptbongdaofficial/{channel_path}"],capture_output=True,text=True,timeout=60)
|
| 69 |
videos=[]
|
| 70 |
for line in result.stdout.strip().split('\n'):
|
| 71 |
if not line:continue
|
|
@@ -74,7 +74,6 @@ def _yt_playlist(channel_path, count=15):
|
|
| 74 |
vid=info.get("id","")
|
| 75 |
title=info.get("title","")
|
| 76 |
if not vid or not title:continue
|
| 77 |
-
# Thumbnail: use YouTube default
|
| 78 |
thumb=f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg"
|
| 79 |
videos.append({"title":title,"link":f"https://www.youtube.com/watch?v={vid}","img":thumb,"source":"fpt","id":vid})
|
| 80 |
except:pass
|
|
@@ -82,11 +81,9 @@ def _yt_playlist(channel_path, count=15):
|
|
| 82 |
except:return[]
|
| 83 |
|
| 84 |
def scrape_fpt_shorts():
|
| 85 |
-
"""Get YouTube Shorts from FPT Bóng Đá."""
|
| 86 |
return _yt_playlist("shorts", 15)
|
| 87 |
|
| 88 |
def scrape_fpt_highlights():
|
| 89 |
-
"""Get YouTube Videos from FPT Bóng Đá."""
|
| 90 |
return _yt_playlist("videos", 15)
|
| 91 |
|
| 92 |
# ===== LIVESCORE =====
|
|
@@ -146,28 +143,20 @@ def api_livescore_featured():
|
|
| 146 |
return all_matches[0]
|
| 147 |
return None
|
| 148 |
return JSONResponse(_cached("ls_featured",_f,ttl=30))
|
| 149 |
-
|
| 150 |
# ===== VIDEO APIs =====
|
| 151 |
@app.get("/api/shorts")
|
| 152 |
def api_shorts():
|
| 153 |
-
"""YouTube Shorts from FPT Bóng Đá."""
|
| 154 |
return JSONResponse(_cached("fpt_shorts",scrape_fpt_shorts,ttl=_cache_ttl_yt))
|
| 155 |
-
|
| 156 |
@app.get("/api/highlights")
|
| 157 |
def api_highlights():
|
| 158 |
-
"""YouTube Videos from FPT Bóng Đá."""
|
| 159 |
return JSONResponse(_cached("fpt_highlights",scrape_fpt_highlights,ttl=_cache_ttl_yt))
|
| 160 |
-
|
| 161 |
@app.get("/api/video_url")
|
| 162 |
def api_video_url(url:str=Query(...)):
|
| 163 |
-
"""Get playable video URL. YouTube → embed URL. BDP → MP4."""
|
| 164 |
-
# YouTube
|
| 165 |
if "youtube.com" in url or "youtu.be" in url:
|
| 166 |
m=re.search(r'(?:v=|shorts/|youtu\.be/)([a-zA-Z0-9_-]{11})',url)
|
| 167 |
if m:
|
| 168 |
vid=m.group(1)
|
| 169 |
-
return JSONResponse({"src":f"https://www.youtube.com/embed/{vid}?autoplay=1&rel=0","poster":f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg","type":"youtube"})
|
| 170 |
-
# BDP
|
| 171 |
if "bongdaplus.vn" in url:
|
| 172 |
try:
|
| 173 |
m=re.search(r'-(\d{6,})\.html',url)
|
|
@@ -179,7 +168,6 @@ def api_video_url(url:str=Query(...)):
|
|
| 179 |
return JSONResponse({"src":source.get("src","") if source else "","poster":video.get("poster",""),"type":"video"})
|
| 180 |
except:pass
|
| 181 |
return JSONResponse({"error":"not found"})
|
| 182 |
-
|
| 183 |
@app.get("/api/bdp_videos")
|
| 184 |
def api_bdp_videos():
|
| 185 |
def _f():
|
|
@@ -198,8 +186,7 @@ def api_bdp_videos():
|
|
| 198 |
return arts[:20]
|
| 199 |
except:return[]
|
| 200 |
return JSONResponse(_cached("bdp_videos",_f))
|
| 201 |
-
|
| 202 |
-
# ===== NEWS SCRAPERS =====
|
| 203 |
def scrape_vne(cat_url):
|
| 204 |
try:
|
| 205 |
soup=_get(cat_url);arts=[]
|
|
@@ -267,8 +254,7 @@ def scrape_dantri_article(url):
|
|
| 267 |
except:return None
|
| 268 |
def scrape_bbc_vietnamese():
|
| 269 |
try:
|
| 270 |
-
|
| 271 |
-
r=requests.get("https://www.bbc.com/vietnamese",headers=bbc_h,timeout=15);r.encoding="utf-8"
|
| 272 |
soup=BeautifulSoup(r.text,"lxml");arts=[];seen=set()
|
| 273 |
for a in soup.select("a[href*='/vietnamese/']"):
|
| 274 |
href=a.get("href","")
|
|
@@ -276,8 +262,7 @@ def scrape_bbc_vietnamese():
|
|
| 276 |
if not href.startswith("http"):href="https://www.bbc.com"+href
|
| 277 |
if href in seen:continue
|
| 278 |
title=a.get_text(strip=True)
|
| 279 |
-
if not title or len(title)<15:continue
|
| 280 |
-
if any(x in title.lower() for x in["đăng nhập","trang chủ","bbc news"]):continue
|
| 281 |
img="";container=a.parent
|
| 282 |
for _ in range(3):
|
| 283 |
if container:
|
|
@@ -339,13 +324,13 @@ async def video_share(url:str=Query(default=""),title:str=Query(default="VNEWS V
|
|
| 339 |
og_image=unquote(img) if img else "https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg"
|
| 340 |
decoded_url=unquote(url);decoded_title=unquote(title)
|
| 341 |
redirect_script=f'<script>localStorage.setItem("pending_video",JSON.stringify({{"url":"{decoded_url}","type":"{type}"}}));location.href="{SPACE_URL}";</script>' if decoded_url else f'<script>location.href="{SPACE_URL}";</script>'
|
| 342 |
-
return HTMLResponse(f'<!DOCTYPE html><html><head><meta charset="utf-8"><title>{decoded_title}</title><
|
| 343 |
@app.get("/s")
|
| 344 |
async def share_redirect(url:str=Query(default=""),title:str=Query(default="VNEWS"),img:str=Query(default="")):
|
| 345 |
og_image=unquote(img) if img else "https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg"
|
| 346 |
decoded_url=unquote(url)
|
| 347 |
redirect_script=f'<script>localStorage.setItem("pending_article","{decoded_url}");location.href="{SPACE_URL}";</script>' if decoded_url else f'<script>location.href="{SPACE_URL}";</script>'
|
| 348 |
-
return HTMLResponse(f'<!DOCTYPE html><html><head><meta charset="utf-8"><title>{unquote(title)}</title><
|
| 349 |
@app.get("/")
|
| 350 |
async def index():
|
| 351 |
with open("/app/static/index.html","r",encoding="utf-8") as f:return HTMLResponse(content=f.read())
|
|
|
|
| 17 |
_cache = {}
|
| 18 |
_cache_ttl = 300
|
| 19 |
_cache_ttl_live = 60
|
| 20 |
+
_cache_ttl_yt = 1800
|
| 21 |
PRIORITY_LEAGUES = ["Ngoại Hạng Anh","FA Cup","Champions League","LaLiga","Copa del Rey","Serie A","Bundesliga","Ligue 1","V-League"]
|
| 22 |
LEAGUE_IDS = {"nha":27110,"laliga":27233,"seriea":27044,"bundesliga":26891,"ligue1":27212}
|
| 23 |
def _cached(key, fn, ttl=None):
|
|
|
|
| 63 |
|
| 64 |
# ===== YOUTUBE FPT BÓNG ĐÁ =====
|
| 65 |
def _yt_playlist(channel_path, count=15):
|
| 66 |
+
"""Get LATEST videos from YouTube channel sorted by date."""
|
| 67 |
try:
|
| 68 |
+
result=subprocess.run(["yt-dlp","--flat-playlist","--dump-json","--no-download","--playlist-end",str(count),"--playlist-sort","date",f"https://www.youtube.com/@fptbongdaofficial/{channel_path}"],capture_output=True,text=True,timeout=60)
|
| 69 |
videos=[]
|
| 70 |
for line in result.stdout.strip().split('\n'):
|
| 71 |
if not line:continue
|
|
|
|
| 74 |
vid=info.get("id","")
|
| 75 |
title=info.get("title","")
|
| 76 |
if not vid or not title:continue
|
|
|
|
| 77 |
thumb=f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg"
|
| 78 |
videos.append({"title":title,"link":f"https://www.youtube.com/watch?v={vid}","img":thumb,"source":"fpt","id":vid})
|
| 79 |
except:pass
|
|
|
|
| 81 |
except:return[]
|
| 82 |
|
| 83 |
def scrape_fpt_shorts():
|
|
|
|
| 84 |
return _yt_playlist("shorts", 15)
|
| 85 |
|
| 86 |
def scrape_fpt_highlights():
|
|
|
|
| 87 |
return _yt_playlist("videos", 15)
|
| 88 |
|
| 89 |
# ===== LIVESCORE =====
|
|
|
|
| 143 |
return all_matches[0]
|
| 144 |
return None
|
| 145 |
return JSONResponse(_cached("ls_featured",_f,ttl=30))
|
|
|
|
| 146 |
# ===== VIDEO APIs =====
|
| 147 |
@app.get("/api/shorts")
|
| 148 |
def api_shorts():
|
|
|
|
| 149 |
return JSONResponse(_cached("fpt_shorts",scrape_fpt_shorts,ttl=_cache_ttl_yt))
|
|
|
|
| 150 |
@app.get("/api/highlights")
|
| 151 |
def api_highlights():
|
|
|
|
| 152 |
return JSONResponse(_cached("fpt_highlights",scrape_fpt_highlights,ttl=_cache_ttl_yt))
|
|
|
|
| 153 |
@app.get("/api/video_url")
|
| 154 |
def api_video_url(url:str=Query(...)):
|
|
|
|
|
|
|
| 155 |
if "youtube.com" in url or "youtu.be" in url:
|
| 156 |
m=re.search(r'(?:v=|shorts/|youtu\.be/)([a-zA-Z0-9_-]{11})',url)
|
| 157 |
if m:
|
| 158 |
vid=m.group(1)
|
| 159 |
+
return JSONResponse({"src":f"https://www.youtube.com/embed/{vid}?autoplay=1&rel=0&enablejsapi=1","poster":f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg","type":"youtube"})
|
|
|
|
| 160 |
if "bongdaplus.vn" in url:
|
| 161 |
try:
|
| 162 |
m=re.search(r'-(\d{6,})\.html',url)
|
|
|
|
| 168 |
return JSONResponse({"src":source.get("src","") if source else "","poster":video.get("poster",""),"type":"video"})
|
| 169 |
except:pass
|
| 170 |
return JSONResponse({"error":"not found"})
|
|
|
|
| 171 |
@app.get("/api/bdp_videos")
|
| 172 |
def api_bdp_videos():
|
| 173 |
def _f():
|
|
|
|
| 186 |
return arts[:20]
|
| 187 |
except:return[]
|
| 188 |
return JSONResponse(_cached("bdp_videos",_f))
|
| 189 |
+
# ===== NEWS =====
|
|
|
|
| 190 |
def scrape_vne(cat_url):
|
| 191 |
try:
|
| 192 |
soup=_get(cat_url);arts=[]
|
|
|
|
| 254 |
except:return None
|
| 255 |
def scrape_bbc_vietnamese():
|
| 256 |
try:
|
| 257 |
+
r=requests.get("https://www.bbc.com/vietnamese",headers={"User-Agent":"Mozilla/5.0","Accept-Language":"en-GB"},timeout=15);r.encoding="utf-8"
|
|
|
|
| 258 |
soup=BeautifulSoup(r.text,"lxml");arts=[];seen=set()
|
| 259 |
for a in soup.select("a[href*='/vietnamese/']"):
|
| 260 |
href=a.get("href","")
|
|
|
|
| 262 |
if not href.startswith("http"):href="https://www.bbc.com"+href
|
| 263 |
if href in seen:continue
|
| 264 |
title=a.get_text(strip=True)
|
| 265 |
+
if not title or len(title)<15 or any(x in title.lower() for x in["đăng nhập","trang chủ","bbc news"]):continue
|
|
|
|
| 266 |
img="";container=a.parent
|
| 267 |
for _ in range(3):
|
| 268 |
if container:
|
|
|
|
| 324 |
og_image=unquote(img) if img else "https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg"
|
| 325 |
decoded_url=unquote(url);decoded_title=unquote(title)
|
| 326 |
redirect_script=f'<script>localStorage.setItem("pending_video",JSON.stringify({{"url":"{decoded_url}","type":"{type}"}}));location.href="{SPACE_URL}";</script>' if decoded_url else f'<script>location.href="{SPACE_URL}";</script>'
|
| 327 |
+
return HTMLResponse(f'<!DOCTYPE html><html><head><meta charset="utf-8"><title>{decoded_title}</title></head><body style="background:#111;color:#fff;text-align:center;padding:40px"><p>⏳</p>{redirect_script}</body></html>')
|
| 328 |
@app.get("/s")
|
| 329 |
async def share_redirect(url:str=Query(default=""),title:str=Query(default="VNEWS"),img:str=Query(default="")):
|
| 330 |
og_image=unquote(img) if img else "https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg"
|
| 331 |
decoded_url=unquote(url)
|
| 332 |
redirect_script=f'<script>localStorage.setItem("pending_article","{decoded_url}");location.href="{SPACE_URL}";</script>' if decoded_url else f'<script>location.href="{SPACE_URL}";</script>'
|
| 333 |
+
return HTMLResponse(f'<!DOCTYPE html><html><head><meta charset="utf-8"><title>{unquote(title)}</title></head><body>{redirect_script}</body></html>')
|
| 334 |
@app.get("/")
|
| 335 |
async def index():
|
| 336 |
with open("/app/static/index.html","r",encoding="utf-8") as f:return HTMLResponse(content=f.read())
|