File size: 11,883 Bytes
5385081 501f7c8 025df0a 1e22d57 025df0a 5385081 1e22d57 5385081 856c316 1e22d57 0053998 5385081 2fe32ae 5385081 2fe32ae 5385081 2fe32ae 5385081 4c90cb6 025df0a 1e22d57 5385081 1c9d9ab 5385081 1c9d9ab 5385081 1b03480 5385081 025df0a 5385081 025df0a 5385081 025df0a 5385081 025df0a 5385081 1e22d57 5385081 1e22d57 5385081 0053998 5385081 0053998 5385081 17be8c7 5385081 2b0cb3a 5385081 900d809 5385081 900d809 5385081 501f7c8 5385081 17be8c7 5385081 204c16c 5385081 1e22d57 5385081 17be8c7 5385081 1e22d57 5385081 1e22d57 5385081 204c16c 5385081 1e22d57 5385081 900d809 5385081 025df0a 5385081 025df0a 5385081 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 | """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()
|