VAIVI / app_wrapper.py
bep40's picture
revert: app_wrapper.py back to previous (import from shorts_carousel only)
1e22d57 verified
"""Entry point: shorts carousel + TikTok-style video playback for ALL video sources."""
import sys, os, re as _re
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from shorts_carousel import scrape_24h_news_shorts, render_shorts_carousel, SHORTS_CSS, _extract_24h_video_url
import app
# ══ CSS Patches ══
SHORTS_EXTRA_CSS = """
/* Shorts feed: video 9:16 crop center */
.shorts-feed-mode .tiktok-video{object-fit:cover!important}
/* TikTok feed fullwidth */
.tiktok-fullscreen-container{width:100%!important;max-width:100%!important;border-radius:0!important}
/* Fullheight toggle for highlight */
.tiktok-fullscreen-container.fullheight-mode .tiktok-video{object-fit:cover!important}
.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)}
.tiktok-fullheight-btn.active{background:rgba(92,184,122,.5)}
"""
FULLWIDTH_CSS = """
/* ══ FULLWIDTH: kill all Gradio padding ══ */
body,.gradio-container,.main,.contain{margin:0!important;padding:0!important;max-width:100%!important;width:100%!important}
.gradio-container>.main{padding:0!important}
.gradio-container>.main>.contain{padding:0!important}
.gradio-container>.main>.contain>div{padding:0!important;margin:0!important}
.gradio-container>.main>.contain>div>div{padding:0!important}
#component-0,#component-1,#component-2,#component-3,#component-4,#component-5{padding:0!important;margin:0!important}
.gap{gap:0!important}
.panel,.form,.block{padding:0!important}
div[class*="svelte"]{padding-left:0!important;padding-right:0!important}
.bdp-wrap{padding:6px 4px!important}
.bdp-article{padding:12px 8px 30px!important;max-width:100%!important}
.controls-row{padding:0 4px!important}
.vslide-wrap{margin:4px 2px!important;border-radius:8px!important}
.bdp-header{padding:12px 8px!important}
@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}
}
"""
if "vslide-shorts-item" not in app.CSS:
app.CSS += SHORTS_CSS
if "shorts-feed-mode" not in app.CSS:
app.CSS += SHORTS_EXTRA_CSS
if "FULLWIDTH: kill all Gradio padding" not in app.CSS:
app.CSS += FULLWIDTH_CSS
# ══ Override app._extract_24h_video_urls with fixed multi-part version ══
def _fixed_extract_24h_video_urls(article_url):
vid = _extract_24h_video_url(article_url)
if not vid:
return []
all_parts = vid.get("all_parts", [vid["src"]])
poster = vid.get("poster", "")
return [{"src": p, "poster": poster, "vtype": "hls"} for p in all_parts]
app._extract_24h_video_urls = _fixed_extract_24h_video_urls
# ══ Fix homepage slider: bdpOpenTikTok → bdpOpen with 3 params ══
_orig_render_video_slider = getattr(app, 'render_video_slider_html', None)
if _orig_render_video_slider:
def _patched_render_video_slider(videos):
html = _orig_render_video_slider(videos)
html = html.replace("window.bdpOpenTikTok(", "window.bdpOpen(")
html = _re.sub(
r"window\.bdpOpen\('([^']+)','([^']+)'\)",
r"window.bdpOpen('\1','\2','')",
html
)
return html
app.render_video_slider_html = _patched_render_video_slider
# ══ Patch render_homepage_html: add shorts carousel ══
_orig_render_homepage = app.render_homepage_html
def _patched_render_homepage(articles, *args, **kwargs):
html = _orig_render_homepage(articles, *args, **kwargs)
try:
shorts = scrape_24h_news_shorts()[:15]
shorts_html = render_shorts_carousel(shorts, app.esc, app.safe_url, app.make_id, app.slug)
if shorts_html:
insert_point = html.find('<div class="vslide-wrap">')
if insert_point > 0:
html = html[:insert_point] + shorts_html + html[insert_point:]
else:
insert_point = html.find('<div class="bdp-wrap">') + len('<div class="bdp-wrap">')
if insert_point > len('<div class="bdp-wrap">'):
html = html[:insert_point] + shorts_html + html[insert_point:]
except Exception as e:
print(f"[shorts] Error: {e}")
return html
app.render_homepage_html = _patched_render_homepage
# ══ Patch render_video_page_html: fullheight button ══
_orig_render_video_page = app.render_video_page_html
def _patched_render_video_page():
html = _orig_render_video_page()
toggle_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>'
html = html.replace('<div class="tiktok-unmute-hint"', toggle_btn + '<div class="tiktok-unmute-hint"')
return html
app.render_video_page_html = _patched_render_video_page
# ══ Patch read_article: video URLs → TikTok feed ══
_orig_read_article = app.read_article
def _patched_read_article(url):
if not url or url == "#" or len(url) < 10:
return "<p>Không tìm thấy bài viết.</p>"
if "24h.com.vn" in url:
result = _render_24h_tiktok_feed(url)
if result:
return result
if "bongdaplus.vn/video/" in url:
result = _render_bdp_tiktok_feed(url)
if result:
return result
return _orig_read_article(url)
def _render_bdp_tiktok_feed(current_url):
from concurrent.futures import ThreadPoolExecutor, as_completed
try:
bdp_list = app.scrape_bdp_video_list()[:15]
except:
return None
if not bdp_list:
return None
def _fetch_bdp(art):
vid_id = app._extract_bdp_video_id(art["link"])
if not vid_id: return None
embed = app.fetch_bdp_embed_data(vid_id)
if not embed or not embed.get("mp4"): return None
return [{"title": art["title"], "link": art["link"], "img": art.get("img",""),
"src": embed["mp4"], "poster": embed.get("poster",""), "vtype": "mp4"}]
videos = []
with ThreadPoolExecutor(max_workers=6) as ex:
futures = {ex.submit(_fetch_bdp, a): a for a in bdp_list}
for f in as_completed(futures):
try:
r = f.result()
if r: videos.extend(r)
except: pass
if not videos:
return None
return _build_tiktok_html(videos, current_url, "BDP")
def _render_24h_tiktok_feed(current_url):
from concurrent.futures import ThreadPoolExecutor, as_completed
try:
all_articles = app.scrape_24h_video_list()[:20]
except:
all_articles = []
if len(all_articles) < 10:
try:
shorts = scrape_24h_news_shorts()[:10]
seen = {a["link"] for a in all_articles}
for s in shorts:
if s["link"] not in seen:
all_articles.append(s)
except: pass
if not all_articles:
return None
def _fetch_video(art):
vid = _extract_24h_video_url(art["link"])
if not vid:
return []
results = []
all_parts = vid.get("all_parts", [vid["src"]])
for pi, part_url in enumerate(all_parts):
label = f" (Phần {pi+1})" if len(all_parts) > 1 else ""
results.append({
"title": art["title"] + label, "link": art["link"],
"img": art.get("img", ""), "src": part_url,
"poster": vid["poster"], "vtype": vid["vtype"]
})
return results
videos = []
with ThreadPoolExecutor(max_workers=8) as ex:
futures = {ex.submit(_fetch_video, a): a for a in all_articles}
for f in as_completed(futures):
try:
r = f.result()
if r: videos.extend(r)
except: pass
if not videos:
return None
return _build_tiktok_html(videos, current_url, "24h")
def _build_tiktok_html(videos, current_url, source_label):
current_aid = app.make_id(current_url)
ordered = []
rest = []
for v in videos:
if app.make_id(v["link"]) == current_aid:
ordered.append(v)
else:
rest.append(v)
ordered.extend(rest)
badge_cls = "bdp-badge-24h" if source_label == "24h" else "bdp-badge-bdp"
is_shorts = source_label == "24h"
slides = []
for vi, v in enumerate(ordered):
poster = app.safe_url(v.get("poster", ""))
poster_attr = f' poster="{poster}"' if poster else ""
vsrc = v["src"]
aid = app.make_id(v["link"])
sl = app.slug(v["title"])
share_js = f"event.stopPropagation();window.bdpShareHash('{app.esc(v['title'])}','{sl}','{aid}')"
if v.get("vtype") == "hls":
vtag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} data-hls-src="{vsrc}" muted loop></video>'
else:
vtag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} muted loop><source src="{app.safe_url(vsrc)}" type="video/mp4"></video>'
fullheight_btn = ''
if not is_shorts:
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>'
slides.append(f'''<div class="tiktok-slide" data-index="{vi}" data-aid="{aid}">
{vtag}
<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 {badge_cls}">{source_label}</span>
<p class="tiktok-title">{v["title"]}</p>
<div class="tiktok-actions">
<button class="tiktok-action-btn" onclick="{share_js}">\U0001f4e4 Chia s\u1ebb</button>
</div>
</div>
{fullheight_btn}
<div class="tiktok-unmute-hint" onclick="window.bdpTikTokUnmute(this)">\U0001f507 B\u1eadt ti\u1ebfng</div>
<span class="tiktok-counter">{vi+1}/{len(ordered)}</span>
</div>''')
list_cards = []
for v in ordered:
img = app.safe_url(v.get("img") or v.get("poster",""))
aid = app.make_id(v["link"])
sl = app.slug(v["title"])
click = f"window.bdpOpen('{app.esc(v['link'])}','{aid}','{sl}')"
list_cards.append(f'''<div class="bdp-card" onclick="{click}">
<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 {badge_cls}">{source_label}</span>
<h3 class="bdp-card-title">{v["title"]}</h3></div></div>''')
grid_html = f'''<div class="bdp-wrap"><div class="bdp-topbar"><span>\U0001f3ac Video</span><span>{len(ordered)} video</span></div>
<div class="bdp-grid">{''.join(list_cards)}</div></div>'''
mode_class = "shorts-feed-mode" if is_shorts else ""
return f'''<div class="tiktok-fullscreen-container {mode_class}" id="tiktok-fullscreen-feed">
<div class="tiktok-fullscreen-feed">{''.join(slides)}</div>
</div>
{grid_html}'''
app.read_article = _patched_read_article
# Patch CSS on demo
app.demo.css = app.CSS
# Export demo
demo = app.demo
if __name__ == "__main__":
demo.launch()