Spaces:
Running
Running
revert: app_wrapper.py back to previous (shorts routing, no comment/share changes)
Browse files- app_wrapper.py +112 -120
app_wrapper.py
CHANGED
|
@@ -16,17 +16,6 @@ SHORTS_EXTRA_CSS = """
|
|
| 16 |
.tiktok-fullscreen-container.fullheight-mode .tiktok-video{object-fit:cover!important}
|
| 17 |
.tiktok-fullheight-btn{position:absolute;bottom:70px;right:12px;background:rgba(255,255,255,.15);color:#fff;border:none;padding:6px 12px;border-radius:16px;font-size:11px;cursor:pointer;z-index:8;backdrop-filter:blur(6px)}
|
| 18 |
.tiktok-fullheight-btn.active{background:rgba(92,184,122,.5)}
|
| 19 |
-
/* Comment button on right side */
|
| 20 |
-
.tiktok-comment-btn{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:rgba(255,255,255,.15);color:#fff;border:none;width:44px;height:44px;border-radius:50%;font-size:20px;cursor:pointer;z-index:8;backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;transition:background .2s}
|
| 21 |
-
.tiktok-comment-btn:hover{background:rgba(255,255,255,.3)}
|
| 22 |
-
.tiktok-comment-panel{position:absolute;right:0;top:0;bottom:0;width:280px;background:rgba(17,17,17,.95);z-index:20;padding:12px;overflow-y:auto;transform:translateX(100%);transition:transform .3s;border-left:1px solid #333}
|
| 23 |
-
.tiktok-comment-panel.open{transform:translateX(0)}
|
| 24 |
-
.tiktok-comment-panel h4{color:#fff;font-size:14px;margin:0 0 10px}
|
| 25 |
-
.tiktok-comment-panel .cmt-input{width:100%;padding:8px;background:#222;border:1px solid #444;border-radius:6px;color:#eee;font-size:12px;margin-bottom:6px;box-sizing:border-box}
|
| 26 |
-
.tiktok-comment-panel .cmt-send{background:#2d8659;color:#fff;border:none;padding:6px 14px;border-radius:14px;font-size:12px;cursor:pointer}
|
| 27 |
-
.tiktok-comment-panel .cmt-list{margin-top:10px}
|
| 28 |
-
.tiktok-comment-panel .cmt-item{background:#1a1a1a;border-radius:6px;padding:6px 8px;margin-bottom:6px;font-size:11px;color:#ccc}
|
| 29 |
-
.tiktok-comment-panel .cmt-close{position:absolute;top:8px;right:8px;background:none;border:none;color:#aaa;font-size:18px;cursor:pointer}
|
| 30 |
"""
|
| 31 |
|
| 32 |
FULLWIDTH_CSS = """
|
|
@@ -59,21 +48,27 @@ if "shorts-feed-mode" not in app.CSS:
|
|
| 59 |
if "FULLWIDTH: kill all Gradio padding" not in app.CSS:
|
| 60 |
app.CSS += FULLWIDTH_CSS
|
| 61 |
|
| 62 |
-
# ══ Override app._extract_24h_video_urls ══
|
| 63 |
def _fixed_extract_24h_video_urls(article_url):
|
| 64 |
vid = _extract_24h_video_url(article_url)
|
| 65 |
-
if not vid:
|
|
|
|
| 66 |
all_parts = vid.get("all_parts", [vid["src"]])
|
| 67 |
-
|
|
|
|
| 68 |
app._extract_24h_video_urls = _fixed_extract_24h_video_urls
|
| 69 |
|
| 70 |
-
# ══ Fix homepage slider ══
|
| 71 |
_orig_render_video_slider = getattr(app, 'render_video_slider_html', None)
|
| 72 |
if _orig_render_video_slider:
|
| 73 |
def _patched_render_video_slider(videos):
|
| 74 |
html = _orig_render_video_slider(videos)
|
| 75 |
html = html.replace("window.bdpOpenTikTok(", "window.bdpOpen(")
|
| 76 |
-
html = _re.sub(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
return html
|
| 78 |
app.render_video_slider_html = _patched_render_video_slider
|
| 79 |
|
|
@@ -85,13 +80,13 @@ def _patched_render_homepage(articles, *args, **kwargs):
|
|
| 85 |
shorts = scrape_24h_news_shorts()[:15]
|
| 86 |
shorts_html = render_shorts_carousel(shorts, app.esc, app.safe_url, app.make_id, app.slug)
|
| 87 |
if shorts_html:
|
| 88 |
-
|
| 89 |
-
if
|
| 90 |
-
html = html[:
|
| 91 |
else:
|
| 92 |
-
|
| 93 |
-
if
|
| 94 |
-
html = html[:
|
| 95 |
except Exception as e:
|
| 96 |
print(f"[shorts] Error: {e}")
|
| 97 |
return html
|
|
@@ -106,131 +101,161 @@ def _patched_render_video_page():
|
|
| 106 |
return html
|
| 107 |
app.render_video_page_html = _patched_render_video_page
|
| 108 |
|
| 109 |
-
# ══ Patch read_article ══
|
| 110 |
_orig_read_article = app.read_article
|
| 111 |
def _patched_read_article(url):
|
| 112 |
if not url or url == "#" or len(url) < 10:
|
| 113 |
return "<p>Không tìm thấy bài viết.</p>"
|
| 114 |
if "24h.com.vn" in url:
|
| 115 |
is_highlight = "/bong-da/video-bong-da-" in url or "video-highlight" in url
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
if "bongdaplus.vn/video/" in url:
|
| 119 |
result = _render_bdp_tiktok_feed(url)
|
| 120 |
-
if result:
|
|
|
|
| 121 |
return _orig_read_article(url)
|
| 122 |
|
| 123 |
def _render_bdp_tiktok_feed(current_url):
|
| 124 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 125 |
-
try:
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
| 129 |
vid_id = app._extract_bdp_video_id(art["link"])
|
| 130 |
-
if not vid_id: return
|
| 131 |
embed = app.fetch_bdp_embed_data(vid_id)
|
| 132 |
-
if not embed or not embed.get("mp4"): return
|
| 133 |
return [{"title": art["title"], "link": art["link"], "img": art.get("img",""),
|
| 134 |
"src": embed["mp4"], "poster": embed.get("poster",""), "vtype": "mp4"}]
|
| 135 |
videos = []
|
| 136 |
with ThreadPoolExecutor(max_workers=6) as ex:
|
| 137 |
-
|
|
|
|
| 138 |
try:
|
| 139 |
r = f.result()
|
| 140 |
if r: videos.extend(r)
|
| 141 |
except: pass
|
| 142 |
-
|
|
|
|
|
|
|
| 143 |
|
| 144 |
def _render_shorts_tiktok_feed(current_url):
|
|
|
|
| 145 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 146 |
-
try:
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
| 150 |
vid = _extract_24h_video_url(art["link"])
|
| 151 |
-
if not vid:
|
| 152 |
-
|
| 153 |
-
|
|
|
|
|
|
|
| 154 |
videos = []
|
| 155 |
with ThreadPoolExecutor(max_workers=8) as ex:
|
| 156 |
-
|
|
|
|
| 157 |
try:
|
| 158 |
r = f.result()
|
| 159 |
if r: videos.extend(r)
|
| 160 |
except: pass
|
| 161 |
-
|
|
|
|
|
|
|
| 162 |
|
| 163 |
def _render_highlight_tiktok_feed(current_url):
|
|
|
|
| 164 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 165 |
-
try:
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
| 169 |
vid = _extract_24h_video_url(art["link"])
|
| 170 |
-
if not vid:
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
videos = []
|
| 176 |
with ThreadPoolExecutor(max_workers=8) as ex:
|
| 177 |
-
|
|
|
|
| 178 |
try:
|
| 179 |
r = f.result()
|
| 180 |
if r: videos.extend(r)
|
| 181 |
except: pass
|
| 182 |
-
|
|
|
|
|
|
|
| 183 |
|
| 184 |
def _build_tiktok_html(videos, current_url, source_label):
|
| 185 |
-
|
| 186 |
-
ordered = [
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
is_shorts = source_label == "24h"
|
| 189 |
|
| 190 |
slides = []
|
| 191 |
for vi, v in enumerate(ordered):
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
aid = app.make_id(v["link"])
|
| 196 |
sl = app.slug(v["title"])
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
| 201 |
|
| 202 |
-
|
| 203 |
if not is_shorts:
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
# Comment button + panel
|
| 207 |
-
cmt_btn = f'<button class="tiktok-comment-btn" onclick="event.stopPropagation();var p=this.nextElementSibling;p.classList.toggle(\'open\');">\U0001f4ac</button>'
|
| 208 |
-
cmt_panel = f'''<div class="tiktok-comment-panel" onclick="event.stopPropagation();">
|
| 209 |
-
<button class="cmt-close" onclick="this.parentElement.classList.remove('open')">\u2715</button>
|
| 210 |
-
<h4>\U0001f4ac B\u00ecnh lu\u1eadn</h4>
|
| 211 |
-
<div class="cmt-list" id="tiktok-cmt-{aid}"></div>
|
| 212 |
-
<input class="cmt-input" id="tiktok-cmt-name-{aid}" placeholder="T\u00ean..." maxlength="30">
|
| 213 |
-
<textarea class="cmt-input" id="tiktok-cmt-text-{aid}" placeholder="Vi\u1ebft b\u00ecnh lu\u1eadn..." rows="2" maxlength="300" style="resize:none"></textarea>
|
| 214 |
-
<button class="cmt-send" onclick="window.bdpAddCmt('{aid}');window.bdpRenderCmtTiktok('{aid}');">G\u1eedi</button>
|
| 215 |
-
</div>'''
|
| 216 |
|
| 217 |
slides.append(f'''<div class="tiktok-slide" data-index="{vi}" data-aid="{aid}">
|
| 218 |
-
{
|
| 219 |
<div class="tiktok-pause-icon">\u25b6</div>
|
| 220 |
<div class="tiktok-seek-controls">
|
| 221 |
<button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,-10)">\u23ea 10s</button>
|
| 222 |
<button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,10)">10s \u23e9</button>
|
| 223 |
</div>
|
| 224 |
<div class="tiktok-bottom">
|
| 225 |
-
<span class="bdp-badge {
|
| 226 |
<p class="tiktok-title">{v["title"]}</p>
|
| 227 |
<div class="tiktok-actions">
|
| 228 |
-
<button class="tiktok-action-btn" onclick="{
|
| 229 |
</div>
|
| 230 |
</div>
|
| 231 |
-
{
|
| 232 |
-
{cmt_btn}
|
| 233 |
-
{cmt_panel}
|
| 234 |
<div class="tiktok-unmute-hint" onclick="window.bdpTikTokUnmute(this)">\U0001f507 B\u1eadt ti\u1ebfng</div>
|
| 235 |
<span class="tiktok-counter">{vi+1}/{len(ordered)}</span>
|
| 236 |
</div>''')
|
|
@@ -243,53 +268,20 @@ def _build_tiktok_html(videos, current_url, source_label):
|
|
| 243 |
click = f"window.bdpOpen('{app.esc(v['link'])}','{aid}','{sl}')"
|
| 244 |
list_cards.append(f'''<div class="bdp-card" onclick="{click}">
|
| 245 |
<div class="bdp-card-img"><img src="{img}" alt="" class="bdp-lazy-img"><div class="bdp-play-overlay">\u25b6</div></div>
|
| 246 |
-
<div class="bdp-card-body"><span class="bdp-badge {
|
| 247 |
<h3 class="bdp-card-title">{v["title"]}</h3></div></div>''')
|
| 248 |
|
| 249 |
-
|
|
|
|
| 250 |
|
| 251 |
mode_class = "shorts-feed-mode" if is_shorts else ""
|
| 252 |
return f'''<div class="tiktok-fullscreen-container {mode_class}" id="tiktok-fullscreen-feed">
|
| 253 |
<div class="tiktok-fullscreen-feed">{''.join(slides)}</div>
|
| 254 |
</div>
|
| 255 |
-
{
|
| 256 |
|
| 257 |
app.read_article = _patched_read_article
|
| 258 |
|
| 259 |
-
# ══ Patch JS: add bdpShareWithImage and bdpRenderCmtTiktok functions ══
|
| 260 |
-
# These are added via CSS (actually head meta won't work, but we can add to JS_FUNC)
|
| 261 |
-
_orig_js = app.JS_FUNC
|
| 262 |
-
_extra_js = """
|
| 263 |
-
/* Share with image for og:image */
|
| 264 |
-
window.bdpShareWithImage=async function(title,sl,aid,imgUrl){
|
| 265 |
-
var hashUrl=SPACE_BASE+'/#/'+sl+'/'+aid;
|
| 266 |
-
var sd={title:title,url:hashUrl,text:title};
|
| 267 |
-
if(navigator.share){try{await navigator.share(sd);return;}catch(e){if(e.name==='AbortError')return;}}
|
| 268 |
-
if(navigator.clipboard&&navigator.clipboard.writeText){try{await navigator.clipboard.writeText(hashUrl);window.bdpToast('\\u2705 \\u0110\\u00e3 sao ch\\u00e9p li\\u00ean k\\u1ebft!');return;}catch(e){}}
|
| 269 |
-
var ta=document.createElement('textarea');ta.value=hashUrl;ta.style.cssText='position:fixed;opacity:0';
|
| 270 |
-
document.body.appendChild(ta);ta.focus();ta.select();
|
| 271 |
-
try{document.execCommand('copy');window.bdpToast('\\u2705 \\u0110\\u00e3 sao ch\\u00e9p!');}catch(e){}
|
| 272 |
-
document.body.removeChild(ta);
|
| 273 |
-
};
|
| 274 |
-
/* Render comments in TikTok panel */
|
| 275 |
-
window.bdpRenderCmtTiktok=function(a){
|
| 276 |
-
var l=document.getElementById('tiktok-cmt-'+a);if(!l)return;
|
| 277 |
-
try{var c=JSON.parse(localStorage.getItem('bdp_cmt_'+a))||[];}catch(e){var c=[];}
|
| 278 |
-
if(!c.length){l.innerHTML='<div style="color:#666;font-size:11px;">Ch\\u01b0a c\\u00f3 b\\u00ecnh lu\\u1eadn</div>';return;}
|
| 279 |
-
var h='';for(var i=c.length-1;i>=0;i--){var x=c[i];h+='<div class="cmt-item"><b style="color:#5cb87a">'+x.name+'</b> <span style="color:#555;font-size:10px">'+x.date+'</span><div>'+x.text.replace(/</g,'<')+'</div></div>';}
|
| 280 |
-
l.innerHTML=h;
|
| 281 |
-
};
|
| 282 |
-
"""
|
| 283 |
-
|
| 284 |
-
# Inject extra JS before the closing } of the JS_FUNC
|
| 285 |
-
if "bdpShareWithImage" not in _orig_js:
|
| 286 |
-
# Insert before the last closing brace and the hash routing section
|
| 287 |
-
insert_point = _orig_js.rfind("var hh=window.location.hash")
|
| 288 |
-
if insert_point > 0:
|
| 289 |
-
app.JS_FUNC = _orig_js[:insert_point] + _extra_js + "\n" + _orig_js[insert_point:]
|
| 290 |
-
else:
|
| 291 |
-
app.JS_FUNC = _orig_js.rstrip().rstrip("}") + _extra_js + "\n}"
|
| 292 |
-
|
| 293 |
# Patch CSS on demo
|
| 294 |
app.demo.css = app.CSS
|
| 295 |
|
|
|
|
| 16 |
.tiktok-fullscreen-container.fullheight-mode .tiktok-video{object-fit:cover!important}
|
| 17 |
.tiktok-fullheight-btn{position:absolute;bottom:70px;right:12px;background:rgba(255,255,255,.15);color:#fff;border:none;padding:6px 12px;border-radius:16px;font-size:11px;cursor:pointer;z-index:8;backdrop-filter:blur(6px)}
|
| 18 |
.tiktok-fullheight-btn.active{background:rgba(92,184,122,.5)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
"""
|
| 20 |
|
| 21 |
FULLWIDTH_CSS = """
|
|
|
|
| 48 |
if "FULLWIDTH: kill all Gradio padding" not in app.CSS:
|
| 49 |
app.CSS += FULLWIDTH_CSS
|
| 50 |
|
| 51 |
+
# ══ Override app._extract_24h_video_urls with fixed multi-part version ══
|
| 52 |
def _fixed_extract_24h_video_urls(article_url):
|
| 53 |
vid = _extract_24h_video_url(article_url)
|
| 54 |
+
if not vid:
|
| 55 |
+
return []
|
| 56 |
all_parts = vid.get("all_parts", [vid["src"]])
|
| 57 |
+
poster = vid.get("poster", "")
|
| 58 |
+
return [{"src": p, "poster": poster, "vtype": "hls"} for p in all_parts]
|
| 59 |
app._extract_24h_video_urls = _fixed_extract_24h_video_urls
|
| 60 |
|
| 61 |
+
# ══ Fix homepage slider: bdpOpenTikTok → bdpOpen with 3 params ══
|
| 62 |
_orig_render_video_slider = getattr(app, 'render_video_slider_html', None)
|
| 63 |
if _orig_render_video_slider:
|
| 64 |
def _patched_render_video_slider(videos):
|
| 65 |
html = _orig_render_video_slider(videos)
|
| 66 |
html = html.replace("window.bdpOpenTikTok(", "window.bdpOpen(")
|
| 67 |
+
html = _re.sub(
|
| 68 |
+
r"window\.bdpOpen\('([^']+)','([^']+)'\)",
|
| 69 |
+
r"window.bdpOpen('\1','\2','')",
|
| 70 |
+
html
|
| 71 |
+
)
|
| 72 |
return html
|
| 73 |
app.render_video_slider_html = _patched_render_video_slider
|
| 74 |
|
|
|
|
| 80 |
shorts = scrape_24h_news_shorts()[:15]
|
| 81 |
shorts_html = render_shorts_carousel(shorts, app.esc, app.safe_url, app.make_id, app.slug)
|
| 82 |
if shorts_html:
|
| 83 |
+
insert_point = html.find('<div class="vslide-wrap">')
|
| 84 |
+
if insert_point > 0:
|
| 85 |
+
html = html[:insert_point] + shorts_html + html[insert_point:]
|
| 86 |
else:
|
| 87 |
+
insert_point = html.find('<div class="bdp-wrap">') + len('<div class="bdp-wrap">')
|
| 88 |
+
if insert_point > len('<div class="bdp-wrap">'):
|
| 89 |
+
html = html[:insert_point] + shorts_html + html[insert_point:]
|
| 90 |
except Exception as e:
|
| 91 |
print(f"[shorts] Error: {e}")
|
| 92 |
return html
|
|
|
|
| 101 |
return html
|
| 102 |
app.render_video_page_html = _patched_render_video_page
|
| 103 |
|
| 104 |
+
# ══ Patch read_article: video URLs → TikTok feed ══
|
| 105 |
_orig_read_article = app.read_article
|
| 106 |
def _patched_read_article(url):
|
| 107 |
if not url or url == "#" or len(url) < 10:
|
| 108 |
return "<p>Không tìm thấy bài viết.</p>"
|
| 109 |
if "24h.com.vn" in url:
|
| 110 |
is_highlight = "/bong-da/video-bong-da-" in url or "video-highlight" in url
|
| 111 |
+
if is_highlight:
|
| 112 |
+
result = _render_highlight_tiktok_feed(url)
|
| 113 |
+
else:
|
| 114 |
+
result = _render_shorts_tiktok_feed(url)
|
| 115 |
+
if result:
|
| 116 |
+
return result
|
| 117 |
if "bongdaplus.vn/video/" in url:
|
| 118 |
result = _render_bdp_tiktok_feed(url)
|
| 119 |
+
if result:
|
| 120 |
+
return result
|
| 121 |
return _orig_read_article(url)
|
| 122 |
|
| 123 |
def _render_bdp_tiktok_feed(current_url):
|
| 124 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 125 |
+
try:
|
| 126 |
+
bdp_list = app.scrape_bdp_video_list()[:15]
|
| 127 |
+
except:
|
| 128 |
+
return None
|
| 129 |
+
if not bdp_list:
|
| 130 |
+
return None
|
| 131 |
+
def _fetch_bdp(art):
|
| 132 |
vid_id = app._extract_bdp_video_id(art["link"])
|
| 133 |
+
if not vid_id: return None
|
| 134 |
embed = app.fetch_bdp_embed_data(vid_id)
|
| 135 |
+
if not embed or not embed.get("mp4"): return None
|
| 136 |
return [{"title": art["title"], "link": art["link"], "img": art.get("img",""),
|
| 137 |
"src": embed["mp4"], "poster": embed.get("poster",""), "vtype": "mp4"}]
|
| 138 |
videos = []
|
| 139 |
with ThreadPoolExecutor(max_workers=6) as ex:
|
| 140 |
+
futures = {ex.submit(_fetch_bdp, a): a for a in bdp_list}
|
| 141 |
+
for f in as_completed(futures):
|
| 142 |
try:
|
| 143 |
r = f.result()
|
| 144 |
if r: videos.extend(r)
|
| 145 |
except: pass
|
| 146 |
+
if not videos:
|
| 147 |
+
return None
|
| 148 |
+
return _build_tiktok_html(videos, current_url, "BDP")
|
| 149 |
|
| 150 |
def _render_shorts_tiktok_feed(current_url):
|
| 151 |
+
"""TikTok feed using SHORTS articles (from video-tin-tuc page)."""
|
| 152 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 153 |
+
try:
|
| 154 |
+
all_articles = scrape_24h_news_shorts()[:15]
|
| 155 |
+
except:
|
| 156 |
+
all_articles = []
|
| 157 |
+
if not all_articles:
|
| 158 |
+
return None
|
| 159 |
+
def _fetch_video(art):
|
| 160 |
vid = _extract_24h_video_url(art["link"])
|
| 161 |
+
if not vid:
|
| 162 |
+
return []
|
| 163 |
+
return [{"title": art["title"], "link": art["link"],
|
| 164 |
+
"img": art.get("img", ""), "src": vid["src"],
|
| 165 |
+
"poster": vid["poster"], "vtype": vid["vtype"]}]
|
| 166 |
videos = []
|
| 167 |
with ThreadPoolExecutor(max_workers=8) as ex:
|
| 168 |
+
futures = {ex.submit(_fetch_video, a): a for a in all_articles}
|
| 169 |
+
for f in as_completed(futures):
|
| 170 |
try:
|
| 171 |
r = f.result()
|
| 172 |
if r: videos.extend(r)
|
| 173 |
except: pass
|
| 174 |
+
if not videos:
|
| 175 |
+
return None
|
| 176 |
+
return _build_tiktok_html(videos, current_url, "24h")
|
| 177 |
|
| 178 |
def _render_highlight_tiktok_feed(current_url):
|
| 179 |
+
"""TikTok feed using HIGHLIGHT articles (video-bong-da)."""
|
| 180 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 181 |
+
try:
|
| 182 |
+
all_articles = app.scrape_24h_video_list()[:20]
|
| 183 |
+
except:
|
| 184 |
+
all_articles = []
|
| 185 |
+
if not all_articles:
|
| 186 |
+
return None
|
| 187 |
+
def _fetch_video(art):
|
| 188 |
vid = _extract_24h_video_url(art["link"])
|
| 189 |
+
if not vid:
|
| 190 |
+
return []
|
| 191 |
+
results = []
|
| 192 |
+
all_parts = vid.get("all_parts", [vid["src"]])
|
| 193 |
+
for pi, part_url in enumerate(all_parts):
|
| 194 |
+
label = f" (Phần {pi+1})" if len(all_parts) > 1 else ""
|
| 195 |
+
results.append({
|
| 196 |
+
"title": art["title"] + label, "link": art["link"],
|
| 197 |
+
"img": art.get("img", ""), "src": part_url,
|
| 198 |
+
"poster": vid["poster"], "vtype": vid["vtype"]
|
| 199 |
+
})
|
| 200 |
+
return results
|
| 201 |
videos = []
|
| 202 |
with ThreadPoolExecutor(max_workers=8) as ex:
|
| 203 |
+
futures = {ex.submit(_fetch_video, a): a for a in all_articles}
|
| 204 |
+
for f in as_completed(futures):
|
| 205 |
try:
|
| 206 |
r = f.result()
|
| 207 |
if r: videos.extend(r)
|
| 208 |
except: pass
|
| 209 |
+
if not videos:
|
| 210 |
+
return None
|
| 211 |
+
return _build_tiktok_html(videos, current_url, "24h")
|
| 212 |
|
| 213 |
def _build_tiktok_html(videos, current_url, source_label):
|
| 214 |
+
current_aid = app.make_id(current_url)
|
| 215 |
+
ordered = []
|
| 216 |
+
rest = []
|
| 217 |
+
for v in videos:
|
| 218 |
+
if app.make_id(v["link"]) == current_aid:
|
| 219 |
+
ordered.append(v)
|
| 220 |
+
else:
|
| 221 |
+
rest.append(v)
|
| 222 |
+
ordered.extend(rest)
|
| 223 |
+
|
| 224 |
+
badge_cls = "bdp-badge-24h" if source_label == "24h" else "bdp-badge-bdp"
|
| 225 |
is_shorts = source_label == "24h"
|
| 226 |
|
| 227 |
slides = []
|
| 228 |
for vi, v in enumerate(ordered):
|
| 229 |
+
poster = app.safe_url(v.get("poster", ""))
|
| 230 |
+
poster_attr = f' poster="{poster}"' if poster else ""
|
| 231 |
+
vsrc = v["src"]
|
| 232 |
aid = app.make_id(v["link"])
|
| 233 |
sl = app.slug(v["title"])
|
| 234 |
+
share_js = f"event.stopPropagation();window.bdpShareHash('{app.esc(v['title'])}','{sl}','{aid}')"
|
| 235 |
+
if v.get("vtype") == "hls":
|
| 236 |
+
vtag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} data-hls-src="{vsrc}" muted loop></video>'
|
| 237 |
+
else:
|
| 238 |
+
vtag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} muted loop><source src="{app.safe_url(vsrc)}" type="video/mp4"></video>'
|
| 239 |
|
| 240 |
+
fullheight_btn = ''
|
| 241 |
if not is_shorts:
|
| 242 |
+
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>'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
slides.append(f'''<div class="tiktok-slide" data-index="{vi}" data-aid="{aid}">
|
| 245 |
+
{vtag}
|
| 246 |
<div class="tiktok-pause-icon">\u25b6</div>
|
| 247 |
<div class="tiktok-seek-controls">
|
| 248 |
<button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,-10)">\u23ea 10s</button>
|
| 249 |
<button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,10)">10s \u23e9</button>
|
| 250 |
</div>
|
| 251 |
<div class="tiktok-bottom">
|
| 252 |
+
<span class="bdp-badge {badge_cls}">{source_label}</span>
|
| 253 |
<p class="tiktok-title">{v["title"]}</p>
|
| 254 |
<div class="tiktok-actions">
|
| 255 |
+
<button class="tiktok-action-btn" onclick="{share_js}">\U0001f4e4 Chia s\u1ebb</button>
|
| 256 |
</div>
|
| 257 |
</div>
|
| 258 |
+
{fullheight_btn}
|
|
|
|
|
|
|
| 259 |
<div class="tiktok-unmute-hint" onclick="window.bdpTikTokUnmute(this)">\U0001f507 B\u1eadt ti\u1ebfng</div>
|
| 260 |
<span class="tiktok-counter">{vi+1}/{len(ordered)}</span>
|
| 261 |
</div>''')
|
|
|
|
| 268 |
click = f"window.bdpOpen('{app.esc(v['link'])}','{aid}','{sl}')"
|
| 269 |
list_cards.append(f'''<div class="bdp-card" onclick="{click}">
|
| 270 |
<div class="bdp-card-img"><img src="{img}" alt="" class="bdp-lazy-img"><div class="bdp-play-overlay">\u25b6</div></div>
|
| 271 |
+
<div class="bdp-card-body"><span class="bdp-badge {badge_cls}">{source_label}</span>
|
| 272 |
<h3 class="bdp-card-title">{v["title"]}</h3></div></div>''')
|
| 273 |
|
| 274 |
+
grid_html = f'''<div class="bdp-wrap"><div class="bdp-topbar"><span>\U0001f3ac Video</span><span>{len(ordered)} video</span></div>
|
| 275 |
+
<div class="bdp-grid">{''.join(list_cards)}</div></div>'''
|
| 276 |
|
| 277 |
mode_class = "shorts-feed-mode" if is_shorts else ""
|
| 278 |
return f'''<div class="tiktok-fullscreen-container {mode_class}" id="tiktok-fullscreen-feed">
|
| 279 |
<div class="tiktok-fullscreen-feed">{''.join(slides)}</div>
|
| 280 |
</div>
|
| 281 |
+
{grid_html}'''
|
| 282 |
|
| 283 |
app.read_article = _patched_read_article
|
| 284 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
# Patch CSS on demo
|
| 286 |
app.demo.css = app.CSS
|
| 287 |
|