bep40 commited on
Commit
a38c268
·
verified ·
1 Parent(s): e64c495

feat: 24h multi-part videos, slider->TikTok, seek buttons, grid->TikTok

Browse files
Files changed (1) hide show
  1. app.py +96 -18
app.py CHANGED
@@ -265,19 +265,35 @@ def scrape_24h_article(url):
265
  except Exception as e:
266
  return {"title":"⚠️ Lỗi","time":"","summary":str(e),"body":[],"related":[],"source_url":url,"source":"24h","og_image":""}
267
 
268
- def _extract_24h_video_url(article_url):
269
- """Quick extract just the m3u8 URL + poster from a 24h article (for TikTok feed)."""
 
270
  try:
271
  r = requests.get(article_url, headers=HEADERS, timeout=10); r.encoding="utf-8"
272
  m3u8s = re.findall(r'(https?://cdn\.24h\.com\.vn/[^\s"\'\\]+\.m3u8)', r.text)
273
- videos = [u for u in m3u8s if '_720p' not in u]
274
- if not videos: videos = m3u8s
275
- if not videos: return None
276
  soup = BeautifulSoup(r.text, "lxml")
277
  og = soup.find("meta", property="og:image")
278
  poster = og.get("content","") if og else ""
279
- return {"src": videos[0], "poster": poster, "vtype": "hls"}
280
- except: return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
  # ── VNE Scrapers ───────────────────────────────────────────────────────────────
283
  def scrape_vne_list(url):
@@ -436,10 +452,14 @@ def fetch_tiktok_feed_videos():
436
  "vtype": "mp4", "source": "bdp", "link": art["link"]}
437
 
438
  def _fetch_24h_vid(art):
439
- data = _extract_24h_video_url(art["link"])
440
- if not data: return None
441
- return {"title": art["title"], "src": data["src"], "poster": data["poster"],
442
- "vtype": data["vtype"], "source": "24h", "link": art["link"]}
 
 
 
 
443
 
444
  # Get video lists
445
  bdp_list = scrape_bdp_video_list()[:12]
@@ -457,7 +477,9 @@ def fetch_tiktok_feed_videos():
457
  for f in as_completed(h24_futures):
458
  try:
459
  r = f.result()
460
- if r: h24_results.append(r)
 
 
461
  except: pass
462
 
463
  # Interleave BDP and 24h
@@ -512,7 +534,7 @@ def render_video_slider_html(videos):
512
  title = v.get("title", "")
513
  aid = make_id(link)
514
  sl = slug(title) if title else f"video-{i}"
515
- click_js = f"window.bdpOpen('{esc(link)}','{aid}','{sl}')"
516
  src_badge = ""
517
  if v.get("source") == "24h":
518
  src_badge = '<span class="vslide-badge vslide-badge-24h">24h</span>'
@@ -575,9 +597,13 @@ def render_video_page_html():
575
  else:
576
  video_tag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} muted loop><source src="{safe_url(vsrc)}" type="video/mp4"></video>'
577
 
578
- slides.append(f'''<div class="tiktok-slide" data-index="{vi}">
579
  {video_tag}
580
  <div class="tiktok-pause-icon">▶</div>
 
 
 
 
581
  <div class="tiktok-bottom">
582
  <span class="bdp-badge {badge_cls}">{src_label}</span>
583
  <p class="tiktok-title">{v.get("title","")}</p>
@@ -603,11 +629,11 @@ def render_video_page_html():
603
  grid_parts.append(f'<div class="bdp-topbar"><span>⏱ {now}</span><span>📰 {len(articles)} video · BDP + 24h</span></div>')
604
  if feat:
605
  grid_parts.append('<div class="bdp-grid bdp-grid-featured">')
606
- for a in feat[:6]: grid_parts.append(_list_card(a,True))
607
  grid_parts.append('</div>')
608
  if reg:
609
  grid_parts.append('<div class="bdp-grid">')
610
- for a in reg[:40]: grid_parts.append(_list_card(a,False))
611
  grid_parts.append('</div>')
612
  grid_parts.append('</div>')
613
  grid_html = '\n'.join(grid_parts)
@@ -631,7 +657,7 @@ def render_list_html(articles,group_name=""):
631
  parts.append('</div>')
632
  return '\n'.join(parts)
633
 
634
- def _list_card(art,big):
635
  img_html=""
636
  if art.get("img"):
637
  c="bdp-card-img bdp-card-img-big" if big else "bdp-card-img"
@@ -648,7 +674,10 @@ def _list_card(art,big):
648
  elif art.get("source")=="bdp": badge=f'<span class="bdp-badge bdp-badge-bdp">{grp or "BongDaPlus"}</span>'
649
  sl=slug(art["title"])
650
  share_js=f"event.stopPropagation();window.bdpShareHash('{esc(art['title'])}','{sl}','{aid}')"
651
- click_js=f"window.bdpOpen('{esc(link)}','{aid}','{sl}')"
 
 
 
652
  return f"""<div class="bdp-card" onclick="{click_js}">
653
  {img_html}<div class="bdp-card-body">{badge}<h3 class="{tc}">{art['title']}</h3>
654
  {summ_html}<div class="bdp-card-footer">{time_html}
@@ -826,6 +855,10 @@ footer,.built-with{display:none!important}
826
  .tiktok-unmute-hint{position:absolute;top:12px;right:12px;background:rgba(0,0,0,.6);color:#fff;font-size:12px;padding:6px 12px;border-radius:18px;cursor:pointer;z-index:4;backdrop-filter:blur(4px);transition:opacity .3s}
827
  .tiktok-pause-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:70px;height:70px;background:rgba(0,0,0,.5);border-radius:50%;color:#fff;font-size:28px;z-index:5;pointer-events:none;display:none;align-items:center;justify-content:center;line-height:70px;text-align:center}
828
  .tiktok-slide.paused .tiktok-pause-icon{display:block}
 
 
 
 
829
  .vslide-badge{position:absolute;top:6px;left:6px;font-size:9px;padding:1px 6px;border-radius:3px;font-weight:700;z-index:2}
830
  .vslide-badge-24h{background:#e67e22;color:#fff}
831
  """
@@ -888,6 +921,51 @@ function initHlsVideo(v){
888
  }
889
 
890
  /* ══ TikTok Fullscreen Feed ══ */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
891
  window.bdpTikTokUnmute=function(hint){
892
  var feed=hint.closest('.tiktok-fullscreen-feed')||hint.closest('.tiktok-feed');
893
  var slide=hint.closest('.tiktok-slide');
 
265
  except Exception as e:
266
  return {"title":"⚠️ Lỗi","time":"","summary":str(e),"body":[],"related":[],"source_url":url,"source":"24h","og_image":""}
267
 
268
+ def _extract_24h_video_urls(article_url):
269
+ """Extract ALL m3u8 URLs (multiple halves) + poster from a 24h article.
270
+ Returns list of {src, poster, vtype} or empty list."""
271
  try:
272
  r = requests.get(article_url, headers=HEADERS, timeout=10); r.encoding="utf-8"
273
  m3u8s = re.findall(r'(https?://cdn\.24h\.com\.vn/[^\s"\'\\]+\.m3u8)', r.text)
274
+ full = list(dict.fromkeys(u for u in m3u8s if '_720p' not in u))
275
+ if not full: return []
 
276
  soup = BeautifulSoup(r.text, "lxml")
277
  og = soup.find("meta", property="og:image")
278
  poster = og.get("content","") if og else ""
279
+ results = [{"src": full[0], "poster": poster, "vtype": "hls"}]
280
+ # Probe for numbered parts: {base}01.m3u8 → {base}02.m3u8, 03, etc.
281
+ base_match = re.match(r'(.+?)(\d{2})(\.m3u8)$', full[0])
282
+ if base_match:
283
+ base, ext = base_match.group(1), base_match.group(3)
284
+ for i in range(2, 10):
285
+ part_url = f"{base}{i:02d}{ext}"
286
+ if part_url in full:
287
+ results.append({"src": part_url, "poster": poster, "vtype": "hls"})
288
+ continue
289
+ try:
290
+ tr = requests.head(part_url, headers=HEADERS, timeout=3)
291
+ if tr.status_code == 200:
292
+ results.append({"src": part_url, "poster": poster, "vtype": "hls"})
293
+ else: break
294
+ except: break
295
+ return results
296
+ except: return []
297
 
298
  # ── VNE Scrapers ───────────────────────────────────────────────────────────────
299
  def scrape_vne_list(url):
 
452
  "vtype": "mp4", "source": "bdp", "link": art["link"]}
453
 
454
  def _fetch_24h_vid(art):
455
+ vids = _extract_24h_video_urls(art["link"])
456
+ if not vids: return []
457
+ results = []
458
+ for pi, v in enumerate(vids):
459
+ label = f" (P{pi+1})" if len(vids)>1 else ""
460
+ results.append({"title": art["title"]+label, "src": v["src"], "poster": v["poster"],
461
+ "vtype": v["vtype"], "source": "24h", "link": art["link"]})
462
+ return results
463
 
464
  # Get video lists
465
  bdp_list = scrape_bdp_video_list()[:12]
 
477
  for f in as_completed(h24_futures):
478
  try:
479
  r = f.result()
480
+ if r:
481
+ if isinstance(r, list): h24_results.extend(r)
482
+ else: h24_results.append(r)
483
  except: pass
484
 
485
  # Interleave BDP and 24h
 
534
  title = v.get("title", "")
535
  aid = make_id(link)
536
  sl = slug(title) if title else f"video-{i}"
537
+ click_js = f"window.bdpOpenTikTok('{esc(link)}','{aid}')"
538
  src_badge = ""
539
  if v.get("source") == "24h":
540
  src_badge = '<span class="vslide-badge vslide-badge-24h">24h</span>'
 
597
  else:
598
  video_tag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} muted loop><source src="{safe_url(vsrc)}" type="video/mp4"></video>'
599
 
600
+ slides.append(f'''<div class="tiktok-slide" data-index="{vi}" data-aid="{aid}">
601
  {video_tag}
602
  <div class="tiktok-pause-icon">▶</div>
603
+ <div class="tiktok-seek-controls">
604
+ <button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,-10)">⏪ 10s</button>
605
+ <button class="tiktok-seek-btn" onclick="event.stopPropagation();window.bdpSeek(this,10)">10s ⏩</button>
606
+ </div>
607
  <div class="tiktok-bottom">
608
  <span class="bdp-badge {badge_cls}">{src_label}</span>
609
  <p class="tiktok-title">{v.get("title","")}</p>
 
629
  grid_parts.append(f'<div class="bdp-topbar"><span>⏱ {now}</span><span>📰 {len(articles)} video · BDP + 24h</span></div>')
630
  if feat:
631
  grid_parts.append('<div class="bdp-grid bdp-grid-featured">')
632
+ for a in feat[:6]: grid_parts.append(_list_card(a,True,tiktok=True))
633
  grid_parts.append('</div>')
634
  if reg:
635
  grid_parts.append('<div class="bdp-grid">')
636
+ for a in reg[:40]: grid_parts.append(_list_card(a,False,tiktok=True))
637
  grid_parts.append('</div>')
638
  grid_parts.append('</div>')
639
  grid_html = '\n'.join(grid_parts)
 
657
  parts.append('</div>')
658
  return '\n'.join(parts)
659
 
660
+ def _list_card(art,big,tiktok=False):
661
  img_html=""
662
  if art.get("img"):
663
  c="bdp-card-img bdp-card-img-big" if big else "bdp-card-img"
 
674
  elif art.get("source")=="bdp": badge=f'<span class="bdp-badge bdp-badge-bdp">{grp or "BongDaPlus"}</span>'
675
  sl=slug(art["title"])
676
  share_js=f"event.stopPropagation();window.bdpShareHash('{esc(art['title'])}','{sl}','{aid}')"
677
+ if tiktok:
678
+ click_js=f"window.bdpOpenTikTok('{esc(link)}','{aid}')"
679
+ else:
680
+ click_js=f"window.bdpOpen('{esc(link)}','{aid}','{sl}')"
681
  return f"""<div class="bdp-card" onclick="{click_js}">
682
  {img_html}<div class="bdp-card-body">{badge}<h3 class="{tc}">{art['title']}</h3>
683
  {summ_html}<div class="bdp-card-footer">{time_html}
 
855
  .tiktok-unmute-hint{position:absolute;top:12px;right:12px;background:rgba(0,0,0,.6);color:#fff;font-size:12px;padding:6px 12px;border-radius:18px;cursor:pointer;z-index:4;backdrop-filter:blur(4px);transition:opacity .3s}
856
  .tiktok-pause-icon{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:70px;height:70px;background:rgba(0,0,0,.5);border-radius:50%;color:#fff;font-size:28px;z-index:5;pointer-events:none;display:none;align-items:center;justify-content:center;line-height:70px;text-align:center}
857
  .tiktok-slide.paused .tiktok-pause-icon{display:block}
858
+ .tiktok-seek-controls{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;gap:40px;z-index:6;pointer-events:auto}
859
+ .tiktok-seek-btn{background:rgba(0,0,0,.4);color:#fff;border:none;padding:8px 14px;border-radius:20px;font-size:12px;cursor:pointer;backdrop-filter:blur(4px);font-weight:600;opacity:.7;transition:opacity .2s}
860
+ .tiktok-seek-btn:hover{opacity:1}
861
+ .tiktok-seek-btn:active{transform:scale(.9)}
862
  .vslide-badge{position:absolute;top:6px;left:6px;font-size:9px;padding:1px 6px;border-radius:3px;font-weight:700;z-index:2}
863
  .vslide-badge-24h{background:#e67e22;color:#fff}
864
  """
 
921
  }
922
 
923
  /* ══ TikTok Fullscreen Feed ══ */
924
+ window.bdpSeek=function(btn,sec){
925
+ var slide=btn.closest('.tiktok-slide');
926
+ if(!slide) return;
927
+ var v=slide.querySelector('.tiktok-video');
928
+ if(!v) return;
929
+ v.currentTime=Math.max(0,Math.min(v.duration||9999,v.currentTime+sec));
930
+ };
931
+
932
+ window.bdpOpenTikTok=function(url,aid){
933
+ /* Switch to Video tab and scroll TikTok feed to matching video */
934
+ try{localStorage.setItem('bdp_url_'+aid,url);}catch(e){}
935
+ /* Find the dropdown and switch to Video Tổng Hợp */
936
+ var dd=document.querySelector('.controls-row select, .controls-row input[role="listbox"]');
937
+ if(!dd){
938
+ /* Gradio dropdown: find the actual element */
939
+ var labels=document.querySelectorAll('.controls-row .wrap');
940
+ labels.forEach(function(l){
941
+ var inp=l.querySelector('input');
942
+ if(inp){
943
+ inp.value='🎬 Video Tổng Hợp';
944
+ inp.dispatchEvent(new Event('input',{bubbles:true}));
945
+ /* Also try clicking the option */
946
+ setTimeout(function(){
947
+ var opts=document.querySelectorAll('[role="option"]');
948
+ opts.forEach(function(o){if(o.textContent.indexOf('Video')>-1)o.click();});
949
+ },200);
950
+ }
951
+ });
952
+ }
953
+ /* After switching, scroll to the matching video in TikTok feed */
954
+ setTimeout(function(){
955
+ var feed=document.querySelector('.tiktok-fullscreen-feed');
956
+ if(!feed) return;
957
+ /* Find slide with matching link */
958
+ var slides=feed.querySelectorAll('.tiktok-slide');
959
+ var targetIdx=-1;
960
+ slides.forEach(function(sl,i){
961
+ if(sl.getAttribute('data-aid')===aid) targetIdx=i;
962
+ });
963
+ if(targetIdx>=0 && slides[targetIdx]){
964
+ slides[targetIdx].scrollIntoView({behavior:'smooth'});
965
+ }
966
+ window.scrollTo({top:0,behavior:'smooth'});
967
+ },1500);
968
+ };
969
  window.bdpTikTokUnmute=function(hint){
970
  var feed=hint.closest('.tiktok-fullscreen-feed')||hint.closest('.tiktok-feed');
971
  var slide=hint.closest('.tiktok-slide');