Spaces:
Running
Running
fix: async shorts, 3:4 all video feeds, fix dark screen overlay, shorts TikTok feed
Browse files- app_wrapper.py +125 -223
app_wrapper.py
CHANGED
|
@@ -1,269 +1,171 @@
|
|
| 1 |
-
"""Entry point: shorts
|
| 2 |
import sys, os, re as _re
|
|
|
|
| 3 |
|
| 4 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 5 |
from shorts_carousel import scrape_24h_news_shorts, render_shorts_carousel, SHORTS_CSS, _extract_24h_video_url
|
| 6 |
|
| 7 |
import app
|
| 8 |
|
| 9 |
-
# ══ CSS
|
| 10 |
-
|
| 11 |
-
/*
|
| 12 |
-
.
|
| 13 |
-
/* TikTok feed fullwidth */
|
| 14 |
.tiktok-fullscreen-container{width:100%!important;max-width:100%!important;border-radius:0!important}
|
| 15 |
-
/*
|
| 16 |
-
.tiktok-fullscreen-container.
|
| 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 |
-
|
| 22 |
-
/*
|
| 23 |
body,.gradio-container,.main,.contain{margin:0!important;padding:0!important;max-width:100%!important;width:100%!important}
|
| 24 |
-
.gradio-container>.main{padding:0!important}
|
| 25 |
-
.gradio-container>.main>.contain{padding:0!important}
|
| 26 |
-
.gradio-container>.main>.contain>div{padding:0!important;margin:0!important}
|
| 27 |
-
.gradio-container>.main>.contain>div>div{padding:0!important}
|
| 28 |
#component-0,#component-1,#component-2,#component-3,#component-4,#component-5{padding:0!important;margin:0!important}
|
| 29 |
-
.gap{gap:0!important}
|
| 30 |
-
.panel,.form,.block{padding:0!important}
|
| 31 |
div[class*="svelte"]{padding-left:0!important;padding-right:0!important}
|
| 32 |
.bdp-wrap{padding:6px 4px!important}
|
| 33 |
.bdp-article{padding:12px 8px 30px!important;max-width:100%!important}
|
| 34 |
.controls-row{padding:0 4px!important}
|
| 35 |
.vslide-wrap{margin:4px 2px!important;border-radius:8px!important}
|
| 36 |
.bdp-header{padding:12px 8px!important}
|
| 37 |
-
@media(min-width:768px){
|
| 38 |
-
.bdp-wrap{padding:8px!important}
|
| 39 |
-
.bdp-article{padding:16px 12px 40px!important;max-width:720px!important}
|
| 40 |
-
.vslide-wrap{margin:8px!important;border-radius:12px!important}
|
| 41 |
-
}
|
| 42 |
"""
|
| 43 |
|
| 44 |
if "vslide-shorts-item" not in app.CSS:
|
| 45 |
app.CSS += SHORTS_CSS
|
| 46 |
-
if "
|
| 47 |
-
app.CSS +=
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
html = _orig_render_homepage(articles, *args, **kwargs)
|
| 79 |
try:
|
| 80 |
-
shorts =
|
| 81 |
shorts_html = render_shorts_carousel(shorts, app.esc, app.safe_url, app.make_id, app.slug)
|
| 82 |
if shorts_html:
|
| 83 |
-
|
| 84 |
-
if
|
| 85 |
-
html = html[:
|
| 86 |
else:
|
| 87 |
-
|
| 88 |
-
if
|
| 89 |
-
html = html[:
|
| 90 |
-
except
|
| 91 |
-
print(f"[shorts] Error: {e}")
|
| 92 |
return html
|
| 93 |
-
app.render_homepage_html =
|
| 94 |
-
|
| 95 |
-
# ══
|
| 96 |
-
|
| 97 |
-
def
|
| 98 |
-
html =
|
| 99 |
-
|
| 100 |
-
html = html.replace('<div class="tiktok-unmute-hint"',
|
| 101 |
return html
|
| 102 |
-
app.render_video_page_html =
|
| 103 |
|
| 104 |
-
# ══
|
| 105 |
-
|
| 106 |
-
def
|
| 107 |
-
if not url or url
|
| 108 |
return "<p>Không tìm thấy bài viết.</p>"
|
| 109 |
if "24h.com.vn" in url:
|
| 110 |
-
|
| 111 |
-
if
|
| 112 |
-
return result
|
| 113 |
if "bongdaplus.vn/video/" in url:
|
| 114 |
-
|
| 115 |
-
if
|
| 116 |
-
|
| 117 |
-
return _orig_read_article(url)
|
| 118 |
-
|
| 119 |
-
def _render_bdp_tiktok_feed(current_url):
|
| 120 |
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 121 |
-
try:
|
| 122 |
-
bdp_list = app.scrape_bdp_video_list()[:15]
|
| 123 |
-
except:
|
| 124 |
-
return None
|
| 125 |
-
if not bdp_list:
|
| 126 |
-
return None
|
| 127 |
-
def _fetch_bdp(art):
|
| 128 |
-
vid_id = app._extract_bdp_video_id(art["link"])
|
| 129 |
-
if not vid_id: return None
|
| 130 |
-
embed = app.fetch_bdp_embed_data(vid_id)
|
| 131 |
-
if not embed or not embed.get("mp4"): return None
|
| 132 |
-
return [{"title": art["title"], "link": art["link"], "img": art.get("img",""),
|
| 133 |
-
"src": embed["mp4"], "poster": embed.get("poster",""), "vtype": "mp4"}]
|
| 134 |
-
videos = []
|
| 135 |
-
with ThreadPoolExecutor(max_workers=6) as ex:
|
| 136 |
-
futures = {ex.submit(_fetch_bdp, a): a for a in bdp_list}
|
| 137 |
-
for f in as_completed(futures):
|
| 138 |
-
try:
|
| 139 |
-
r = f.result()
|
| 140 |
-
if r: videos.extend(r)
|
| 141 |
-
except: pass
|
| 142 |
-
if not videos:
|
| 143 |
-
return None
|
| 144 |
-
return _build_tiktok_html(videos, current_url, "BDP")
|
| 145 |
|
| 146 |
-
def
|
| 147 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
return []
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
for pi, part_url in enumerate(all_parts):
|
| 170 |
-
label = f" (Phần {pi+1})" if len(all_parts) > 1 else ""
|
| 171 |
-
results.append({
|
| 172 |
-
"title": art["title"] + label, "link": art["link"],
|
| 173 |
-
"img": art.get("img", ""), "src": part_url,
|
| 174 |
-
"poster": vid["poster"], "vtype": vid["vtype"]
|
| 175 |
-
})
|
| 176 |
-
return results
|
| 177 |
-
|
| 178 |
-
videos = []
|
| 179 |
with ThreadPoolExecutor(max_workers=8) as ex:
|
| 180 |
-
|
| 181 |
-
for f in as_completed(futures):
|
| 182 |
try:
|
| 183 |
-
r
|
| 184 |
-
if r:
|
| 185 |
except: pass
|
| 186 |
-
if not
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
else:
|
| 198 |
-
rest.append(v)
|
| 199 |
-
ordered.extend(rest)
|
| 200 |
-
|
| 201 |
-
badge_cls = "bdp-badge-24h" if source_label == "24h" else "bdp-badge-bdp"
|
| 202 |
-
is_shorts = source_label == "24h"
|
| 203 |
-
|
| 204 |
-
slides = []
|
| 205 |
-
for vi, v in enumerate(ordered):
|
| 206 |
-
poster = app.safe_url(v.get("poster", ""))
|
| 207 |
-
poster_attr = f' poster="{poster}"' if poster else ""
|
| 208 |
-
vsrc = v["src"]
|
| 209 |
aid = app.make_id(v["link"])
|
| 210 |
sl = app.slug(v["title"])
|
| 211 |
-
|
| 212 |
-
if v.get("vtype") == "
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
fullheight_btn = ''
|
| 218 |
-
if not is_shorts:
|
| 219 |
-
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>'
|
| 220 |
-
|
| 221 |
-
slides.append(f'''<div class="tiktok-slide" data-index="{vi}" data-aid="{aid}">
|
| 222 |
-
{vtag}
|
| 223 |
-
<div class="tiktok-pause-icon">\u25b6</div>
|
| 224 |
-
<div class="tiktok-seek-controls">
|
| 225 |
-
<button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,-10)">\u23ea 10s</button>
|
| 226 |
-
<button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,10)">10s \u23e9</button>
|
| 227 |
-
</div>
|
| 228 |
-
<div class="tiktok-bottom">
|
| 229 |
-
<span class="bdp-badge {badge_cls}">{source_label}</span>
|
| 230 |
-
<p class="tiktok-title">{v["title"]}</p>
|
| 231 |
-
<div class="tiktok-actions">
|
| 232 |
-
<button class="tiktok-action-btn" onclick="{share_js}">\U0001f4e4 Chia s\u1ebb</button>
|
| 233 |
-
</div>
|
| 234 |
-
</div>
|
| 235 |
-
{fullheight_btn}
|
| 236 |
-
<div class="tiktok-unmute-hint" onclick="window.bdpTikTokUnmute(this)">\U0001f507 B\u1eadt ti\u1ebfng</div>
|
| 237 |
-
<span class="tiktok-counter">{vi+1}/{len(ordered)}</span>
|
| 238 |
-
</div>''')
|
| 239 |
-
|
| 240 |
-
list_cards = []
|
| 241 |
for v in ordered:
|
| 242 |
-
img
|
| 243 |
-
aid
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 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}'''
|
| 259 |
|
| 260 |
-
app.read_article =
|
| 261 |
-
|
| 262 |
-
# Patch CSS on demo
|
| 263 |
app.demo.css = app.CSS
|
| 264 |
-
|
| 265 |
-
# Export demo
|
| 266 |
demo = app.demo
|
| 267 |
-
|
| 268 |
-
if __name__ == "__main__":
|
| 269 |
-
demo.launch()
|
|
|
|
| 1 |
+
"""Entry point: shorts + TikTok video feeds."""
|
| 2 |
import sys, os, re as _re
|
| 3 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 4 |
|
| 5 |
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 6 |
from shorts_carousel import scrape_24h_news_shorts, render_shorts_carousel, SHORTS_CSS, _extract_24h_video_url
|
| 7 |
|
| 8 |
import app
|
| 9 |
|
| 10 |
+
# ══ CSS ══
|
| 11 |
+
EXTRA_CSS = """
|
| 12 |
+
/* ALL TikTok feeds: 3:4 crop center */
|
| 13 |
+
.tiktok-fullscreen-container .tiktok-video{object-fit:cover!important}
|
|
|
|
| 14 |
.tiktok-fullscreen-container{width:100%!important;max-width:100%!important;border-radius:0!important}
|
| 15 |
+
/* Contain toggle */
|
| 16 |
+
.tiktok-fullscreen-container.contain-mode .tiktok-video{object-fit:contain!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 |
+
/* Fix dark screen: pause icon should not block video */
|
| 20 |
+
.tiktok-pause-icon{pointer-events:none!important;background:rgba(0,0,0,.35)!important}
|
| 21 |
+
.tiktok-slide:not(.paused) .tiktok-pause-icon{display:none!important}
|
| 22 |
+
/* FULLWIDTH */
|
| 23 |
body,.gradio-container,.main,.contain{margin:0!important;padding:0!important;max-width:100%!important;width:100%!important}
|
| 24 |
+
.gradio-container>.main,.gradio-container>.main>.contain,.gradio-container>.main>.contain>div,.gradio-container>.main>.contain>div>div{padding:0!important;margin:0!important}
|
|
|
|
|
|
|
|
|
|
| 25 |
#component-0,#component-1,#component-2,#component-3,#component-4,#component-5{padding:0!important;margin:0!important}
|
| 26 |
+
.gap,.panel,.form,.block{gap:0!important;padding:0!important}
|
|
|
|
| 27 |
div[class*="svelte"]{padding-left:0!important;padding-right:0!important}
|
| 28 |
.bdp-wrap{padding:6px 4px!important}
|
| 29 |
.bdp-article{padding:12px 8px 30px!important;max-width:100%!important}
|
| 30 |
.controls-row{padding:0 4px!important}
|
| 31 |
.vslide-wrap{margin:4px 2px!important;border-radius:8px!important}
|
| 32 |
.bdp-header{padding:12px 8px!important}
|
| 33 |
+
@media(min-width:768px){.bdp-wrap{padding:8px!important}.bdp-article{padding:16px 12px 40px!important;max-width:720px!important}.vslide-wrap{margin:8px!important;border-radius:12px!important}}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
"""
|
| 35 |
|
| 36 |
if "vslide-shorts-item" not in app.CSS:
|
| 37 |
app.CSS += SHORTS_CSS
|
| 38 |
+
if "3:4 crop center" not in app.CSS:
|
| 39 |
+
app.CSS += EXTRA_CSS
|
| 40 |
+
|
| 41 |
+
# ══ Multi-part override ══
|
| 42 |
+
def _fixed_extract(url):
|
| 43 |
+
vid = _extract_24h_video_url(url)
|
| 44 |
+
if not vid: return []
|
| 45 |
+
parts = vid.get("all_parts", [vid["src"]])
|
| 46 |
+
return [{"src": p, "poster": vid.get("poster",""), "vtype": "hls"} for p in parts]
|
| 47 |
+
app._extract_24h_video_urls = _fixed_extract
|
| 48 |
+
|
| 49 |
+
# ══ Slider fix ══
|
| 50 |
+
_orig_slider = getattr(app, 'render_video_slider_html', None)
|
| 51 |
+
if _orig_slider:
|
| 52 |
+
def _patch_slider(videos):
|
| 53 |
+
html = _orig_slider(videos)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
html = html.replace("window.bdpOpenTikTok(", "window.bdpOpen(")
|
| 55 |
+
html = _re.sub(r"window\.bdpOpen\('([^']+)','([^']+)'\)", r"window.bdpOpen('\1','\2','')", html)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
return html
|
| 57 |
+
app.render_video_slider_html = _patch_slider
|
| 58 |
+
|
| 59 |
+
# ══ Homepage: shorts carousel (fetched ASYNC, cached) ══
|
| 60 |
+
_shorts_cache = {"data": None}
|
| 61 |
+
|
| 62 |
+
def _get_shorts():
|
| 63 |
+
"""Get shorts with simple cache to avoid re-fetching on timer refresh."""
|
| 64 |
+
if _shorts_cache["data"] is None:
|
| 65 |
+
try:
|
| 66 |
+
_shorts_cache["data"] = scrape_24h_news_shorts()[:15]
|
| 67 |
+
except:
|
| 68 |
+
_shorts_cache["data"] = []
|
| 69 |
+
return _shorts_cache["data"]
|
| 70 |
+
|
| 71 |
+
# Pre-fetch shorts in background thread at startup
|
| 72 |
+
ThreadPoolExecutor(1).submit(_get_shorts)
|
| 73 |
|
| 74 |
+
_orig_homepage = app.render_homepage_html
|
| 75 |
+
def _patch_homepage(articles, *args, **kwargs):
|
| 76 |
+
html = _orig_homepage(articles, *args, **kwargs)
|
|
|
|
| 77 |
try:
|
| 78 |
+
shorts = _get_shorts()
|
| 79 |
shorts_html = render_shorts_carousel(shorts, app.esc, app.safe_url, app.make_id, app.slug)
|
| 80 |
if shorts_html:
|
| 81 |
+
pt = html.find('<div class="vslide-wrap">')
|
| 82 |
+
if pt > 0:
|
| 83 |
+
html = html[:pt] + shorts_html + html[pt:]
|
| 84 |
else:
|
| 85 |
+
pt = html.find('<div class="bdp-wrap">') + len('<div class="bdp-wrap">')
|
| 86 |
+
if pt > len('<div class="bdp-wrap">'):
|
| 87 |
+
html = html[:pt] + shorts_html + html[pt:]
|
| 88 |
+
except: pass
|
|
|
|
| 89 |
return html
|
| 90 |
+
app.render_homepage_html = _patch_homepage
|
| 91 |
+
|
| 92 |
+
# ══ Video page: contain button ══
|
| 93 |
+
_orig_vp = app.render_video_page_html
|
| 94 |
+
def _patch_vp():
|
| 95 |
+
html = _orig_vp()
|
| 96 |
+
btn = '<button class="tiktok-fullheight-btn" onclick="event.stopPropagation();var c=this.closest(\'.tiktok-fullscreen-container\');c.classList.toggle(\'contain-mode\');var on=c.classList.contains(\'contain-mode\');c.querySelectorAll(\'.tiktok-fullheight-btn\').forEach(function(b){b.classList.toggle(\'active\',on);b.textContent=on?\'\\u2922 3:4\':\'\\u2B07 Contain\';});">\u2B07 Contain</button>'
|
| 97 |
+
html = html.replace('<div class="tiktok-unmute-hint"', btn + '<div class="tiktok-unmute-hint"')
|
| 98 |
return html
|
| 99 |
+
app.render_video_page_html = _patch_vp
|
| 100 |
|
| 101 |
+
# ══ read_article → TikTok feed ══
|
| 102 |
+
_orig_read = app.read_article
|
| 103 |
+
def _patch_read(url):
|
| 104 |
+
if not url or url=="#" or len(url)<10:
|
| 105 |
return "<p>Không tìm thấy bài viết.</p>"
|
| 106 |
if "24h.com.vn" in url:
|
| 107 |
+
r = _tiktok_feed("24h", url)
|
| 108 |
+
if r: return r
|
|
|
|
| 109 |
if "bongdaplus.vn/video/" in url:
|
| 110 |
+
r = _tiktok_feed("bdp", url)
|
| 111 |
+
if r: return r
|
| 112 |
+
return _orig_read(url)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
+
def _tiktok_feed(src, current_url):
|
| 115 |
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 116 |
+
if src == "24h":
|
| 117 |
+
arts = _get_shorts()[:15] # Use cached shorts
|
| 118 |
+
if not arts:
|
| 119 |
+
try: arts = app.scrape_24h_video_list()[:15]
|
| 120 |
+
except: return None
|
| 121 |
+
def _f(a):
|
| 122 |
+
v = _extract_24h_video_url(a["link"])
|
| 123 |
+
if not v: return []
|
| 124 |
+
ps = v.get("all_parts", [v["src"]])
|
| 125 |
+
return [{"title":a["title"]+(f" (P{i+1})" if len(ps)>1 else ""),"link":a["link"],"img":a.get("img",""),"src":p,"poster":v["poster"],"vtype":"hls"} for i,p in enumerate(ps)]
|
| 126 |
+
else:
|
| 127 |
+
try: arts = app.scrape_bdp_video_list()[:15]
|
| 128 |
+
except: return None
|
| 129 |
+
def _f(a):
|
| 130 |
+
vid_id = app._extract_bdp_video_id(a["link"])
|
| 131 |
+
if not vid_id: return []
|
| 132 |
+
e = app.fetch_bdp_embed_data(vid_id)
|
| 133 |
+
if not e or not e.get("mp4"): return []
|
| 134 |
+
return [{"title":a["title"],"link":a["link"],"img":a.get("img",""),"src":e["mp4"],"poster":e.get("poster",""),"vtype":"mp4"}]
|
| 135 |
+
if not arts: return None
|
| 136 |
+
vids = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
with ThreadPoolExecutor(max_workers=8) as ex:
|
| 138 |
+
for f in as_completed({ex.submit(_f,a):a for a in arts}):
|
|
|
|
| 139 |
try:
|
| 140 |
+
r=f.result()
|
| 141 |
+
if r: vids.extend(r)
|
| 142 |
except: pass
|
| 143 |
+
if not vids: return None
|
| 144 |
+
# Order: clicked first
|
| 145 |
+
ca = app.make_id(current_url)
|
| 146 |
+
ordered = [v for v in vids if app.make_id(v["link"])==ca] + [v for v in vids if app.make_id(v["link"])!=ca]
|
| 147 |
+
bg = "bdp-badge-24h" if src=="24h" else "bdp-badge-bdp"
|
| 148 |
+
lb = "24h" if src=="24h" else "BDP"
|
| 149 |
+
slides=[]
|
| 150 |
+
for i,v in enumerate(ordered):
|
| 151 |
+
po = app.safe_url(v.get("poster",""))
|
| 152 |
+
pa = f' poster="{po}"' if po else ""
|
| 153 |
+
vs = v["src"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
aid = app.make_id(v["link"])
|
| 155 |
sl = app.slug(v["title"])
|
| 156 |
+
sj = f"event.stopPropagation();window.bdpShareHash('{app.esc(v['title'])}','{sl}','{aid}')"
|
| 157 |
+
vt = f'<video class="tiktok-video" playsinline preload="metadata"{pa} data-hls-src="{vs}" muted loop></video>' if v.get("vtype")=="hls" else f'<video class="tiktok-video" playsinline preload="metadata"{pa} muted loop><source src="{app.safe_url(vs)}" type="video/mp4"></video>'
|
| 158 |
+
cb = '<button class="tiktok-fullheight-btn" onclick="event.stopPropagation();var c=this.closest(\'.tiktok-fullscreen-container\');c.classList.toggle(\'contain-mode\');var on=c.classList.contains(\'contain-mode\');c.querySelectorAll(\'.tiktok-fullheight-btn\').forEach(function(b){b.classList.toggle(\'active\',on);b.textContent=on?\'\\u2922 3:4\':\'\\u2B07 Contain\';});">\u2B07 Contain</button>'
|
| 159 |
+
slides.append(f'<div class="tiktok-slide" data-index="{i}" data-aid="{aid}">{vt}<div class="tiktok-pause-icon">\u25b6</div><div class="tiktok-seek-controls"><button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,-10)">\u23ea 10s</button><button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,10)">10s \u23e9</button></div><div class="tiktok-bottom"><span class="bdp-badge {bg}">{lb}</span><p class="tiktok-title">{v["title"]}</p><div class="tiktok-actions"><button class="tiktok-action-btn" onclick="{sj}">\U0001f4e4</button></div></div>{cb}<div class="tiktok-unmute-hint" onclick="window.bdpTikTokUnmute(this)">\U0001f507 B\u1eadt ti\u1ebfng</div><span class="tiktok-counter">{i+1}/{len(ordered)}</span></div>')
|
| 160 |
+
cards=[]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
for v in ordered:
|
| 162 |
+
img=app.safe_url(v.get("img")or v.get("poster",""))
|
| 163 |
+
aid=app.make_id(v["link"]);sl=app.slug(v["title"])
|
| 164 |
+
cards.append(f'<div class="bdp-card" onclick="window.bdpOpen(\'{app.esc(v["link"])}\',\'{aid}\',\'{sl}\')"><div class="bdp-card-img"><img src="{img}" alt="" class="bdp-lazy-img"><div class="bdp-play-overlay">\u25b6</div></div><div class="bdp-card-body"><span class="bdp-badge {bg}">{lb}</span><h3 class="bdp-card-title">{v["title"]}</h3></div></div>')
|
| 165 |
+
grid=f'<div class="bdp-wrap"><div class="bdp-topbar"><span>\U0001f3ac Video</span><span>{len(ordered)} video</span></div><div class="bdp-grid">{"".join(cards)}</div></div>'
|
| 166 |
+
return f'<div class="tiktok-fullscreen-container" id="tiktok-fullscreen-feed"><div class="tiktok-fullscreen-feed">{"".join(slides)}</div></div>{grid}'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
+
app.read_article = _patch_read
|
|
|
|
|
|
|
| 169 |
app.demo.css = app.CSS
|
|
|
|
|
|
|
| 170 |
demo = app.demo
|
| 171 |
+
if __name__=="__main__": demo.launch()
|
|
|
|
|
|