Spaces:
Running
Running
v6.3: MyTV scraper cải tiến — dùng URL pattern từ VNEWS matches + verify URL
Browse files
main.py
CHANGED
|
@@ -430,6 +430,97 @@ async def api_short_action(request:Request):
|
|
| 430 |
out={"views":int(st.get("views",0)),"likes":int(st.get("likes",0)),"shares":int(st.get("shares",0)),"comments":st.get("comments",[])[:80]}
|
| 431 |
return JSONResponse({"stats":out})
|
| 432 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
@app.get("/api/highlights")
|
| 434 |
def api_highlights():return JSONResponse(_cached("xemlaibongda_hl",scrape_xemlaibongda,ttl=_cache_ttl))
|
| 435 |
@app.get("/api/highlights/leagues")
|
|
|
|
| 430 |
out={"views":int(st.get("views",0)),"likes":int(st.get("likes",0)),"shares":int(st.get("shares",0)),"comments":st.get("comments",[])[:80]}
|
| 431 |
return JSONResponse({"stats":out})
|
| 432 |
|
| 433 |
+
# ===== MyTV.com.vn Video Scraper =====
|
| 434 |
+
import unicodedata as _ud
|
| 435 |
+
|
| 436 |
+
def _slugify_mytv(s):
|
| 437 |
+
"""Tạo slug cho mytv URL từ tên đội"""
|
| 438 |
+
s = _ud.normalize('NFKD', s)
|
| 439 |
+
s = ''.join(c for c in s if not _ud.combining(c))
|
| 440 |
+
s = s.lower().replace(" ", "-")
|
| 441 |
+
s = re.sub(r'[^a-z0-9-]', '', s)
|
| 442 |
+
return s
|
| 443 |
+
|
| 444 |
+
def scrape_mytv_highlights(limit=20):
|
| 445 |
+
"""Lấy video từ mytv.com.vn cho các trận đấu đã kết thúc.
|
| 446 |
+
Vì mytv là Next.js SPA nên không scrape trực tiếp được.
|
| 447 |
+
Thay vào đó, dùng URL pattern từ các trận đấu trong VNEWS.
|
| 448 |
+
"""
|
| 449 |
+
videos = []
|
| 450 |
+
seen_urls = set()
|
| 451 |
+
|
| 452 |
+
try:
|
| 453 |
+
# Lấy trận đấu đã kết thúc trong 7 ngày qua
|
| 454 |
+
from datetime import datetime as _dt, timedelta as _td
|
| 455 |
+
now = _dt.now()
|
| 456 |
+
|
| 457 |
+
for day_offset in range(7):
|
| 458 |
+
day = now - _td(days=day_offset)
|
| 459 |
+
ds = day.strftime("%Y-%m-%d")
|
| 460 |
+
try:
|
| 461 |
+
day_data = fetch_bongda_api(f"/api/fixtures/get-by-date?date={ds}")
|
| 462 |
+
if not day_data or len(day_data) < 100:
|
| 463 |
+
continue
|
| 464 |
+
soup = BeautifulSoup(day_data, 'lxml')
|
| 465 |
+
for li in soup.select("li.match-detail"):
|
| 466 |
+
status_el = li.select_one('.status .label')
|
| 467 |
+
status_text = status_el.get_text(strip=True) if status_el else ""
|
| 468 |
+
# Chỉ lấy trận đã kết thúc
|
| 469 |
+
if status_text not in ('KT', 'FT', 'Finished'):
|
| 470 |
+
continue
|
| 471 |
+
|
| 472 |
+
home_el = li.select_one('.home-team .name')
|
| 473 |
+
away_el = li.select_one('.away-team .name')
|
| 474 |
+
home = home_el.get_text(strip=True) if home_el else ""
|
| 475 |
+
away = away_el.get_text(strip=True) if away_el else ""
|
| 476 |
+
|
| 477 |
+
if not home or not away:
|
| 478 |
+
continue
|
| 479 |
+
|
| 480 |
+
# Build mytv URL
|
| 481 |
+
home_s = _slugify_mytv(home)
|
| 482 |
+
away_s = _slugify_mytv(away)
|
| 483 |
+
mytv_url = f"https://mytv.com.vn/{home_s}-vs-{away_s}"
|
| 484 |
+
|
| 485 |
+
if mytv_url in seen_urls:
|
| 486 |
+
continue
|
| 487 |
+
seen_urls.add(mytv_url)
|
| 488 |
+
|
| 489 |
+
# Verify URL exists (HEAD request, non-blocking)
|
| 490 |
+
try:
|
| 491 |
+
head_r = requests.head(mytv_url, headers=HEADERS, timeout=5, allow_redirects=True)
|
| 492 |
+
if head_r.status_code == 200 and "anonymous" not in head_r.url:
|
| 493 |
+
videos.append({
|
| 494 |
+
"title": f"{home} vs {away}",
|
| 495 |
+
"url": mytv_url,
|
| 496 |
+
"source": "mytv",
|
| 497 |
+
"type": "full_match",
|
| 498 |
+
"home": home,
|
| 499 |
+
"away": away,
|
| 500 |
+
"date": ds
|
| 501 |
+
})
|
| 502 |
+
except:
|
| 503 |
+
pass
|
| 504 |
+
|
| 505 |
+
if len(videos) >= limit:
|
| 506 |
+
break
|
| 507 |
+
except:
|
| 508 |
+
pass
|
| 509 |
+
|
| 510 |
+
if len(videos) >= limit:
|
| 511 |
+
break
|
| 512 |
+
except:
|
| 513 |
+
pass
|
| 514 |
+
|
| 515 |
+
return videos
|
| 516 |
+
|
| 517 |
+
@app.get("/api/mytv/videos")
|
| 518 |
+
def api_mytv_videos(limit: int = Query(default=20, le=50)):
|
| 519 |
+
"""Lấy danh sách video từ mytv.com.vn"""
|
| 520 |
+
def _f():
|
| 521 |
+
return scrape_mytv_highlights(limit=limit)
|
| 522 |
+
return JSONResponse(_cached("mytv_videos", _f, ttl=300))
|
| 523 |
+
|
| 524 |
@app.get("/api/highlights")
|
| 525 |
def api_highlights():return JSONResponse(_cached("xemlaibongda_hl",scrape_xemlaibongda,ttl=_cache_ttl))
|
| 526 |
@app.get("/api/highlights/leagues")
|