Spaces:
Running
Running
Fix shorts/video classification: tag is_short deterministically from Jina channel tabs (no YouTube HEAD), override /api/shorts
Browse files- app_v2_entry.py +69 -0
app_v2_entry.py
CHANGED
|
@@ -165,6 +165,75 @@ def _search_all(topic,limit=36):
|
|
| 165 |
for s in srcs:
|
| 166 |
if i<len(s) and s[i].get('url') and s[i]['url'] not in seen:seen.add(s[i]['url']);out.append(s[i])
|
| 167 |
return out[:limit]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
# Override article endpoint
|
| 169 |
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/api/article' and 'GET' in getattr(r,'methods',set()))]
|
| 170 |
_JINA_BOILER_STOP=['facebooktelegram','copy link','follow us','bài viết liên quan','tin liên quan',
|
|
|
|
| 165 |
for s in srcs:
|
| 166 |
if i<len(s) and s[i].get('url') and s[i]['url'] not in seen:seen.add(s[i]['url']);out.append(s[i])
|
| 167 |
return out[:limit]
|
| 168 |
+
|
| 169 |
+
# ===== YOUTUBE SHORTS — reliable short/video classification (override) =====
|
| 170 |
+
# Datacenter IPs (HF Space) get an EMPTY feeds/videos.xml from YouTube and YouTube
|
| 171 |
+
# HEAD requests time out, so main.py's HEAD-based _classify_short always defaulted to
|
| 172 |
+
# is_short=True -> every Dân trí/SKĐS item showed as "Short" and the 1:1 toggle never
|
| 173 |
+
# appeared. Fix: classify deterministically from the Jina-rendered channel tabs —
|
| 174 |
+
# a link under /shorts/<id> is a Short, a link under /watch?v=<id> on the /videos tab
|
| 175 |
+
# is a normal (horizontal) video. No request to youtube.com is needed.
|
| 176 |
+
from main import (_yt_jina_page, _yt_rss_shorts, SHORTS_FALLBACK,
|
| 177 |
+
_YT_CHANNEL_IDS, _cached, _cache_ttl_yt)
|
| 178 |
+
|
| 179 |
+
def _yt_jina_shorts_classified(channel, count=24):
|
| 180 |
+
"""Newest videos for a channel via Jina channel tabs, each tagged is_short.
|
| 181 |
+
/shorts tab -> Short (is_short=True). /videos tab -> normal video (is_short=False)
|
| 182 |
+
unless the rendered link itself is a /shorts/ URL."""
|
| 183 |
+
items=[];seen=set()
|
| 184 |
+
# (tab, default_is_short_for_watch_links)
|
| 185 |
+
for path, tab_short in (("videos", False), ("shorts", True)):
|
| 186 |
+
md=_yt_jina_page(channel, path)
|
| 187 |
+
if not md:continue
|
| 188 |
+
for m in re.finditer(r'\[([^\]\[]{8,160})\]\(https?://(?:www\.)?youtube\.com/(shorts/|watch\?v=)([A-Za-z0-9_-]{11})[^)]*\)', md):
|
| 189 |
+
title=html_lib.unescape(m.group(1)).strip().strip('"')
|
| 190 |
+
form=m.group(2); vid=m.group(3)
|
| 191 |
+
if vid in seen:continue
|
| 192 |
+
tl=title.lower()
|
| 193 |
+
if tl in ("home home","shorts shorts","image") or title.startswith("!") or title.startswith("Image"):continue
|
| 194 |
+
# A /shorts/ URL is always a Short; a /watch?v= link inherits the tab default.
|
| 195 |
+
is_short = True if form.startswith("shorts/") else tab_short
|
| 196 |
+
seen.add(vid)
|
| 197 |
+
items.append({"title":title,"link":f"https://www.youtube.com/watch?v={vid}",
|
| 198 |
+
"img":f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg","source":"yt",
|
| 199 |
+
"id":vid,"channel":channel,"is_short":is_short})
|
| 200 |
+
if len(items)>=count:break
|
| 201 |
+
return items
|
| 202 |
+
|
| 203 |
+
def _scrape_shorts_v2():
|
| 204 |
+
"""Stable Dân trí & SKĐS feed with RELIABLE is_short tags (Jina tabs), newest-first.
|
| 205 |
+
Falls back to RSS (tagged short) then the static list so the slider never empties."""
|
| 206 |
+
vids=[]
|
| 207 |
+
with ThreadPoolExecutor(2) as ex:
|
| 208 |
+
futs=[ex.submit(_yt_jina_shorts_classified,ch,24) for ch in ["baodantri7941","baosuckhoedoisongboyte"]]
|
| 209 |
+
for f in as_completed(futs):
|
| 210 |
+
try:
|
| 211 |
+
r=f.result()
|
| 212 |
+
if r:vids.extend(r)
|
| 213 |
+
except:pass
|
| 214 |
+
# If Jina returned nothing for a channel, fill from RSS (newest, tagged short by default).
|
| 215 |
+
have=set(v.get("channel") for v in vids)
|
| 216 |
+
for ch in ["baodantri7941","baosuckhoedoisongboyte"]:
|
| 217 |
+
if ch not in have:
|
| 218 |
+
for v in _yt_rss_shorts(ch,24):
|
| 219 |
+
v.setdefault("is_short",True);vids.append(v)
|
| 220 |
+
merged=[];seen=set()
|
| 221 |
+
for v in vids+[dict(x) for x in SHORTS_FALLBACK]:
|
| 222 |
+
vid=v.get("id")
|
| 223 |
+
if not vid or vid in seen:continue
|
| 224 |
+
# Static fallback items: infer from title (#shorts -> short, else normal video).
|
| 225 |
+
if "is_short" not in v:
|
| 226 |
+
t=(v.get("title","") or "").lower()
|
| 227 |
+
v["is_short"]= ("#shorts" in t) or ("short" in t)
|
| 228 |
+
seen.add(vid);merged.append(v)
|
| 229 |
+
return merged[:40]
|
| 230 |
+
|
| 231 |
+
# Override main.py's /api/shorts so the slider uses the reliable classifier.
|
| 232 |
+
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/api/shorts' and 'GET' in getattr(r,'methods',set()))]
|
| 233 |
+
@app.get('/api/shorts')
|
| 234 |
+
def api_shorts_v2():
|
| 235 |
+
return JSONResponse(_cached("yt_shorts_v4",_scrape_shorts_v2,ttl=_cache_ttl_yt))
|
| 236 |
+
|
| 237 |
# Override article endpoint
|
| 238 |
app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/api/article' and 'GET' in getattr(r,'methods',set()))]
|
| 239 |
_JINA_BOILER_STOP=['facebooktelegram','copy link','follow us','bài viết liên quan','tin liên quan',
|