Spaces:
Running
Running
Add /api/shorts/update + /api/shorts/status endpoints, prioritize runtime cache in scrape_shorts
Browse files
main.py
CHANGED
|
@@ -63,9 +63,9 @@ _cache_ttl_yt = 1800
|
|
| 63 |
SHORTS_FALLBACK = [
|
| 64 |
{"id":"nqlLH6chLRo","title":"Tin nóng VTV Nam Bộ | #shorts","channel":"vtvnambo"},
|
| 65 |
{"id":"E7Kq0v3hG6w","title":"VTV Nam Bộ - Tin tức miền Nam | #shorts","channel":"vtvnambo"},
|
| 66 |
-
{"id":"Lu_iCQ5YwNM","title":"Công an lập
|
| 67 |
{"id":"CwWvijF8BOA","title":"Chú rể Ninh Bình bật khóc nhận món quà bí mật người cha quá cố gửi 26 năm trước","channel":"baodantri7941"},
|
| 68 |
-
{"id":"tvPewsc2ph4","title":"Tính năng
|
| 69 |
{"id":"b1Nxzv9ixlU","title":"Y án 3 năm tù với nữ tài xế uống 8 lon bia lái xe tông chủ tịch xã tử vong","channel":"baodantri7941"},
|
| 70 |
{"id":"Xp5eTwAZAis","title":"Người đánh hàng xóm tại chung cư ở Hà Nội bị tuyên hơn 4 tháng tù","channel":"baodantri7941"},
|
| 71 |
{"id":"Htzvwg6iOBM","title":"Xe điện Audi S6 Sportback e-tron có gì đặc biệt?","channel":"baodantri7941"},
|
|
@@ -75,24 +75,24 @@ SHORTS_FALLBACK = [
|
|
| 75 |
{"id":"VAfgNNgZDRs","title":"Khởi tố 4 đối tượng ném bom xăng vào nhà dân ở Đồng Nai","channel":"baodantri7941"},
|
| 76 |
{"id":"sBH_-zGh0Xw","title":"Vì sao Times New Roman vẫn nổi tiếng sau hàng chục năm?","channel":"baodantri7941"},
|
| 77 |
{"id":"woKn5f2bLHM","title":"Quảng Ninh ngập sâu diện rộng sau đợt mưa lớn","channel":"baodantri7941"},
|
| 78 |
-
{"id":"bcpgRoxbLPw","title":"Giông lốc quật bay mái tôn
|
| 79 |
{"id":"ZIIC5osy544","title":"Bé trai Trung Quốc rơi từ tầng 11 vẫn sống sót kỳ diệu","channel":"baodantri7941"},
|
| 80 |
-
{"id":"uTMJ49NQpyc","title":"Sau lớp mascot 40kg Câu chuyện mưu sinh của người
|
| 81 |
-
{"id":"7Pd6vZ2Lz1M","title":"Hành động ấm lòng của người đàn ông tìm kiếm 5 học sinh tử vong
|
| 82 |
{"id":"SlHLt_ZyPiE","title":"Xử phạt người đàn ông xóa số điện thoại cứu hộ trên cao tốc Bắc Nam","channel":"baosuckhoedoisongboyte"},
|
| 83 |
-
{"id":"IUOprcJyYr4","title":"Phụ nữ táo bón có phải do lười
|
| 84 |
-
{"id":"YY8ojFNE-AU","title":"Quái xế tự quay clip
|
| 85 |
{"id":"OV7_oGdQGII","title":"Bố cô dâu khóc sụt sùi rồi quẩy cực sung gây bão mạng","channel":"baosuckhoedoisongboyte"},
|
| 86 |
-
{"id":"FoxhFyz2skY","title":"Người đàn ông nước ngoài đập phá ô tô bẻ cần gạt nước ở Đà
|
| 87 |
-
{"id":"R1oC_I8dFPU","title":"Thanh niên buông tay lái đứng trên xe máy khi đổ đèo ở
|
| 88 |
-
{"id":"U0Ft6ChWAIo","title":"Cô giáo kể phút tháo chạy
|
| 89 |
{"id":"hH0ANeze_4E","title":"Liên tiếp hàng chục con bò bị sét đánh chết trong ngày mưa dông","channel":"baosuckhoedoisongboyte"},
|
| 90 |
{"id":"pXWt0QbAzRQ","title":"Va chạm giao thông người phụ nữ lăng mạ tài xế ô tô","channel":"baosuckhoedoisongboyte"},
|
| 91 |
-
{"id":"UWWLPY1OYt4","title":"CSGT
|
| 92 |
{"id":"AxhVTQutsuo","title":"Xuất tinh sớm và những hiểu lầm thường gặp","channel":"baosuckhoedoisongboyte"},
|
| 93 |
{"id":"cNy6FgaNxYM","title":"Cô dâu khóc sưng mắt vì 6 chỉ vàng không cánh mày bay trong ngày cưới","channel":"baosuckhoedoisongboyte"},
|
| 94 |
{"id":"IDt_S6q59Ro","title":"Chở bạn gái không đội mũ bảo hiểm thanh niên đấm CSGT","channel":"baosuckhoedoisongboyte"},
|
| 95 |
-
{"id":"LFxJ9Ik6W0A","title":"Mệnh lệnh từ trái tim CSGT Hà Nội
|
| 96 |
]
|
| 97 |
for _v in SHORTS_FALLBACK:
|
| 98 |
_v.setdefault("link", "https://www.youtube.com/watch?v="+_v["id"])
|
|
@@ -119,10 +119,10 @@ def _short_default(): return {"views":0,"likes":0,"shares":0,"comments":[]}
|
|
| 119 |
PRIORITY_LEAGUES = ["Ngoại Hạng Anh","FA Cup","Champions League","LaLiga","Copa del Rey","Serie A","Bundesliga","Ligue 1","V-League"]
|
| 120 |
LEAGUE_IDS = {"nha":27110,"laliga":27233,"seriea":27044,"bundesliga":26891,"ligue1":27212}
|
| 121 |
HL_LEAGUES = {
|
| 122 |
-
"premier-league":{"path":"anh/premier-league","name":"Premier League","emoji":"
|
| 123 |
-
"fa-cup":{"path":"anh/fa-cup","name":"FA Cup","emoji":"
|
| 124 |
"bundesliga":{"path":"duc/bundesliga","name":"Bundesliga","emoji":"🇩🇪"},
|
| 125 |
-
"serie-a":{"path":"italy/serie-a","name":"Serie A","emoji":"🇮
|
| 126 |
"la-liga":{"path":"tay-ban-nha/la-liga","name":"La Liga","emoji":"🇪🇸"},
|
| 127 |
"champions-league":{"path":"cup-chau-au/uefa-champions-league","name":"Champions League","emoji":"⭐"},
|
| 128 |
"europa-league":{"path":"cup-chau-au/uefa-europa-league","name":"Europa League","emoji":"🟠"},
|
|
@@ -222,7 +222,7 @@ def proxy_img(url: str = Query(...)):
|
|
| 222 |
def _scrape_xemlaibongda_page(page_path, limit=20):
|
| 223 |
"""
|
| 224 |
Scrape video từ xemlaibongda.top - Simple & Reliable
|
| 225 |
-
Dùng logic cũ đã test, không fetch
|
| 226 |
"""
|
| 227 |
try:
|
| 228 |
url = f"https://xemlaibongda.top/{page_path}" if page_path else "https://xemlaibongda.top/"
|
|
@@ -242,7 +242,7 @@ def _scrape_xemlaibongda_page(page_path, limit=20):
|
|
| 242 |
if not href.startswith("http"):
|
| 243 |
href = "https://xemlaibongda.top" + href
|
| 244 |
|
| 245 |
-
#
|
| 246 |
clean_href = href.split("?")[0].split("#")[0]
|
| 247 |
if clean_href in seen:
|
| 248 |
continue
|
|
@@ -312,7 +312,7 @@ def _scrape_xemlaibongda_page(page_path, limit=20):
|
|
| 312 |
if text and len(text) >= 5:
|
| 313 |
title = text[:100]
|
| 314 |
|
| 315 |
-
# Fallback: tạo title
|
| 316 |
if not title or len(title) < 3:
|
| 317 |
slug = clean_href.split("/video/")[-1].rstrip("/").split("/xem-lai/")[-1].rstrip("/")
|
| 318 |
title = slug.replace("-", " ").replace("_", " ").title()
|
|
@@ -342,7 +342,7 @@ def _scrape_xemlaibongda_page(page_path, limit=20):
|
|
| 342 |
return []
|
| 343 |
|
| 344 |
def _extract_img_src(a_tag):
|
| 345 |
-
"""Extract image URL từ
|
| 346 |
img = a_tag.find("img")
|
| 347 |
if not img and a_tag.parent:
|
| 348 |
img = a_tag.parent.find("img")
|
|
@@ -418,7 +418,7 @@ def _extract_title(a_tag, href):
|
|
| 418 |
if alt and len(alt) >= 3:
|
| 419 |
return alt
|
| 420 |
|
| 421 |
-
# 4. Thử text content của <a> (loại
|
| 422 |
text = a_tag.get_text(" ", strip=True)
|
| 423 |
if text and len(text) >= 3:
|
| 424 |
# Lấy dòng đầu tiên nếu có nhiều dòng
|
|
@@ -504,14 +504,27 @@ def _yt_channel_shorts_requests(channel, count=15):
|
|
| 504 |
except: return []
|
| 505 |
|
| 506 |
def scrape_shorts():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
vids=[]
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
merged=[]; seen=set()
|
| 516 |
for v in vids:
|
| 517 |
vid=v.get("id")
|
|
@@ -569,6 +582,10 @@ def api_match_stats(event_id:int):return JSONResponse({"html":fetch_bongda_api(f
|
|
| 569 |
|
| 570 |
from match_detail_v2 import fetch_match_detail, fetch_match_detail_by_url
|
| 571 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
@app.get("/api/match/{event_id}/detail")
|
| 573 |
def api_match_detail(event_id: int, url: str = Query(default="")):
|
| 574 |
try:
|
|
@@ -725,7 +742,7 @@ def api_bdp_videos():
|
|
| 725 |
return JSONResponse(_cached("bdp_videos",_f))
|
| 726 |
|
| 727 |
# ===== NEWS =====
|
| 728 |
-
VNE_CATS={"thoi-su":("https://vnexpress.net/thoi-su","Thời Sự"),"the-gioi":("https://vnexpress.net/the-gioi","Thế Giới"),"kinh-doanh":("https://vnexpress.net/kinh-doanh","Kinh Doanh"),"the-thao":("https://vnexpress.net/the-thao","Thể Thao"),"giai-tri":("https://vnexpress.net/giai-tri","Giải Trí"),"suc-khoe":("https://vnexpress.net/suc-khoe","
|
| 729 |
|
| 730 |
def scrape_vne(cat_url):
|
| 731 |
try:
|
|
@@ -871,7 +888,59 @@ def api_storage_status():
|
|
| 871 |
def api_hot_topics():
|
| 872 |
return JSONResponse({"topics":[]})
|
| 873 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 874 |
@app.get("/", response_class=HTMLResponse)
|
| 875 |
async def root():
|
| 876 |
-
return HTMLResponse("<h1>VNEWS</h1><p>
|
| 877 |
-
#
|
|
|
|
|
|
| 63 |
SHORTS_FALLBACK = [
|
| 64 |
{"id":"nqlLH6chLRo","title":"Tin nóng VTV Nam Bộ | #shorts","channel":"vtvnambo"},
|
| 65 |
{"id":"E7Kq0v3hG6w","title":"VTV Nam Bộ - Tin tức miền Nam | #shorts","channel":"vtvnambo"},
|
| 66 |
+
{"id":"Lu_iCQ5YwNM","title":"Công an lập hồ sơ xử lý người phụ nữ chửi bới tát nam tài xế ô tô ở Hà Nội","channel":"baodantri7941"},
|
| 67 |
{"id":"CwWvijF8BOA","title":"Chú rể Ninh Bình bật khóc nhận món quà bí mật người cha quá cố gửi 26 năm trước","channel":"baodantri7941"},
|
| 68 |
+
{"id":"tvPewsc2ph4","title":"Tính năng ẩn trên iPhone giúp giảm mỏi mắt","channel":"baodantri7941"},
|
| 69 |
{"id":"b1Nxzv9ixlU","title":"Y án 3 năm tù với nữ tài xế uống 8 lon bia lái xe tông chủ tịch xã tử vong","channel":"baodantri7941"},
|
| 70 |
{"id":"Xp5eTwAZAis","title":"Người đánh hàng xóm tại chung cư ở Hà Nội bị tuyên hơn 4 tháng tù","channel":"baodantri7941"},
|
| 71 |
{"id":"Htzvwg6iOBM","title":"Xe điện Audi S6 Sportback e-tron có gì đặc biệt?","channel":"baodantri7941"},
|
|
|
|
| 75 |
{"id":"VAfgNNgZDRs","title":"Khởi tố 4 đối tượng ném bom xăng vào nhà dân ở Đồng Nai","channel":"baodantri7941"},
|
| 76 |
{"id":"sBH_-zGh0Xw","title":"Vì sao Times New Roman vẫn nổi tiếng sau hàng chục năm?","channel":"baodantri7941"},
|
| 77 |
{"id":"woKn5f2bLHM","title":"Quảng Ninh ngập sâu diện rộng sau đợt mưa lớn","channel":"baodantri7941"},
|
| 78 |
+
{"id":"bcpgRoxbLPw","title":"Giông lốc quật bay mái tôn ở TP.HCM","channel":"baodantri7941"},
|
| 79 |
{"id":"ZIIC5osy544","title":"Bé trai Trung Quốc rơi từ tầng 11 vẫn sống sót kỳ diệu","channel":"baodantri7941"},
|
| 80 |
+
{"id":"uTMJ49NQpyc","title":"Sau lớp mascot 40kg Câu chuyện mưu sinh của người trẻ ở TPHCM","channel":"baodantri7941"},
|
| 81 |
+
{"id":"7Pd6vZ2Lz1M","title":"Hành động ấm lòng của người đàn ông tìm kiếm 5 học sinh tử vong ở sông Lô","channel":"baosuckhoedoisongboyte"},
|
| 82 |
{"id":"SlHLt_ZyPiE","title":"Xử phạt người đàn ông xóa số điện thoại cứu hộ trên cao tốc Bắc Nam","channel":"baosuckhoedoisongboyte"},
|
| 83 |
+
{"id":"IUOprcJyYr4","title":"Phụ nữ táo bón có phải do lười ăn rau?","channel":"baosuckhoedoisongboyte"},
|
| 84 |
+
{"id":"YY8ojFNE-AU","title":"Quái xế tự quay clip nẹt pô đánh võng đăng TikTok bị xử lý","channel":"baosuckhoedoisongboyte"},
|
| 85 |
{"id":"OV7_oGdQGII","title":"Bố cô dâu khóc sụt sùi rồi quẩy cực sung gây bão mạng","channel":"baosuckhoedoisongboyte"},
|
| 86 |
+
{"id":"FoxhFyz2skY","title":"Người đàn ông nước ngoài đập phá ô tô bẻ cần gạt nước ở Đà Nẵng","channel":"baosuckhoedoisongboyte"},
|
| 87 |
+
{"id":"R1oC_I8dFPU","title":"Thanh niên buông tay lái đứng trên xe máy khi đổ đèo ở Đắk Lắk","channel":"baosuckhoedoisongboyte"},
|
| 88 |
+
{"id":"U0Ft6ChWAIo","title":"Cô giáo kể phút tháo chạy khỏi xe khách trước khi bị lũ vò nát ở Cao Bằng","channel":"baosuckhoedoisongboyte"},
|
| 89 |
{"id":"hH0ANeze_4E","title":"Liên tiếp hàng chục con bò bị sét đánh chết trong ngày mưa dông","channel":"baosuckhoedoisongboyte"},
|
| 90 |
{"id":"pXWt0QbAzRQ","title":"Va chạm giao thông người phụ nữ lăng mạ tài xế ô tô","channel":"baosuckhoedoisongboyte"},
|
| 91 |
+
{"id":"UWWLPY1OYt4","title":"CSGT chặn xe khách khống chế đối tượng cướp dây chuyền tại Gia Lai","channel":"baosuckhoedoisongboyte"},
|
| 92 |
{"id":"AxhVTQutsuo","title":"Xuất tinh sớm và những hiểu lầm thường gặp","channel":"baosuckhoedoisongboyte"},
|
| 93 |
{"id":"cNy6FgaNxYM","title":"Cô dâu khóc sưng mắt vì 6 chỉ vàng không cánh mày bay trong ngày cưới","channel":"baosuckhoedoisongboyte"},
|
| 94 |
{"id":"IDt_S6q59Ro","title":"Chở bạn gái không đội mũ bảo hiểm thanh niên đấm CSGT","channel":"baosuckhoedoisongboyte"},
|
| 95 |
+
{"id":"LFxJ9Ik6W0A","title":"Mệnh lệnh từ trái tim CSGT Hà Nội mở đường đưa bé 5 tháng tuổi đi cấp cứu","channel":"baosuckhoedoisongboyte"},
|
| 96 |
]
|
| 97 |
for _v in SHORTS_FALLBACK:
|
| 98 |
_v.setdefault("link", "https://www.youtube.com/watch?v="+_v["id"])
|
|
|
|
| 119 |
PRIORITY_LEAGUES = ["Ngoại Hạng Anh","FA Cup","Champions League","LaLiga","Copa del Rey","Serie A","Bundesliga","Ligue 1","V-League"]
|
| 120 |
LEAGUE_IDS = {"nha":27110,"laliga":27233,"seriea":27044,"bundesliga":26891,"ligue1":27212}
|
| 121 |
HL_LEAGUES = {
|
| 122 |
+
"premier-league":{"path":"anh/premier-league","name":"Premier League","emoji":"🏴"},
|
| 123 |
+
"fa-cup":{"path":"anh/fa-cup","name":"FA Cup","emoji":"🏆"},
|
| 124 |
"bundesliga":{"path":"duc/bundesliga","name":"Bundesliga","emoji":"🇩🇪"},
|
| 125 |
+
"serie-a":{"path":"italy/serie-a","name":"Serie A","emoji":"🇮🇹"},
|
| 126 |
"la-liga":{"path":"tay-ban-nha/la-liga","name":"La Liga","emoji":"🇪🇸"},
|
| 127 |
"champions-league":{"path":"cup-chau-au/uefa-champions-league","name":"Champions League","emoji":"⭐"},
|
| 128 |
"europa-league":{"path":"cup-chau-au/uefa-europa-league","name":"Europa League","emoji":"🟠"},
|
|
|
|
| 222 |
def _scrape_xemlaibongda_page(page_path, limit=20):
|
| 223 |
"""
|
| 224 |
Scrape video từ xemlaibongda.top - Simple & Reliable
|
| 225 |
+
Dùng logic cũ đã test, không fetch từng trang (tránh timeout)
|
| 226 |
"""
|
| 227 |
try:
|
| 228 |
url = f"https://xemlaibongda.top/{page_path}" if page_path else "https://xemlaibongda.top/"
|
|
|
|
| 242 |
if not href.startswith("http"):
|
| 243 |
href = "https://xemlaibongda.top" + href
|
| 244 |
|
| 245 |
+
# Bỏ query params
|
| 246 |
clean_href = href.split("?")[0].split("#")[0]
|
| 247 |
if clean_href in seen:
|
| 248 |
continue
|
|
|
|
| 312 |
if text and len(text) >= 5:
|
| 313 |
title = text[:100]
|
| 314 |
|
| 315 |
+
# Fallback: tạo title từ slug
|
| 316 |
if not title or len(title) < 3:
|
| 317 |
slug = clean_href.split("/video/")[-1].rstrip("/").split("/xem-lai/")[-1].rstrip("/")
|
| 318 |
title = slug.replace("-", " ").replace("_", " ").title()
|
|
|
|
| 342 |
return []
|
| 343 |
|
| 344 |
def _extract_img_src(a_tag):
|
| 345 |
+
"""Extract image URL từ thẻ <a> và parent elements"""
|
| 346 |
img = a_tag.find("img")
|
| 347 |
if not img and a_tag.parent:
|
| 348 |
img = a_tag.parent.find("img")
|
|
|
|
| 418 |
if alt and len(alt) >= 3:
|
| 419 |
return alt
|
| 420 |
|
| 421 |
+
# 4. Thử text content của <a> (loại bỏ quá dài)
|
| 422 |
text = a_tag.get_text(" ", strip=True)
|
| 423 |
if text and len(text) >= 3:
|
| 424 |
# Lấy dòng đầu tiên nếu có nhiều dòng
|
|
|
|
| 504 |
except: return []
|
| 505 |
|
| 506 |
def scrape_shorts():
|
| 507 |
+
"""Priority 1: runtime cache from GitHub Actions updater"""
|
| 508 |
+
cached = get_runtime_cache()
|
| 509 |
+
if cached and len(cached) >= 10:
|
| 510 |
+
return cached[:60]
|
| 511 |
+
|
| 512 |
+
# Priority 2: try live YouTube scrape (fast-fail on HF Spaces)
|
| 513 |
vids=[]
|
| 514 |
+
try:
|
| 515 |
+
with ThreadPoolExecutor(3) as ex:
|
| 516 |
+
futs=[ex.submit(_yt_channel_shorts_requests,ch,24) for ch in ["baodantri7941","baosuckhoedoisongboyte","vtvnambo"]]
|
| 517 |
+
for f in as_completed(futs, timeout=8): # 8s cap
|
| 518 |
+
try:
|
| 519 |
+
r=f.result()
|
| 520 |
+
if r: vids.extend(r)
|
| 521 |
+
except: pass
|
| 522 |
+
except: pass
|
| 523 |
+
|
| 524 |
+
# Priority 3: hardcoded fallback
|
| 525 |
+
if not vids:
|
| 526 |
+
vids = list(SHORTS_FALLBACK)
|
| 527 |
+
|
| 528 |
merged=[]; seen=set()
|
| 529 |
for v in vids:
|
| 530 |
vid=v.get("id")
|
|
|
|
| 582 |
|
| 583 |
from match_detail_v2 import fetch_match_detail, fetch_match_detail_by_url
|
| 584 |
|
| 585 |
+
# ===== SHORTS RUNTIME CACHE (GitHub Actions updater) =====
|
| 586 |
+
from shorts_cache import get_runtime_cache, set_runtime_cache, get_cache_status, SHORTS_UPDATE_SECRET
|
| 587 |
+
import hmac, hashlib as _hashlib
|
| 588 |
+
|
| 589 |
@app.get("/api/match/{event_id}/detail")
|
| 590 |
def api_match_detail(event_id: int, url: str = Query(default="")):
|
| 591 |
try:
|
|
|
|
| 742 |
return JSONResponse(_cached("bdp_videos",_f))
|
| 743 |
|
| 744 |
# ===== NEWS =====
|
| 745 |
+
VNE_CATS={"thoi-su":("https://vnexpress.net/thoi-su","Thời Sự"),"the-gioi":("https://vnexpress.net/the-gioi","Thế Giới"),"kinh-doanh":("https://vnexpress.net/kinh-doanh","Kinh Doanh"),"the-thao":("https://vnexpress.net/the-thao","Thể Thao"),"giai-tri":("https://vnexpress.net/giai-tri","Giải Trí"),"suc-khoe":("https://vnexpress.net/suc-khoe","Sức Khỏe"),"phap-luat":("https://vnexpress.net/phap-luat","Pháp Luật"),"giao-duc":("https://vnexpress.net/giao-duc","Giáo Dục"),"du-lich":("https://vnexpress.net/du-lich","Du Lịch"),"doi-song":("https://vnexpress.net/doi-song","Đời Sống")}
|
| 746 |
|
| 747 |
def scrape_vne(cat_url):
|
| 748 |
try:
|
|
|
|
| 888 |
def api_hot_topics():
|
| 889 |
return JSONResponse({"topics":[]})
|
| 890 |
|
| 891 |
+
# ===== SHORTS EXTERNAL UPDATER ENDPOINTS =====
|
| 892 |
+
@app.get("/api/shorts/status")
|
| 893 |
+
def api_shorts_updater_status():
|
| 894 |
+
"""Check shorts cache status"""
|
| 895 |
+
return JSONResponse(get_cache_status())
|
| 896 |
+
|
| 897 |
+
@app.post("/api/shorts/update")
|
| 898 |
+
async def api_shorts_updater(request: Request):
|
| 899 |
+
"""Receive fresh shorts from GitHub Actions updater
|
| 900 |
+
|
| 901 |
+
Headers:
|
| 902 |
+
X-Signature: HMAC-SHA256(body, SHORTS_UPDATE_SECRET)
|
| 903 |
+
Body:
|
| 904 |
+
{"items": [...], "source": "yt-dlp", "fetched_at": timestamp}
|
| 905 |
+
"""
|
| 906 |
+
body = await request.body()
|
| 907 |
+
sig = request.headers.get("X-Signature", "")
|
| 908 |
+
|
| 909 |
+
# Verify HMAC signature
|
| 910 |
+
expected = _hashlib.new('sha256', body, usedforsecurity=False)
|
| 911 |
+
# Use hmac properly
|
| 912 |
+
import hmac as hmac_mod
|
| 913 |
+
expected_sig = hmac_mod.new(
|
| 914 |
+
SHORTS_UPDATE_SECRET.encode(),
|
| 915 |
+
body,
|
| 916 |
+
_hashlib.sha256
|
| 917 |
+
).hexdigest()
|
| 918 |
+
|
| 919 |
+
if not hmac_mod.compare_digest(sig, expected_sig):
|
| 920 |
+
from fastapi import HTTPException
|
| 921 |
+
raise HTTPException(status_code=401, detail="invalid signature")
|
| 922 |
+
|
| 923 |
+
try:
|
| 924 |
+
data = json.loads(body)
|
| 925 |
+
except:
|
| 926 |
+
from fastapi import HTTPException
|
| 927 |
+
raise HTTPException(status_code=400, detail="invalid JSON")
|
| 928 |
+
|
| 929 |
+
items = data.get("items", [])
|
| 930 |
+
if not items:
|
| 931 |
+
from fastapi import HTTPException
|
| 932 |
+
raise HTTPException(status_code=400, detail="no items")
|
| 933 |
+
|
| 934 |
+
count = set_runtime_cache(items)
|
| 935 |
+
return JSONResponse({
|
| 936 |
+
"ok": True,
|
| 937 |
+
"saved": count,
|
| 938 |
+
"source": data.get("source", "unknown"),
|
| 939 |
+
"fetched_at": data.get("fetched_at", time.time()),
|
| 940 |
+
})
|
| 941 |
+
|
| 942 |
@app.get("/", response_class=HTMLResponse)
|
| 943 |
async def root():
|
| 944 |
+
return HTMLResponse("<h1>VNEWS v15</h1><p>Shorts external updater active</p>")
|
| 945 |
+
# v15 2026-06-29 - shorts external updater
|
| 946 |
+
|