"""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('
') if insert_point > 0: html = html[:insert_point] + shorts_html + html[insert_point:] else: insert_point = html.find('
') + len('
') if insert_point > len('
'): 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 = '' html = html.replace('
Không tìm thấy bài viết.

" 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'' else: vtag = f'' fullheight_btn = '' if not is_shorts: fullheight_btn = '' slides.append(f'''
{vtag}
\u25b6
{source_label}

{v["title"]}

{fullheight_btn}
\U0001f507 B\u1eadt ti\u1ebfng
{vi+1}/{len(ordered)}
''') 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'''
\u25b6
{source_label}

{v["title"]}

''') grid_html = f'''
\U0001f3ac Video{len(ordered)} video
{''.join(list_cards)}
''' mode_class = "shorts-feed-mode" if is_shorts else "" return f'''
{''.join(slides)}
{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()