fix: all video links (BDP+24h) from slider open in TikTok mode, remove broken 02.m3u8 probe
Browse files- app_wrapper.py +89 -101
app_wrapper.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Entry point: shorts carousel + TikTok-style
|
| 2 |
import sys, os
|
| 3 |
|
| 4 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
@@ -48,26 +48,14 @@ if "shorts-feed-mode" not in app.CSS:
|
|
| 48 |
if "FULLWIDTH: kill all Gradio padding" not in app.CSS:
|
| 49 |
app.CSS += FULLWIDTH_CSS
|
| 50 |
|
| 51 |
-
# ══
|
| 52 |
-
# The homepage slider uses render_video_slider_html which calls bdpOpenTikTok
|
| 53 |
-
# But bdpOpenTikTok is broken/dead. Change it to bdpOpen so clicking a highlight
|
| 54 |
-
# opens the article (which for 24h URLs renders as TikTok feed via our read_article patch)
|
| 55 |
_orig_render_video_slider = getattr(app, 'render_video_slider_html', None)
|
| 56 |
if _orig_render_video_slider:
|
| 57 |
def _patched_render_video_slider(videos):
|
| 58 |
html = _orig_render_video_slider(videos)
|
| 59 |
-
#
|
| 60 |
# bdpOpenTikTok('url','aid') → bdpOpen('url','aid','')
|
| 61 |
-
html = html.replace("
|
| 62 |
-
# Fix: bdpOpen needs 3 params (url, aid, slug). Add empty slug where missing.
|
| 63 |
-
import re as _r
|
| 64 |
-
def _fix_bdpopen(m):
|
| 65 |
-
# If only 2 params, add third empty one
|
| 66 |
-
params = m.group(1)
|
| 67 |
-
if params.count(",") < 2:
|
| 68 |
-
params = params.rstrip(")") + ",''"
|
| 69 |
-
return "window.bdpOpen(" + params + ")"
|
| 70 |
-
html = _r.sub(r"window\.bdpOpen\(([^)]+)\)", _fix_bdpopen, html)
|
| 71 |
return html
|
| 72 |
app.render_video_slider_html = _patched_render_video_slider
|
| 73 |
|
|
@@ -91,61 +79,6 @@ def _patched_render_homepage(articles, *args, **kwargs):
|
|
| 91 |
return html
|
| 92 |
app.render_homepage_html = _patched_render_homepage
|
| 93 |
|
| 94 |
-
# ══ Patch fetch_tiktok_feed_videos: ensure multi-part 24h videos create multiple slides ══
|
| 95 |
-
_orig_fetch_tiktok = app.fetch_tiktok_feed_videos
|
| 96 |
-
def _patched_fetch_tiktok():
|
| 97 |
-
"""Override to ensure multi-part 24h videos are fully expanded."""
|
| 98 |
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 99 |
-
results = []
|
| 100 |
-
|
| 101 |
-
def _fetch_bdp_vid(art):
|
| 102 |
-
vid_id = app._extract_bdp_video_id(art["link"])
|
| 103 |
-
if not vid_id: return None
|
| 104 |
-
embed = app.fetch_bdp_embed_data(vid_id)
|
| 105 |
-
if not embed or not embed.get("mp4"): return None
|
| 106 |
-
return [{"title": art["title"], "src": embed["mp4"], "poster": embed.get("poster",""),
|
| 107 |
-
"vtype": "mp4", "source": "bdp", "link": art["link"]}]
|
| 108 |
-
|
| 109 |
-
def _fetch_24h_vid(art):
|
| 110 |
-
# Use the PLURAL version that probes for 02, 03, etc.
|
| 111 |
-
vids = app._extract_24h_video_urls(art["link"])
|
| 112 |
-
if not vids: return []
|
| 113 |
-
out = []
|
| 114 |
-
for pi, v in enumerate(vids):
|
| 115 |
-
label = f" (Phần {pi+1})" if len(vids) > 1 else ""
|
| 116 |
-
out.append({"title": art["title"] + label, "src": v["src"], "poster": v["poster"],
|
| 117 |
-
"vtype": v["vtype"], "source": "24h", "link": art["link"]})
|
| 118 |
-
return out
|
| 119 |
-
|
| 120 |
-
bdp_list = app.scrape_bdp_video_list()[:12]
|
| 121 |
-
h24_list = app.scrape_24h_video_list()[:12]
|
| 122 |
-
|
| 123 |
-
bdp_results, h24_results = [], []
|
| 124 |
-
with ThreadPoolExecutor(max_workers=8) as ex:
|
| 125 |
-
bdp_futures = {ex.submit(_fetch_bdp_vid, a): a for a in bdp_list}
|
| 126 |
-
h24_futures = {ex.submit(_fetch_24h_vid, a): a for a in h24_list}
|
| 127 |
-
for f in as_completed(bdp_futures):
|
| 128 |
-
try:
|
| 129 |
-
r = f.result()
|
| 130 |
-
if r: bdp_results.extend(r)
|
| 131 |
-
except: pass
|
| 132 |
-
for f in as_completed(h24_futures):
|
| 133 |
-
try:
|
| 134 |
-
r = f.result()
|
| 135 |
-
if r: h24_results.extend(r)
|
| 136 |
-
except: pass
|
| 137 |
-
|
| 138 |
-
# Interleave
|
| 139 |
-
bi, hi = 0, 0
|
| 140 |
-
while bi < len(bdp_results) or hi < len(h24_results):
|
| 141 |
-
if bi < len(bdp_results):
|
| 142 |
-
results.append(bdp_results[bi]); bi += 1
|
| 143 |
-
if hi < len(h24_results):
|
| 144 |
-
results.append(h24_results[hi]); hi += 1
|
| 145 |
-
return results[:25]
|
| 146 |
-
|
| 147 |
-
app.fetch_tiktok_feed_videos = _patched_fetch_tiktok
|
| 148 |
-
|
| 149 |
# ══ Patch render_video_page_html: fullheight button ══
|
| 150 |
_orig_render_video_page = app.render_video_page_html
|
| 151 |
def _patched_render_video_page():
|
|
@@ -155,59 +88,104 @@ def _patched_render_video_page():
|
|
| 155 |
return html
|
| 156 |
app.render_video_page_html = _patched_render_video_page
|
| 157 |
|
| 158 |
-
# ══ Patch read_article:
|
| 159 |
_orig_read_article = app.read_article
|
| 160 |
def _patched_read_article(url):
|
| 161 |
if not url or url == "#" or len(url) < 10:
|
| 162 |
return "<p>Không tìm thấy bài viết.</p>"
|
|
|
|
|
|
|
| 163 |
if "24h.com.vn" in url:
|
| 164 |
-
result =
|
| 165 |
if result:
|
| 166 |
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
return _orig_read_article(url)
|
| 168 |
|
| 169 |
-
def
|
| 170 |
-
"""Render
|
| 171 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 172 |
-
|
|
|
|
| 173 |
try:
|
| 174 |
-
|
| 175 |
except:
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
return None
|
| 180 |
|
| 181 |
def _fetch_video(art):
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
if vid:
|
| 188 |
-
vids = [vid]
|
| 189 |
-
if not vids:
|
| 190 |
-
return []
|
| 191 |
-
out = []
|
| 192 |
-
for pi, v in enumerate(vids):
|
| 193 |
-
label = f" (Phần {pi+1})" if len(vids) > 1 else ""
|
| 194 |
-
out.append({"title": art["title"] + label, "link": art["link"], "img": art.get("img",""),
|
| 195 |
-
"src": v["src"], "poster": v["poster"], "vtype": v["vtype"]})
|
| 196 |
-
return out
|
| 197 |
|
| 198 |
videos = []
|
| 199 |
with ThreadPoolExecutor(max_workers=6) as ex:
|
| 200 |
-
futures = {ex.submit(_fetch_video, a): a for a in
|
| 201 |
for f in as_completed(futures):
|
| 202 |
try:
|
| 203 |
r = f.result()
|
| 204 |
-
if r: videos.
|
| 205 |
except: pass
|
| 206 |
|
| 207 |
if not videos:
|
| 208 |
return None
|
|
|
|
|
|
|
| 209 |
|
| 210 |
-
|
|
|
|
|
|
|
| 211 |
current_aid = app.make_id(current_url)
|
| 212 |
ordered = []
|
| 213 |
rest = []
|
|
@@ -218,6 +196,9 @@ def _render_shorts_tiktok_feed(current_url):
|
|
| 218 |
rest.append(v)
|
| 219 |
ordered.extend(rest)
|
| 220 |
|
|
|
|
|
|
|
|
|
|
| 221 |
slides = []
|
| 222 |
for vi, v in enumerate(ordered):
|
| 223 |
poster = app.safe_url(v.get("poster", ""))
|
|
@@ -232,6 +213,10 @@ def _render_shorts_tiktok_feed(current_url):
|
|
| 232 |
else:
|
| 233 |
vtag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} muted loop><source src="{app.safe_url(vsrc)}" type="video/mp4"></video>'
|
| 234 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
slides.append(f'''<div class="tiktok-slide" data-index="{vi}" data-aid="{aid}">
|
| 236 |
{vtag}
|
| 237 |
<div class="tiktok-pause-icon">\u25b6</div>
|
|
@@ -240,16 +225,18 @@ def _render_shorts_tiktok_feed(current_url):
|
|
| 240 |
<button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,10)">10s \u23e9</button>
|
| 241 |
</div>
|
| 242 |
<div class="tiktok-bottom">
|
| 243 |
-
<span class="bdp-badge
|
| 244 |
<p class="tiktok-title">{v["title"]}</p>
|
| 245 |
<div class="tiktok-actions">
|
| 246 |
<button class="tiktok-action-btn" onclick="{share_js}">\U0001f4e4 Chia s\u1ebb</button>
|
| 247 |
</div>
|
| 248 |
</div>
|
|
|
|
| 249 |
<div class="tiktok-unmute-hint" onclick="window.bdpTikTokUnmute(this)">\U0001f507 B\u1eadt ti\u1ebfng</div>
|
| 250 |
<span class="tiktok-counter">{vi+1}/{len(ordered)}</span>
|
| 251 |
</div>''')
|
| 252 |
|
|
|
|
| 253 |
list_cards = []
|
| 254 |
for v in ordered:
|
| 255 |
img = app.safe_url(v.get("img") or v.get("poster",""))
|
|
@@ -258,13 +245,14 @@ def _render_shorts_tiktok_feed(current_url):
|
|
| 258 |
click = f"window.bdpOpen('{app.esc(v['link'])}','{aid}','{sl}')"
|
| 259 |
list_cards.append(f'''<div class="bdp-card" onclick="{click}">
|
| 260 |
<div class="bdp-card-img"><img src="{img}" alt="" class="bdp-lazy-img"><div class="bdp-play-overlay">\u25b6</div></div>
|
| 261 |
-
<div class="bdp-card-body"><span class="bdp-badge
|
| 262 |
<h3 class="bdp-card-title">{v["title"]}</h3></div></div>''')
|
| 263 |
|
| 264 |
-
grid_html = f'''<div class="bdp-wrap"><div class="bdp-topbar"><span>\
|
| 265 |
<div class="bdp-grid">{''.join(list_cards)}</div></div>'''
|
| 266 |
|
| 267 |
-
|
|
|
|
| 268 |
<div class="tiktok-fullscreen-feed">{''.join(slides)}</div>
|
| 269 |
</div>
|
| 270 |
{grid_html}'''
|
|
|
|
| 1 |
+
"""Entry point: shorts carousel + TikTok-style video playback for ALL video sources."""
|
| 2 |
import sys, os
|
| 3 |
|
| 4 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
| 48 |
if "FULLWIDTH: kill all Gradio padding" not in app.CSS:
|
| 49 |
app.CSS += FULLWIDTH_CSS
|
| 50 |
|
| 51 |
+
# ══ Fix homepage slider: replace bdpOpenTikTok with bdpOpen ══
|
|
|
|
|
|
|
|
|
|
| 52 |
_orig_render_video_slider = getattr(app, 'render_video_slider_html', None)
|
| 53 |
if _orig_render_video_slider:
|
| 54 |
def _patched_render_video_slider(videos):
|
| 55 |
html = _orig_render_video_slider(videos)
|
| 56 |
+
# bdpOpenTikTok doesn't work. Replace with bdpOpen.
|
| 57 |
# bdpOpenTikTok('url','aid') → bdpOpen('url','aid','')
|
| 58 |
+
html = html.replace("bdpOpenTikTok(", "bdpOpen(")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
return html
|
| 60 |
app.render_video_slider_html = _patched_render_video_slider
|
| 61 |
|
|
|
|
| 79 |
return html
|
| 80 |
app.render_homepage_html = _patched_render_homepage
|
| 81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
# ══ Patch render_video_page_html: fullheight button ══
|
| 83 |
_orig_render_video_page = app.render_video_page_html
|
| 84 |
def _patched_render_video_page():
|
|
|
|
| 88 |
return html
|
| 89 |
app.render_video_page_html = _patched_render_video_page
|
| 90 |
|
| 91 |
+
# ══ Patch read_article: video URLs → TikTok feed ══
|
| 92 |
_orig_read_article = app.read_article
|
| 93 |
def _patched_read_article(url):
|
| 94 |
if not url or url == "#" or len(url) < 10:
|
| 95 |
return "<p>Không tìm thấy bài viết.</p>"
|
| 96 |
+
|
| 97 |
+
# 24h URLs → shorts/highlight TikTok feed
|
| 98 |
if "24h.com.vn" in url:
|
| 99 |
+
result = _render_24h_tiktok_feed(url)
|
| 100 |
if result:
|
| 101 |
return result
|
| 102 |
+
|
| 103 |
+
# BDP video URLs → BDP TikTok feed
|
| 104 |
+
if "bongdaplus.vn/video/" in url:
|
| 105 |
+
result = _render_bdp_tiktok_feed(url)
|
| 106 |
+
if result:
|
| 107 |
+
return result
|
| 108 |
+
|
| 109 |
return _orig_read_article(url)
|
| 110 |
|
| 111 |
+
def _render_bdp_tiktok_feed(current_url):
|
| 112 |
+
"""Render BDP video in TikTok feed style (with other BDP videos)."""
|
| 113 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 114 |
+
|
| 115 |
+
# Get BDP video list
|
| 116 |
try:
|
| 117 |
+
bdp_list = app.scrape_bdp_video_list()[:15]
|
| 118 |
except:
|
| 119 |
+
return None
|
| 120 |
+
|
| 121 |
+
if not bdp_list:
|
| 122 |
+
return None
|
| 123 |
+
|
| 124 |
+
def _fetch_bdp(art):
|
| 125 |
+
vid_id = app._extract_bdp_video_id(art["link"])
|
| 126 |
+
if not vid_id: return None
|
| 127 |
+
embed = app.fetch_bdp_embed_data(vid_id)
|
| 128 |
+
if not embed or not embed.get("mp4"): return None
|
| 129 |
+
return {"title": art["title"], "link": art["link"], "img": art.get("img",""),
|
| 130 |
+
"src": embed["mp4"], "poster": embed.get("poster",""), "vtype": "mp4"}
|
| 131 |
+
|
| 132 |
+
videos = []
|
| 133 |
+
with ThreadPoolExecutor(max_workers=6) as ex:
|
| 134 |
+
futures = {ex.submit(_fetch_bdp, a): a for a in bdp_list}
|
| 135 |
+
for f in as_completed(futures):
|
| 136 |
+
try:
|
| 137 |
+
r = f.result()
|
| 138 |
+
if r: videos.append(r)
|
| 139 |
+
except: pass
|
| 140 |
+
|
| 141 |
+
if not videos:
|
| 142 |
+
return None
|
| 143 |
+
|
| 144 |
+
return _build_tiktok_html(videos, current_url, "bdp")
|
| 145 |
+
|
| 146 |
+
def _render_24h_tiktok_feed(current_url):
|
| 147 |
+
"""Render 24h video in TikTok feed style (with other 24h videos)."""
|
| 148 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 149 |
|
| 150 |
+
# Try shorts list first, then highlights
|
| 151 |
+
try:
|
| 152 |
+
all_articles = scrape_24h_news_shorts()[:15]
|
| 153 |
+
except:
|
| 154 |
+
all_articles = []
|
| 155 |
+
|
| 156 |
+
if not all_articles:
|
| 157 |
+
try:
|
| 158 |
+
all_articles = app.scrape_24h_video_list()[:15]
|
| 159 |
+
except:
|
| 160 |
+
all_articles = []
|
| 161 |
+
|
| 162 |
+
if not all_articles:
|
| 163 |
return None
|
| 164 |
|
| 165 |
def _fetch_video(art):
|
| 166 |
+
vid = _extract_24h_video_url(art["link"])
|
| 167 |
+
if vid:
|
| 168 |
+
return {"title": art["title"], "link": art["link"], "img": art.get("img",""),
|
| 169 |
+
"src": vid["src"], "poster": vid["poster"], "vtype": vid["vtype"]}
|
| 170 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
videos = []
|
| 173 |
with ThreadPoolExecutor(max_workers=6) as ex:
|
| 174 |
+
futures = {ex.submit(_fetch_video, a): a for a in all_articles}
|
| 175 |
for f in as_completed(futures):
|
| 176 |
try:
|
| 177 |
r = f.result()
|
| 178 |
+
if r: videos.append(r)
|
| 179 |
except: pass
|
| 180 |
|
| 181 |
if not videos:
|
| 182 |
return None
|
| 183 |
+
|
| 184 |
+
return _build_tiktok_html(videos, current_url, "24h")
|
| 185 |
|
| 186 |
+
def _build_tiktok_html(videos, current_url, source_label):
|
| 187 |
+
"""Build TikTok feed HTML from video list. Clicked video placed first."""
|
| 188 |
+
# Reorder: clicked video first
|
| 189 |
current_aid = app.make_id(current_url)
|
| 190 |
ordered = []
|
| 191 |
rest = []
|
|
|
|
| 196 |
rest.append(v)
|
| 197 |
ordered.extend(rest)
|
| 198 |
|
| 199 |
+
badge_cls = "bdp-badge-24h" if source_label == "24h" else "bdp-badge-bdp"
|
| 200 |
+
is_shorts = source_label == "24h"
|
| 201 |
+
|
| 202 |
slides = []
|
| 203 |
for vi, v in enumerate(ordered):
|
| 204 |
poster = app.safe_url(v.get("poster", ""))
|
|
|
|
| 213 |
else:
|
| 214 |
vtag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} muted loop><source src="{app.safe_url(vsrc)}" type="video/mp4"></video>'
|
| 215 |
|
| 216 |
+
fullheight_btn = ''
|
| 217 |
+
if not is_shorts:
|
| 218 |
+
fullheight_btn = '<button class="tiktok-fullheight-btn" onclick="event.stopPropagation();var c=this.closest(\'.tiktok-fullscreen-container\');c.classList.toggle(\'fullheight-mode\');var on=c.classList.contains(\'fullheight-mode\');c.querySelectorAll(\'.tiktok-fullheight-btn\').forEach(function(b){b.classList.toggle(\'active\',on);b.textContent=on?\'\\u2B07 Contain\':\'\\u2922 Fullheight\';});">\u2922 Fullheight</button>'
|
| 219 |
+
|
| 220 |
slides.append(f'''<div class="tiktok-slide" data-index="{vi}" data-aid="{aid}">
|
| 221 |
{vtag}
|
| 222 |
<div class="tiktok-pause-icon">\u25b6</div>
|
|
|
|
| 225 |
<button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,10)">10s \u23e9</button>
|
| 226 |
</div>
|
| 227 |
<div class="tiktok-bottom">
|
| 228 |
+
<span class="bdp-badge {badge_cls}">{source_label}</span>
|
| 229 |
<p class="tiktok-title">{v["title"]}</p>
|
| 230 |
<div class="tiktok-actions">
|
| 231 |
<button class="tiktok-action-btn" onclick="{share_js}">\U0001f4e4 Chia s\u1ebb</button>
|
| 232 |
</div>
|
| 233 |
</div>
|
| 234 |
+
{fullheight_btn}
|
| 235 |
<div class="tiktok-unmute-hint" onclick="window.bdpTikTokUnmute(this)">\U0001f507 B\u1eadt ti\u1ebfng</div>
|
| 236 |
<span class="tiktok-counter">{vi+1}/{len(ordered)}</span>
|
| 237 |
</div>''')
|
| 238 |
|
| 239 |
+
# Video list cards below
|
| 240 |
list_cards = []
|
| 241 |
for v in ordered:
|
| 242 |
img = app.safe_url(v.get("img") or v.get("poster",""))
|
|
|
|
| 245 |
click = f"window.bdpOpen('{app.esc(v['link'])}','{aid}','{sl}')"
|
| 246 |
list_cards.append(f'''<div class="bdp-card" onclick="{click}">
|
| 247 |
<div class="bdp-card-img"><img src="{img}" alt="" class="bdp-lazy-img"><div class="bdp-play-overlay">\u25b6</div></div>
|
| 248 |
+
<div class="bdp-card-body"><span class="bdp-badge {badge_cls}">{source_label}</span>
|
| 249 |
<h3 class="bdp-card-title">{v["title"]}</h3></div></div>''')
|
| 250 |
|
| 251 |
+
grid_html = f'''<div class="bdp-wrap"><div class="bdp-topbar"><span>\U0001f3ac Video</span><span>{len(ordered)} video</span></div>
|
| 252 |
<div class="bdp-grid">{''.join(list_cards)}</div></div>'''
|
| 253 |
|
| 254 |
+
mode_class = "shorts-feed-mode" if is_shorts else ""
|
| 255 |
+
return f'''<div class="tiktok-fullscreen-container {mode_class}" id="tiktok-fullscreen-feed">
|
| 256 |
<div class="tiktok-fullscreen-feed">{''.join(slides)}</div>
|
| 257 |
</div>
|
| 258 |
{grid_html}'''
|