Spaces:
Running
Running
feat: app.py as patcher entry point, patches and runs main app with 24h shorts
Browse files
app.py
CHANGED
|
@@ -1,502 +1,57 @@
|
|
| 1 |
-
|
| 2 |
-
import
|
| 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 |
-
("⚽","Bóng Đá QT","bong-da-qt"),
|
| 56 |
-
("⚽","Ngoại Hạng Anh","ngoai-hang-anh"),
|
| 57 |
-
("⚽","La Liga","la-liga"),
|
| 58 |
-
("🏆","Champions League","champions-league"),
|
| 59 |
-
("⚽","Bóng Đá VN","bong-da-vn"),
|
| 60 |
-
("🔄","Chuyển Nhượng","chuyen-nhuong"),
|
| 61 |
-
]
|
| 62 |
-
CAT_KEYS = list(CATEGORIES.keys())
|
| 63 |
-
CAT_HASH_TO_KEY = {ci[2]: CAT_KEYS[i] for i, ci in enumerate(CAT_ICONS)}
|
| 64 |
-
HOMEPAGE_SOURCES = [
|
| 65 |
-
("vne","https://vnexpress.net/thoi-su","Thời Sự"),
|
| 66 |
-
("vne","https://vnexpress.net/the-gioi","Thế Giới"),
|
| 67 |
-
("vne","https://vnexpress.net/kinh-doanh","Kinh Doanh"),
|
| 68 |
-
("vne","https://vnexpress.net/so-hoa","Công Nghệ"),
|
| 69 |
-
("vne","https://vnexpress.net/the-thao","Thể Thao"),
|
| 70 |
-
("vne","https://vnexpress.net/giai-tri","Giải Trí"),
|
| 71 |
-
("bdp","https://bongdaplus.vn/tin-moi","Bóng Đá"),
|
| 72 |
-
]
|
| 73 |
-
|
| 74 |
-
# ── Helpers ────────────────────────────────────────────────────────────────────
|
| 75 |
-
def strip_links(html):
|
| 76 |
-
return re.sub(r'</a>', '', re.sub(r'<a\s[^>]*>', '', html))
|
| 77 |
-
|
| 78 |
-
def esc(text):
|
| 79 |
-
return text.replace("\\","\\\\").replace("'","\\'").replace('"','\\"').replace("\n"," ")
|
| 80 |
-
|
| 81 |
-
def safe_url(url):
|
| 82 |
-
if not url: return ""
|
| 83 |
-
try:
|
| 84 |
-
return urlquote(url, safe=':/?#[]@!$&()*+,;=-._~%')
|
| 85 |
-
except Exception:
|
| 86 |
-
return url.replace(" ","%20").replace("'","%27").replace('"',"%22")
|
| 87 |
-
|
| 88 |
-
def make_id(url):
|
| 89 |
-
return hashlib.md5(url.encode()).hexdigest()[:12]
|
| 90 |
-
|
| 91 |
-
def slug(text):
|
| 92 |
-
s = text.lower().strip()
|
| 93 |
-
for p,r in [('[àáạảãâầấậẩẫăằắặẳẵ]','a'),('[èéẹẻẽêềếệểễ]','e'),('[ìíịỉĩ]','i'),
|
| 94 |
-
('[òóọỏõôồốộổỗơờớợởỡ]','o'),('[ùúụủũưừứựửữ]','u'),('[ỳýỵỷỹ]','y'),('[đ]','d')]:
|
| 95 |
-
s = re.sub(p,r,s)
|
| 96 |
-
return re.sub(r'[\s-]+','-',re.sub(r'[^a-z0-9\s-]','',s)).strip('-')[:60]
|
| 97 |
-
|
| 98 |
-
def _extract_bdp_video_id(url):
|
| 99 |
-
m = re.search(r'-(\d{6,})\.html', url)
|
| 100 |
-
return m.group(1) if m else None
|
| 101 |
-
|
| 102 |
-
# ══════════════════════════════════════════════════════════��═══════════════════
|
| 103 |
-
# SCRAPERS
|
| 104 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 105 |
-
def _get(url):
|
| 106 |
-
r = requests.get(url, headers=HEADERS, timeout=15); r.encoding="utf-8"
|
| 107 |
-
return BeautifulSoup(r.text, "lxml")
|
| 108 |
-
|
| 109 |
-
def _get_raw(url):
|
| 110 |
-
r = requests.get(url, headers=HEADERS, timeout=15); r.encoding="utf-8"
|
| 111 |
-
return r.text, BeautifulSoup(r.text, "lxml")
|
| 112 |
-
|
| 113 |
-
# ── BDP Video Embed Fetcher ────────────────────────────────────────────────────
|
| 114 |
-
def fetch_bdp_embed_data(video_id):
|
| 115 |
-
try:
|
| 116 |
-
embed_url = f"{BASE_BDP}/video-embed/{video_id}.html"
|
| 117 |
-
html, soup = _get_raw(embed_url)
|
| 118 |
-
video = soup.select_one("video#videoPlayer")
|
| 119 |
-
if not video: return None
|
| 120 |
-
source = video.find("source")
|
| 121 |
-
result = {"mp4": source.get("src","") if source else "", "poster": video.get("poster","")}
|
| 122 |
-
carousel_match = re.findall(r'vdo\.relatedCarousel\((\[.*?\])\)', html, re.DOTALL)
|
| 123 |
-
if carousel_match:
|
| 124 |
-
posters = re.findall(r"poster:\s*'([^']+)'", carousel_match[0])
|
| 125 |
-
vdorefs = re.findall(r"vdoref:\s*'([^']+)'", carousel_match[0])
|
| 126 |
-
titles = re.findall(r"title:\s*'([^']*)'", carousel_match[0])
|
| 127 |
-
result["related"] = [
|
| 128 |
-
{"poster": p, "link": BASE_BDP + v if not v.startswith("http") else v, "title": t}
|
| 129 |
-
for p, v, t in zip(posters, vdorefs, titles)
|
| 130 |
-
]
|
| 131 |
-
return result
|
| 132 |
-
except: return None
|
| 133 |
-
|
| 134 |
-
# ── BDP Scrapers ───────────────────────────────────────────────────────────────
|
| 135 |
-
def scrape_bdp_list(url):
|
| 136 |
-
try:
|
| 137 |
-
soup = _get(url); articles,seen=[],set()
|
| 138 |
-
for sel,feat in [("div.news.fst",True),("div.sld-itm.news",True),("li.news",False)]:
|
| 139 |
-
for it in soup.select(sel):
|
| 140 |
-
tag = it.find("a",class_="title") or it.find("a",href=True)
|
| 141 |
-
if not tag: continue
|
| 142 |
-
t=tag.get_text(strip=True); lk=tag.get("href","")
|
| 143 |
-
if not t or len(t)<5: continue
|
| 144 |
-
if lk and not lk.startswith("http"): lk=BASE_BDP+lk
|
| 145 |
-
if lk in seen: continue
|
| 146 |
-
im=it.find("img"); img=(im.get("data-src") or im.get("src")) if im else None
|
| 147 |
-
sm=it.find("p",class_="summ"); tt=it.find("div",class_="in-time")
|
| 148 |
-
is_video = "/video/" in lk
|
| 149 |
-
articles.append({"title":t,"link":lk,"img":img,"summary":sm.get_text(strip=True) if sm else "",
|
| 150 |
-
"time":tt.get_text(strip=True) if tt else "","featured":feat,"source":"bdp","group":"","is_video":is_video})
|
| 151 |
-
seen.add(lk)
|
| 152 |
-
return articles
|
| 153 |
-
except Exception as e:
|
| 154 |
-
return [{"title":f"⚠️ {e}","link":"#","img":None,"summary":"","time":"","featured":False,"source":"bdp","group":"","is_video":False}]
|
| 155 |
-
|
| 156 |
-
def scrape_bdp_video_list():
|
| 157 |
-
try:
|
| 158 |
-
soup = _get(f"{BASE_BDP}/video")
|
| 159 |
-
articles, seen = [], set()
|
| 160 |
-
for a in soup.find_all("a", href=True):
|
| 161 |
-
href = a.get("href", "")
|
| 162 |
-
if "/video/" not in href: continue
|
| 163 |
-
if href in ("/video/", "/video/ban-thang-dep", "/video/highlight"): continue
|
| 164 |
-
if not href.startswith("http"): href = BASE_BDP + href
|
| 165 |
-
if href in seen: continue
|
| 166 |
-
title = a.get_text(strip=True)
|
| 167 |
-
title = re.sub(r'^\d{2}:\d{2}', '', title).strip()
|
| 168 |
-
if not title or len(title) < 5: continue
|
| 169 |
-
img_tag = a.find("img")
|
| 170 |
-
if not img_tag:
|
| 171 |
-
parent = a.parent
|
| 172 |
-
if parent: img_tag = parent.find("img")
|
| 173 |
-
img = None
|
| 174 |
-
if img_tag: img = img_tag.get("data-src") or img_tag.get("src") or img_tag.get("data-original")
|
| 175 |
-
articles.append({"title": title, "link": href, "img": img, "summary": "", "time": "",
|
| 176 |
-
"featured": len(articles) < 3, "source": "bdp", "group": "BongDaPlus Video", "is_video": True})
|
| 177 |
-
seen.add(href)
|
| 178 |
-
return articles[:30]
|
| 179 |
-
except Exception as e:
|
| 180 |
-
return [{"title": f"⚠️ {e}", "link": "#", "img": None, "summary": "", "time": "",
|
| 181 |
-
"featured": False, "source": "bdp", "group": "", "is_video": False}]
|
| 182 |
-
|
| 183 |
-
def scrape_bdp_article(url):
|
| 184 |
-
try:
|
| 185 |
-
soup = _get(url)
|
| 186 |
-
h1=soup.select_one(".lead-title h1") or soup.select_one("h1")
|
| 187 |
-
te=soup.select_one(".emobar .rgt"); se=soup.select_one("div.summary")
|
| 188 |
-
og=soup.find("meta",property="og:image"); og_img=og.get("content","") if og else ""
|
| 189 |
-
cd=soup.select_one("div.content#postContent") or soup.select_one("div.content")
|
| 190 |
-
body=_extract_body(cd) if cd else []
|
| 191 |
-
is_video_page = "/video/" in url
|
| 192 |
-
if is_video_page:
|
| 193 |
-
vid_id = _extract_bdp_video_id(url)
|
| 194 |
-
if vid_id:
|
| 195 |
-
embed = fetch_bdp_embed_data(vid_id)
|
| 196 |
-
if embed and embed.get("mp4"):
|
| 197 |
-
body.insert(0, {"type": "video", "src": embed["mp4"], "poster": embed.get("poster", ""), "vtype": "mp4"})
|
| 198 |
-
if not og_img or "logo" in og_img.lower(): og_img = embed.get("poster", og_img)
|
| 199 |
-
else:
|
| 200 |
-
for iframe in soup.select("iframe"):
|
| 201 |
-
src = iframe.get("src", "")
|
| 202 |
-
if "video-embed" in src:
|
| 203 |
-
vid = _fetch_bdp_video(src)
|
| 204 |
-
if vid: body.insert(0, vid)
|
| 205 |
-
return {"title":h1.get_text(strip=True) if h1 else "","time":te.get_text(strip=True) if te else "",
|
| 206 |
-
"summary":se.get_text(strip=True) if se else "","body":body,"related":_bdp_relates(soup),
|
| 207 |
-
"source_url":url,"source":"bdp","og_image":og_img}
|
| 208 |
-
except Exception as e:
|
| 209 |
-
return {"title":"⚠️ Lỗi","time":"","summary":str(e),"body":[],"related":[],"source_url":url,"source":"bdp","og_image":""}
|
| 210 |
-
|
| 211 |
-
def _fetch_bdp_video(embed_src):
|
| 212 |
-
try:
|
| 213 |
-
full = embed_src if embed_src.startswith("http") else BASE_BDP+embed_src
|
| 214 |
-
html, soup = _get_raw(full)
|
| 215 |
-
video = soup.select_one("video#videoPlayer") or soup.select_one("video")
|
| 216 |
-
if video:
|
| 217 |
-
source = video.find("source")
|
| 218 |
-
if source:
|
| 219 |
-
return {"type":"video","src":source.get("src",""),"poster":video.get("poster",""),"vtype":"mp4"}
|
| 220 |
-
except: pass
|
| 221 |
-
return None
|
| 222 |
-
|
| 223 |
-
# ── 24h.com.vn Scrapers ───────────────────────────────────────────────────────
|
| 224 |
-
def scrape_24h_video_list():
|
| 225 |
-
"""Scrape 24h.com.vn video-highlight page using <article> tags."""
|
| 226 |
-
try:
|
| 227 |
-
url = f"{BASE_24H}/video-highlight-c953.html"
|
| 228 |
-
r = requests.get(url, headers=HEADERS, timeout=15); r.encoding="utf-8"
|
| 229 |
-
soup = BeautifulSoup(r.text, "lxml")
|
| 230 |
-
articles, seen = [], set()
|
| 231 |
-
for art in soup.find_all("article"):
|
| 232 |
-
a = art.find("a", href=True)
|
| 233 |
-
if not a: continue
|
| 234 |
-
href = a.get("href","")
|
| 235 |
-
if not href.startswith("http"): href = BASE_24H + href
|
| 236 |
-
if href in seen: continue
|
| 237 |
-
img_tag = art.find("img")
|
| 238 |
-
title = ""
|
| 239 |
-
if img_tag: title = img_tag.get("alt","")
|
| 240 |
-
if not title: title = a.get("title","") or a.get_text(strip=True)
|
| 241 |
-
if not title or len(title) < 10: continue
|
| 242 |
-
img_src = None
|
| 243 |
-
if img_tag:
|
| 244 |
-
for attr in ["data-original","data-src","src"]:
|
| 245 |
-
v = img_tag.get(attr,"")
|
| 246 |
-
if v and "base64" not in v and len(v) > 20:
|
| 247 |
-
img_src = v; break
|
| 248 |
-
seen.add(href)
|
| 249 |
-
articles.append({"title": title, "link": href, "img": img_src, "summary": "", "time": "",
|
| 250 |
-
"featured": len(articles) < 3, "source": "24h", "group": "24h Video", "is_video": True})
|
| 251 |
-
return articles[:30]
|
| 252 |
-
except:
|
| 253 |
-
return []
|
| 254 |
-
|
| 255 |
-
def scrape_24h_article(url):
|
| 256 |
-
"""Scrape a 24h.com.vn article - extract m3u8 video URL."""
|
| 257 |
-
try:
|
| 258 |
-
r = requests.get(url, headers=HEADERS, timeout=15); r.encoding="utf-8"
|
| 259 |
-
soup = BeautifulSoup(r.text, "lxml")
|
| 260 |
-
h1 = soup.select_one("h1")
|
| 261 |
-
title = h1.get_text(strip=True) if h1 else ""
|
| 262 |
-
og = soup.find("meta", property="og:image")
|
| 263 |
-
og_img = og.get("content","") if og else ""
|
| 264 |
-
desc_meta = soup.find("meta", property="og:description")
|
| 265 |
-
summary = desc_meta.get("content","") if desc_meta else ""
|
| 266 |
-
# Extract m3u8 video URLs
|
| 267 |
-
m3u8s = re.findall(r'(https?://cdn\.24h\.com\.vn/[^\s"\'\\]+\.m3u8)', r.text)
|
| 268 |
-
videos = [u for u in m3u8s if '_720p' not in u]
|
| 269 |
-
if not videos: videos = m3u8s
|
| 270 |
-
body = []
|
| 271 |
-
for vsrc in videos[:3]:
|
| 272 |
-
body.append({"type": "video", "src": vsrc, "poster": og_img, "vtype": "hls"})
|
| 273 |
-
# Extract text content
|
| 274 |
-
content_selectors = ["div.the-article-body", "div.nws-mainContent", "div.nwsCt",
|
| 275 |
-
"div#ctl00_mainContent_ctl00_divNewsContent", "div.detail-content"]
|
| 276 |
-
cd = None
|
| 277 |
-
for sel in content_selectors:
|
| 278 |
-
cd = soup.select_one(sel)
|
| 279 |
-
if cd: break
|
| 280 |
-
if cd:
|
| 281 |
-
for ch in cd.children:
|
| 282 |
-
if not hasattr(ch,'name') or not ch.name: continue
|
| 283 |
-
if ch.name == "p":
|
| 284 |
-
t = ch.get_text(strip=True)
|
| 285 |
-
if t: body.append({"type":"p","html":strip_links(''.join(str(c) for c in ch.children))})
|
| 286 |
-
elif ch.name in ("h2","h3","h4"):
|
| 287 |
-
body.append({"type":"heading","text":ch.get_text(strip=True)})
|
| 288 |
-
return {"title": title, "time": "", "summary": summary[:200], "body": body,
|
| 289 |
-
"related": [], "source_url": url, "source": "24h", "og_image": og_img}
|
| 290 |
-
except Exception as e:
|
| 291 |
-
return {"title":"⚠️ Lỗi","time":"","summary":str(e),"body":[],"related":[],"source_url":url,"source":"24h","og_image":""}
|
| 292 |
-
|
| 293 |
-
def _extract_24h_video_urls(article_url):
|
| 294 |
-
"""Extract ALL m3u8 URLs (multiple halves/parts) + poster from a 24h article.
|
| 295 |
-
Pattern: ...name1.m3u8 → ...name2.m3u8, ...name3.m3u8, etc.
|
| 296 |
-
Returns list of {src, poster, vtype} or empty list."""
|
| 297 |
-
try:
|
| 298 |
-
r = requests.get(article_url, headers=HEADERS, timeout=10); r.encoding="utf-8"
|
| 299 |
-
m3u8s = re.findall(r'(https?://cdn\.24h\.com\.vn/[^\s"\'\\<>]+\.m3u8)', r.text)
|
| 300 |
-
masters = list(dict.fromkeys(u for u in m3u8s if '_720p' not in u))
|
| 301 |
-
if not masters: return []
|
| 302 |
-
soup = BeautifulSoup(r.text, "lxml")
|
| 303 |
-
og = soup.find("meta", property="og:image")
|
| 304 |
-
poster = og.get("content","") if og else ""
|
| 305 |
-
results = [{"src": masters[0], "poster": poster, "vtype": "hls"}]
|
| 306 |
-
# Probe numbered parts: ...name1.m3u8 → ...name2.m3u8, etc.
|
| 307 |
-
base_match = re.match(r'(.+?)(\d+)(\.m3u8)$', masters[0])
|
| 308 |
-
if base_match:
|
| 309 |
-
base, start_num, ext = base_match.group(1), int(base_match.group(2)), base_match.group(3)
|
| 310 |
-
for i in range(start_num + 1, start_num + 10):
|
| 311 |
-
part_url = f"{base}{i}{ext}"
|
| 312 |
-
if part_url in masters:
|
| 313 |
-
results.append({"src": part_url, "poster": poster, "vtype": "hls"})
|
| 314 |
-
continue
|
| 315 |
-
try:
|
| 316 |
-
tr = requests.head(part_url, headers=HEADERS, timeout=3, allow_redirects=True)
|
| 317 |
-
if tr.status_code == 200:
|
| 318 |
-
results.append({"src": part_url, "poster": poster, "vtype": "hls"})
|
| 319 |
-
else: break
|
| 320 |
-
except: break
|
| 321 |
-
return results
|
| 322 |
-
except: return []
|
| 323 |
-
|
| 324 |
-
# ── VNE Scrapers ───────────────────────────────────────────────────────────────
|
| 325 |
-
def scrape_vne_list(url):
|
| 326 |
-
try:
|
| 327 |
-
soup=_get(url); articles,seen=[],set()
|
| 328 |
-
for i,it in enumerate(soup.select("article.item-news")):
|
| 329 |
-
a=it.select_one("h2.title-news a") or it.select_one("h3.title-news a") or it.find("a",href=True,title=True)
|
| 330 |
-
if not a: continue
|
| 331 |
-
t=a.get("title","") or a.get_text(strip=True); lk=a.get("href","")
|
| 332 |
-
if not t or len(t)<5 or lk in seen: continue
|
| 333 |
-
im=it.find("img"); img=None
|
| 334 |
-
if im:
|
| 335 |
-
img=im.get("data-src") or im.get("src")
|
| 336 |
-
if img and 'blank' in img:
|
| 337 |
-
src=it.find("source")
|
| 338 |
-
if src: img=src.get("srcset","").split(",")[0].strip().split(" ")[0]
|
| 339 |
-
desc=it.select_one("p.description")
|
| 340 |
-
articles.append({"title":t,"link":lk,"img":img,"summary":desc.get_text(strip=True)[:150] if desc else "",
|
| 341 |
-
"time":"","featured":i==0,"source":"vne","group":"","is_video":False})
|
| 342 |
-
seen.add(lk)
|
| 343 |
-
return articles
|
| 344 |
-
except Exception as e:
|
| 345 |
-
return [{"title":f"⚠️ {e}","link":"#","img":None,"summary":"","time":"","featured":False,"source":"vne","group":"","is_video":False}]
|
| 346 |
-
|
| 347 |
-
def scrape_vne_article(url):
|
| 348 |
-
try:
|
| 349 |
-
soup=_get(url)
|
| 350 |
-
h1=soup.select_one("h1.title-detail"); desc=soup.select_one("p.description"); dt=soup.select_one("span.date")
|
| 351 |
-
og=soup.find("meta",property="og:image"); og_img=og.get("content","") if og else ""
|
| 352 |
-
cd=soup.select_one("article.fck_detail"); body=[]
|
| 353 |
-
if cd:
|
| 354 |
-
for ch in cd.children:
|
| 355 |
-
if not hasattr(ch,'name') or not ch.name: continue
|
| 356 |
-
if ch.name=="p":
|
| 357 |
-
vid=ch.find("video")
|
| 358 |
-
if vid:
|
| 359 |
-
vsrc=vid.get("src","")
|
| 360 |
-
if vsrc:
|
| 361 |
-
vtype="hls" if ("m3u8" in vsrc or "mpegURL" in (vid.get("type","") or "")) else "mp4"
|
| 362 |
-
body.append({"type":"video","src":vsrc,"poster":vid.get("poster",""),"vtype":vtype})
|
| 363 |
-
continue
|
| 364 |
-
im=ch.find("img")
|
| 365 |
-
if im:
|
| 366 |
-
s=im.get("data-src") or im.get("src")
|
| 367 |
-
if s: body.append({"type":"img","src":s,"alt":im.get("alt","")})
|
| 368 |
-
t=ch.get_text(strip=True)
|
| 369 |
-
if t: body.append({"type":"p","html":strip_links(''.join(str(c) for c in ch.children))})
|
| 370 |
-
elif ch.name=="figure":
|
| 371 |
-
vid=ch.find("video")
|
| 372 |
-
if vid:
|
| 373 |
-
vsrc=vid.get("src","")
|
| 374 |
-
cap=ch.find("figcaption")
|
| 375 |
-
if vsrc:
|
| 376 |
-
vtype="hls" if ("m3u8" in vsrc or "mpegURL" in (vid.get("type","") or "")) else "mp4"
|
| 377 |
-
body.append({"type":"video","src":vsrc,"poster":vid.get("poster",""),"vtype":vtype,
|
| 378 |
-
"caption":cap.get_text(strip=True) if cap else ""})
|
| 379 |
-
continue
|
| 380 |
-
im=ch.find("img"); cap=ch.find("figcaption")
|
| 381 |
-
if im:
|
| 382 |
-
s=im.get("data-src") or im.get("src")
|
| 383 |
-
if s: body.append({"type":"img","src":s,"alt":cap.get_text(strip=True) if cap else ""})
|
| 384 |
-
elif ch.name in ("h2","h3","h4"):
|
| 385 |
-
body.append({"type":"heading","text":ch.get_text(strip=True)})
|
| 386 |
-
return {"title":h1.get_text(strip=True) if h1 else "","time":dt.get_text(strip=True) if dt else "",
|
| 387 |
-
"summary":desc.get_text(strip=True) if desc else "","body":body,"related":[],
|
| 388 |
-
"source_url":url,"source":"vne","og_image":og_img}
|
| 389 |
-
except Exception as e:
|
| 390 |
-
return {"title":"⚠️ Lỗi","time":"","summary":str(e),"body":[],"related":[],"source_url":url,"source":"vne","og_image":""}
|
| 391 |
-
|
| 392 |
-
def _extract_body(cd):
|
| 393 |
-
body=[]
|
| 394 |
-
for ch in cd.children:
|
| 395 |
-
if not hasattr(ch,'name') or not ch.name: continue
|
| 396 |
-
if ch.name=="p":
|
| 397 |
-
im=ch.find("img")
|
| 398 |
-
if im:
|
| 399 |
-
s=im.get("src") or im.get("data-src")
|
| 400 |
-
if s: body.append({"type":"img","src":s,"alt":im.get("alt","")})
|
| 401 |
-
t=ch.get_text(strip=True)
|
| 402 |
-
if t: body.append({"type":"p","html":strip_links(''.join(str(c) for c in ch.children))})
|
| 403 |
-
elif ch.name in ("h2","h3","h4"):
|
| 404 |
-
body.append({"type":"heading","text":ch.get_text(strip=True)})
|
| 405 |
-
elif ch.name=="blockquote":
|
| 406 |
-
body.append({"type":"quote","text":ch.get_text(strip=True)})
|
| 407 |
-
return body
|
| 408 |
-
|
| 409 |
-
def _bdp_relates(soup):
|
| 410 |
-
rels=[]
|
| 411 |
-
rd=soup.select_one("div.relates")
|
| 412 |
-
if rd:
|
| 413 |
-
for a in rd.find_all("a",href=True):
|
| 414 |
-
t=a.get_text(strip=True); h=a.get("href","")
|
| 415 |
-
if t and len(t)>5:
|
| 416 |
-
if not h.startswith("http"): h=BASE_BDP+h
|
| 417 |
-
rels.append({"title":t,"link":h})
|
| 418 |
-
return rels[:5]
|
| 419 |
-
|
| 420 |
-
def fetch_video_list():
|
| 421 |
-
"""Fetch videos from 24h only."""
|
| 422 |
-
try:
|
| 423 |
-
return scrape_24h_video_list()
|
| 424 |
-
except:
|
| 425 |
-
return []
|
| 426 |
-
|
| 427 |
-
def fetch_tiktok_feed_videos():
|
| 428 |
-
"""Pre-fetch video URLs for TikTok fullscreen feed. 24h only."""
|
| 429 |
-
results = []
|
| 430 |
-
|
| 431 |
-
def _fetch_24h_vid(art):
|
| 432 |
-
vids = _extract_24h_video_urls(art["link"])
|
| 433 |
-
if not vids: return []
|
| 434 |
-
out = []
|
| 435 |
-
for pi, v in enumerate(vids):
|
| 436 |
-
label = f" (Phần {pi+1})" if len(vids)>1 else ""
|
| 437 |
-
out.append({"title": art["title"]+label, "src": v["src"], "poster": v["poster"],
|
| 438 |
-
"vtype": v["vtype"], "source": "24h", "link": art["link"]})
|
| 439 |
-
return out
|
| 440 |
-
|
| 441 |
-
h24_list = scrape_24h_video_list()[:15]
|
| 442 |
-
|
| 443 |
-
with ThreadPoolExecutor(max_workers=8) as ex:
|
| 444 |
-
h24_futures = {ex.submit(_fetch_24h_vid, a): a for a in h24_list}
|
| 445 |
-
for f in as_completed(h24_futures):
|
| 446 |
-
try:
|
| 447 |
-
r = f.result()
|
| 448 |
-
if r:
|
| 449 |
-
if isinstance(r, list): results.extend(r)
|
| 450 |
-
else: results.append(r)
|
| 451 |
-
except: pass
|
| 452 |
-
|
| 453 |
-
return results[:25]
|
| 454 |
-
|
| 455 |
-
# ── Dispatch ───────────────────────────────────────────────────────────────────
|
| 456 |
-
def fetch_homepage():
|
| 457 |
-
all_articles=[]
|
| 458 |
-
h24_videos=[]
|
| 459 |
-
def _fetch(src,url,group):
|
| 460 |
-
arts=scrape_bdp_list(url) if src=="bdp" else scrape_vne_list(url)
|
| 461 |
-
for a in arts: a["group"]=group
|
| 462 |
-
return arts
|
| 463 |
-
def _fetch_24h():
|
| 464 |
-
nonlocal h24_videos
|
| 465 |
-
try: h24_videos=scrape_24h_video_list()[:15]
|
| 466 |
-
except: pass
|
| 467 |
-
with ThreadPoolExecutor(max_workers=6) as ex:
|
| 468 |
-
ex.submit(_fetch_24h)
|
| 469 |
-
futures={ex.submit(_fetch,s,u,g):g for s,u,g in HOMEPAGE_SOURCES}
|
| 470 |
-
for f in as_completed(futures):
|
| 471 |
-
try: all_articles.extend(f.result())
|
| 472 |
-
except: pass
|
| 473 |
-
return all_articles, h24_videos
|
| 474 |
-
|
| 475 |
-
def fetch_news_list(category):
|
| 476 |
-
val=CATEGORIES.get(category,list(CATEGORIES.values())[0])
|
| 477 |
-
parts=val.split("::"); src,url_or_key,group=parts[0],parts[1],parts[2]
|
| 478 |
-
if src=="mix" and url_or_key=="home":
|
| 479 |
-
articles, h24_videos = fetch_homepage()
|
| 480 |
-
return render_homepage_html(articles, h24_videos)
|
| 481 |
-
if src=="mix" and url_or_key=="video":
|
| 482 |
-
return render_video_page_html()
|
| 483 |
-
articles=scrape_bdp_list(url_or_key) if src=="bdp" else scrape_vne_list(url_or_key)
|
| 484 |
-
for a in articles: a["group"]=group
|
| 485 |
-
return render_list_html(articles,group)
|
| 486 |
-
|
| 487 |
-
def read_article(url):
|
| 488 |
-
if not url or url=="#" or len(url)<10: return "<p>Không tìm thấy bài viết.</p>"
|
| 489 |
-
if "vnexpress.net" in url: return render_article_html(scrape_vne_article(url))
|
| 490 |
-
if "24h.com.vn" in url: return render_article_html(scrape_24h_article(url))
|
| 491 |
-
return render_article_html(scrape_bdp_article(url))
|
| 492 |
-
|
| 493 |
-
# ══════════════════════════════════════════════════════���═══════════════════════
|
| 494 |
-
# HTML RENDERERS
|
| 495 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 496 |
-
|
| 497 |
-
def render_video_carousel_html(videos):
|
| 498 |
-
"""Carousel video 24h highlights, dùng dữ liệu list page (không fetch từng bài)."""
|
| 499 |
-
vids_with_img = [v for v in videos if v.get("img")]
|
| 500 |
if not vids_with_img: return ""
|
| 501 |
items = []
|
| 502 |
for i, v in enumerate(vids_with_img[:15]):
|
|
@@ -504,700 +59,35 @@ def render_video_carousel_html(videos):
|
|
| 504 |
link = v.get("link","#")
|
| 505 |
title = v.get("title","")
|
| 506 |
aid = make_id(link); sl = slug(title)
|
| 507 |
-
click_js = f"window.bdpOpenTikTok('{esc(link)}','{aid}')"
|
| 508 |
-
items.append(f'''<div class="vslide-item" onclick="{click_js}">
|
| 509 |
-
<div class="vslide-thumb"><img src="{img}" alt="" class="bdp-lazy-img">
|
| 510 |
-
<div class="vslide-play">▶</div><span class="vslide-badge vslide-badge-24h">24h</span></div>
|
| 511 |
-
<p class="vslide-title">{title}</p></div>''')
|
| 512 |
-
return f'''<div class="vslide-wrap">
|
| 513 |
-
<div class="vslide-header"><span class="vslide-label">🎬 Video Highlight</span>
|
| 514 |
-
<div class="vslide-nav"><button class="vslide-btn" onclick="window.bdpSlideScroll(-1,'vslide-video')">◀</button>
|
| 515 |
-
<button class="vslide-btn" onclick="window.bdpSlideScroll(1,'vslide-video')">▶</button></div></div>
|
| 516 |
-
<div class="vslide-track" id="vslide-video">{''.join(items)}</div></div>'''
|
| 517 |
-
|
| 518 |
-
def render_featured_carousel_html(articles):
|
| 519 |
-
"""Carousel tin nổi bật + mới nhất, lấy từ articles đã fetch sẵn (không fetch thêm)."""
|
| 520 |
-
# Lấy bài featured có ảnh, ưu tiên bài đầu mỗi nhóm
|
| 521 |
-
top = []
|
| 522 |
-
seen = set()
|
| 523 |
-
for a in articles:
|
| 524 |
-
if a.get("img") and a.get("link","#") != "#" and a["link"] not in seen:
|
| 525 |
-
top.append(a)
|
| 526 |
-
seen.add(a["link"])
|
| 527 |
-
if len(top) >= 15: break
|
| 528 |
-
if not top: return ""
|
| 529 |
-
items = []
|
| 530 |
-
for i, a in enumerate(top):
|
| 531 |
-
img = safe_url(a.get("img",""))
|
| 532 |
-
link = a.get("link","#")
|
| 533 |
-
title = a.get("title","")
|
| 534 |
-
aid = make_id(link); sl = slug(title)
|
| 535 |
-
grp = a.get("group","")
|
| 536 |
-
src = a.get("source","")
|
| 537 |
-
badge_cls = "vslide-badge-vne" if src=="vne" else "vslide-badge-bdp" if src=="bdp" else "vslide-badge-24h"
|
| 538 |
-
badge_txt = grp or ("VnExpress" if src=="vne" else "BDP" if src=="bdp" else "24h")
|
| 539 |
-
click_js = f"window.bdpOpen('{esc(link)}','{aid}','{sl}')"
|
| 540 |
-
items.append(f'''<div class="vslide-item" onclick="{click_js}">
|
| 541 |
<div class="vslide-thumb"><img src="{img}" alt="" class="bdp-lazy-img">
|
| 542 |
-
<span class="vslide-badge
|
| 543 |
-
<p class="vslide-title">{title}</p></div>''')
|
| 544 |
-
return f'''<div class="vslide-wrap">
|
| 545 |
-
<div class="vslide-header"><span class="vslide-label">
|
| 546 |
-
<div class="vslide-nav"><button class="vslide-btn" onclick="window.bdpSlideScroll(-1,'vslide-
|
| 547 |
-
<button class="vslide-btn" onclick="window.bdpSlideScroll(1,'vslide-
|
| 548 |
-
<div class="vslide-track" id="vslide-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
def render_video_page_html():
|
| 572 |
-
"""Render video page with fullscreen TikTok feed + grid list."""
|
| 573 |
-
articles = fetch_video_list()
|
| 574 |
-
tiktok_videos = fetch_tiktok_feed_videos()
|
| 575 |
-
now = datetime.now(timezone(timedelta(hours=7))).strftime("%H:%M:%S %d/%m/%Y")
|
| 576 |
-
|
| 577 |
-
# Build TikTok fullscreen feed HTML
|
| 578 |
-
tiktok_html = ""
|
| 579 |
-
if tiktok_videos:
|
| 580 |
-
slides = []
|
| 581 |
-
for vi, v in enumerate(tiktok_videos):
|
| 582 |
-
poster = safe_url(v.get("poster",""))
|
| 583 |
-
poster_attr = f' poster="{poster}"' if poster else ""
|
| 584 |
-
vsrc = v["src"]
|
| 585 |
-
vtype = v.get("vtype","mp4")
|
| 586 |
-
title_esc = esc(v.get("title",""))
|
| 587 |
-
src_label = "24h" if v.get("source")=="24h" else "BDP"
|
| 588 |
-
badge_cls = "bdp-badge-24h" if v.get("source")=="24h" else "bdp-badge-bdp"
|
| 589 |
-
aid = make_id(v.get("link",""))
|
| 590 |
-
vid = make_id(vsrc)
|
| 591 |
-
sl = slug(v.get("title",""))
|
| 592 |
-
open_js = f"event.stopPropagation();window.bdpOpen('{esc(v.get('link',''))}','{aid}','{sl}')"
|
| 593 |
-
share_js = f"event.stopPropagation();window.bdpShareTikTok('{title_esc}','{sl}','{vid}')"
|
| 594 |
-
|
| 595 |
-
if vtype == "hls":
|
| 596 |
-
video_tag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} data-hls-src="{vsrc}" muted loop></video>'
|
| 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}" data-vid="{vid}">
|
| 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>
|
| 610 |
-
<div class="tiktok-actions">
|
| 611 |
-
<button class="tiktok-action-btn" onclick="{open_js}">📰 Đọc</button>
|
| 612 |
-
<button class="tiktok-action-btn" onclick="{share_js}">📤 Chia sẻ</button>
|
| 613 |
-
</div>
|
| 614 |
-
</div>
|
| 615 |
-
<div class="tiktok-unmute-hint" onclick="window.bdpTikTokUnmute(this)">🔇 Nhấn để bật tiếng</div>
|
| 616 |
-
<span class="tiktok-counter">{vi+1}/{len(tiktok_videos)}</span>
|
| 617 |
-
</div>''')
|
| 618 |
-
|
| 619 |
-
tiktok_html = f'''<div class="tiktok-fullscreen-container" id="tiktok-fullscreen-feed">
|
| 620 |
-
<div class="tiktok-fullscreen-feed">{''.join(slides)}</div>
|
| 621 |
-
</div>'''
|
| 622 |
-
|
| 623 |
-
# Also render article grid below
|
| 624 |
-
grid_html = ""
|
| 625 |
-
if articles:
|
| 626 |
-
feat=[a for a in articles if a.get("featured")]
|
| 627 |
-
reg=[a for a in articles if not a.get("featured")]
|
| 628 |
-
grid_parts = ['<div class="bdp-wrap">']
|
| 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)
|
| 640 |
-
|
| 641 |
-
return tiktok_html + grid_html
|
| 642 |
-
|
| 643 |
-
def render_list_html(articles,group_name=""):
|
| 644 |
-
if not articles: return "<p class='bdp-empty'>Không tìm thấy tin tức.</p>"
|
| 645 |
-
now=datetime.now(timezone(timedelta(hours=7))).strftime("%H:%M:%S %d/%m/%Y")
|
| 646 |
-
feat=[a for a in articles if a.get("featured")]
|
| 647 |
-
reg=[a for a in articles if not a.get("featured")]
|
| 648 |
-
parts=[f'<div class="bdp-wrap"><div class="bdp-topbar"><span>⏱ {now}</span><span>📰 {len(articles)} tin · {group_name}</span></div>']
|
| 649 |
-
if feat:
|
| 650 |
-
parts.append('<div class="bdp-grid bdp-grid-featured">')
|
| 651 |
-
for a in feat[:6]: parts.append(_list_card(a,True))
|
| 652 |
-
parts.append('</div>')
|
| 653 |
-
if reg:
|
| 654 |
-
parts.append('<div class="bdp-grid">')
|
| 655 |
-
for a in reg[:40]: parts.append(_list_card(a,False))
|
| 656 |
-
parts.append('</div>')
|
| 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"
|
| 664 |
-
is_video = art.get("is_video", False) or "/video/" in art.get("link","")
|
| 665 |
-
play_overlay = '<div class="bdp-play-overlay">▶</div>' if is_video else ""
|
| 666 |
-
img_html=f'<div class="{c}"><img src="{safe_url(art["img"])}" alt="" class="bdp-lazy-img">{play_overlay}</div>'
|
| 667 |
-
time_html=f'<span class="bdp-card-time">🕐 {art["time"]}</span>' if art.get("time") else ""
|
| 668 |
-
summ_html=f'<p class="bdp-card-summ">{art["summary"][:120]}...</p>' if art.get("summary") and len(art["summary"])>10 else ""
|
| 669 |
-
link=art.get("link","#"); aid=make_id(link)
|
| 670 |
-
tc="bdp-card-title bdp-card-title-big" if big else "bdp-card-title"
|
| 671 |
-
grp=art.get("group",""); badge=""
|
| 672 |
-
if art.get("source")=="vne": badge=f'<span class="bdp-badge bdp-badge-vne">{grp or "VnExpress"}</span>'
|
| 673 |
-
elif art.get("source")=="24h": badge=f'<span class="bdp-badge bdp-badge-24h">{grp or "24h"}</span>'
|
| 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}
|
| 684 |
-
<button class="bdp-share-btn" onclick="{share_js}" title="Chia sẻ">📤</button></div></div></div>"""
|
| 685 |
-
|
| 686 |
-
def render_article_html(article):
|
| 687 |
-
aid=make_id(article["source_url"]); sl=slug(article["title"])
|
| 688 |
-
src_url=article.get("source_url","")
|
| 689 |
-
og_img=safe_url(article.get("og_image",""))
|
| 690 |
-
share_js=f"window.bdpShareHash('{esc(article['title'])}','{sl}','{aid}')"
|
| 691 |
-
src_map={"vne":"VnExpress","bdp":"BongDaPlus","24h":"24h.com.vn"}
|
| 692 |
-
src_label=src_map.get(article.get("source",""),"")
|
| 693 |
-
seo=f'<div style="display:none" itemscope itemtype="https://schema.org/NewsArticle"><meta itemprop="headline" content="{esc(article["title"])}"><meta itemprop="image" content="{og_img}"><meta itemprop="description" content="{esc(article.get("summary","")[:160])}"></div>'
|
| 694 |
-
|
| 695 |
-
parts=[f"""{seo}<div class="bdp-article">
|
| 696 |
-
<h1 class="bdp-article-title">{article['title']}</h1>
|
| 697 |
-
<div class="bdp-article-meta"><span>🕐 {article['time']} · {src_label}</span>
|
| 698 |
-
<button class="bdp-share-article-btn" onclick="{share_js}">📤 Chia sẻ</button></div>"""]
|
| 699 |
-
if article.get("summary"):
|
| 700 |
-
parts.append(f'<div class="bdp-article-summary">{article["summary"]}</div>')
|
| 701 |
-
|
| 702 |
-
for item in article.get("body",[]):
|
| 703 |
-
if item["type"]=="video":
|
| 704 |
-
poster=safe_url(item.get("poster",""))
|
| 705 |
-
poster_attr=f' poster="{poster}"' if poster else ""
|
| 706 |
-
caption=item.get("caption","")
|
| 707 |
-
cap_html=f'<p class="bdp-figcap">{caption}</p>' if caption else ""
|
| 708 |
-
vtype=item.get("vtype","mp4")
|
| 709 |
-
vsrc=item["src"]
|
| 710 |
-
if vtype=="hls":
|
| 711 |
-
parts.append(f'<div class="bdp-video-wrap"><video controls playsinline preload="metadata"{poster_attr} class="bdp-video" data-hls-src="{vsrc}"></video>{cap_html}</div>')
|
| 712 |
-
else:
|
| 713 |
-
parts.append(f'<div class="bdp-video-wrap"><video controls playsinline preload="metadata"{poster_attr} class="bdp-video"><source src="{safe_url(vsrc)}" type="video/mp4"></video>{cap_html}</div>')
|
| 714 |
-
elif item["type"]=="img":
|
| 715 |
-
alt=item.get("alt",""); cap=f'<figcaption class="bdp-figcap">{alt}</figcaption>' if alt else ""
|
| 716 |
-
parts.append(f'<figure class="bdp-figure"><img src="{safe_url(item["src"])}" alt="{alt}" class="bdp-lazy-img">{cap}</figure>')
|
| 717 |
-
elif item["type"]=="p":
|
| 718 |
-
parts.append(f'<p class="bdp-article-p">{item["html"]}</p>')
|
| 719 |
-
elif item["type"]=="heading":
|
| 720 |
-
parts.append(f'<h2 class="bdp-article-h2">{item["text"]}</h2>')
|
| 721 |
-
elif item["type"]=="quote":
|
| 722 |
-
parts.append(f'<blockquote class="bdp-quote">{item["text"]}</blockquote>')
|
| 723 |
-
if article.get("related"):
|
| 724 |
-
parts.append('<div class="bdp-related"><h3>📰 Tin liên quan</h3>')
|
| 725 |
-
for rel in article["related"]:
|
| 726 |
-
rid=make_id(rel["link"]); rs=slug(rel["title"])
|
| 727 |
-
parts.append(f'<div class="bdp-related-item" onclick="window.bdpOpen(\'{esc(rel["link"])}\',\'{rid}\',\'{rs}\')"><span>▸ {rel["title"]}</span></div>')
|
| 728 |
-
parts.append('</div>')
|
| 729 |
-
parts.append(f"""<div class="bdp-comments" id="comments-{aid}"><h3>💬 Bình luận</h3>
|
| 730 |
-
<div id="cmt-list-{aid}"></div><div class="bdp-cmt-form">
|
| 731 |
-
<input id="cmt-name-{aid}" class="bdp-cmt-input" placeholder="Tên của bạn..." maxlength="50">
|
| 732 |
-
<textarea id="cmt-text-{aid}" class="bdp-cmt-textarea" placeholder="Viết bình luận..." rows="3" maxlength="500"></textarea>
|
| 733 |
-
<button class="bdp-cmt-submit" onclick="window.bdpAddCmt('{aid}')">Gửi bình luận</button></div></div>""")
|
| 734 |
-
parts.append('</div>')
|
| 735 |
-
return '\n'.join(parts)
|
| 736 |
-
|
| 737 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 738 |
-
CSS = """
|
| 739 |
-
body,html{margin:0!important;padding:0!important;overflow-x:hidden;background:#111!important}
|
| 740 |
-
.gradio-container{max-width:100%!important;width:100%!important;margin:0!important;padding:0!important;border-radius:0!important;background:#111!important}
|
| 741 |
-
.main,.contain{max-width:100%!important;width:100%!important;padding:0!important;margin:0!important}
|
| 742 |
-
.gradio-container>.main>.contain{padding-top:0!important}
|
| 743 |
-
.gap{gap:0!important}
|
| 744 |
-
footer,.built-with{display:none!important}
|
| 745 |
-
#article-url-input,#btn-read-article{display:none!important;height:0!important;overflow:hidden!important}
|
| 746 |
-
.bdp-header{background:linear-gradient(135deg,#0d1117,#1a3a2a 50%,#8b7500);padding:14px 16px;text-align:center}
|
| 747 |
-
.bdp-header h1{color:#fff;font-size:20px;margin:0;font-weight:800;text-shadow:0 2px 6px rgba(0,0,0,.4)}
|
| 748 |
-
.bdp-header p{color:rgba(255,255,255,.6);font-size:11px;margin:2px 0 0}
|
| 749 |
-
@media(min-width:768px){.bdp-header h1{font-size:26px}.bdp-header{padding:20px}}
|
| 750 |
-
#cat-input,#btn-switch-cat{display:none!important;height:0!important;overflow:hidden!important}
|
| 751 |
-
.vslide-wrap{margin:8px;background:#1a1a1a;border-radius:12px;overflow:hidden;border:1px solid #2a2a2a}
|
| 752 |
-
.vslide-header{display:flex;justify-content:space-between;align-items:center;padding:10px 14px 6px}
|
| 753 |
-
.vslide-label{color:#f0c040;font-size:15px;font-weight:700}
|
| 754 |
-
.vslide-nav{display:flex;gap:6px}
|
| 755 |
-
.vslide-btn{background:#333;color:#fff;border:none;width:30px;height:30px;border-radius:50%;cursor:pointer;font-size:13px;display:flex;align-items:center;justify-content:center;transition:background .15s}
|
| 756 |
-
.vslide-btn:hover{background:#555}
|
| 757 |
-
.vslide-track{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;gap:10px;padding:6px 14px 14px;scrollbar-width:none}
|
| 758 |
-
.vslide-track::-webkit-scrollbar{display:none}
|
| 759 |
-
.vslide-item{flex:0 0 200px;scroll-snap-align:start;cursor:pointer;transition:transform .15s}
|
| 760 |
-
.vslide-item:hover{transform:scale(1.03)}
|
| 761 |
-
@media(min-width:768px){.vslide-item{flex:0 0 240px}}
|
| 762 |
-
.vslide-thumb{position:relative;width:100%;aspect-ratio:16/9;border-radius:8px;overflow:hidden;background:#222}
|
| 763 |
-
.vslide-thumb img{width:100%;height:100%;object-fit:cover}
|
| 764 |
-
.vslide-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:40px;height:40px;background:rgba(0,0,0,.6);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:16px;pointer-events:none;transition:background .15s}
|
| 765 |
-
.vslide-item:hover .vslide-play{background:rgba(220,50,50,.8)}
|
| 766 |
-
.vslide-title{color:#ccc;font-size:12px;margin:6px 0 0;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
| 767 |
-
.bdp-wrap{padding:8px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
|
| 768 |
-
.bdp-topbar{display:flex;justify-content:space-between;padding:4px 4px 8px;color:#666;font-size:11px}
|
| 769 |
-
.bdp-empty{text-align:center;color:#666;padding:60px 20px}
|
| 770 |
-
.bdp-section{margin-bottom:16px}
|
| 771 |
-
.bdp-section-title{font-size:16px;font-weight:700;color:#5cb87a;margin:4px 0 8px;border-left:3px solid #5cb87a;padding-left:8px}
|
| 772 |
-
@media(min-width:768px){.bdp-section-title{font-size:18px}}
|
| 773 |
-
.bdp-grid{display:grid;grid-template-columns:1fr;gap:8px}
|
| 774 |
-
.bdp-grid-featured,.bdp-grid-home{margin-bottom:4px}
|
| 775 |
-
@media(min-width:420px){.bdp-grid{grid-template-columns:repeat(2,1fr)}}
|
| 776 |
-
@media(min-width:768px){.bdp-grid{grid-template-columns:repeat(3,1fr);gap:10px}}
|
| 777 |
-
@media(min-width:1100px){.bdp-grid{grid-template-columns:repeat(4,1fr)}}
|
| 778 |
-
.bdp-card{background:#1a1a1a;border-radius:10px;overflow:hidden;cursor:pointer;transition:transform .15s,box-shadow .15s;border:1px solid #222}
|
| 779 |
-
.bdp-card:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.5)}
|
| 780 |
-
.bdp-card:active{transform:scale(.98)}
|
| 781 |
-
.bdp-card-img{width:100%;height:130px;overflow:hidden;background:#222;position:relative}
|
| 782 |
-
.bdp-card-img-big{height:170px}
|
| 783 |
-
.bdp-card-img img{width:100%;height:100%;object-fit:cover}
|
| 784 |
-
@media(min-width:768px){.bdp-card-img{height:150px}.bdp-card-img-big{height:190px}}
|
| 785 |
-
.bdp-play-overlay{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:36px;height:36px;background:rgba(0,0,0,.55);border-radius:50%;display:flex;align-items:center;justify-content:center;color:#fff;font-size:15px;pointer-events:none}
|
| 786 |
-
.bdp-card-body{padding:8px 10px 6px}
|
| 787 |
-
.bdp-card-title{font-size:13px;font-weight:600;color:#eee;margin:0;line-height:1.4;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
| 788 |
-
.bdp-card-title-big{font-size:14.5px}
|
| 789 |
-
@media(min-width:768px){.bdp-card-title{font-size:13.5px}.bdp-card-title-big{font-size:15px}}
|
| 790 |
-
.bdp-card-summ{font-size:11.5px;color:#777;margin:4px 0 0;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
| 791 |
-
.bdp-card-footer{display:flex;justify-content:space-between;align-items:center;margin-top:6px}
|
| 792 |
-
.bdp-card-time{color:#555;font-size:10.5px}
|
| 793 |
-
.bdp-share-btn{background:none;border:none;cursor:pointer;font-size:15px;padding:3px 5px;border-radius:6px;transition:background .15s;color:#777}
|
| 794 |
-
.bdp-share-btn:hover{background:#333}
|
| 795 |
-
.bdp-badge{font-size:9px;padding:1px 6px;border-radius:3px;font-weight:700;display:inline-block;margin-bottom:4px}
|
| 796 |
-
.bdp-badge-vne{background:#c0392b;color:#fff}
|
| 797 |
-
.bdp-badge-bdp{background:#1a5c35;color:#fff}
|
| 798 |
-
.bdp-badge-24h{background:#e67e22;color:#fff}
|
| 799 |
-
.bdp-article{padding:14px 12px 30px;max-width:720px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
|
| 800 |
-
@media(min-width:768px){.bdp-article{padding:20px 16px 50px}}
|
| 801 |
-
.bdp-article-title{font-size:21px;font-weight:800;color:#f0f0f0;line-height:1.3;margin:0 0 8px}
|
| 802 |
-
@media(min-width:768px){.bdp-article-title{font-size:27px}}
|
| 803 |
-
.bdp-article-meta{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:6px;color:#666;font-size:12px;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #2a2a2a}
|
| 804 |
-
.bdp-share-article-btn{background:linear-gradient(135deg,#1a5c35,#2d8659);color:#fff;border:none;padding:6px 14px;border-radius:18px;font-size:12px;cursor:pointer;font-weight:600}
|
| 805 |
-
.bdp-share-article-btn:hover{opacity:.85}
|
| 806 |
-
.bdp-article-summary{background:#1a2a1f;border-left:4px solid #2d8659;padding:12px 14px;margin-bottom:16px;border-radius:0 8px 8px 0;font-weight:600;color:#ccc;line-height:1.5;font-size:14.5px}
|
| 807 |
-
.bdp-article-p{font-size:15.5px;line-height:1.75;color:#ccc;margin:0 0 12px}
|
| 808 |
-
@media(min-width:768px){.bdp-article-p{font-size:16.5px}}
|
| 809 |
-
.bdp-article-h2{font-size:19px;font-weight:700;color:#eee;margin:24px 0 10px}
|
| 810 |
-
.bdp-quote{border-left:4px solid #b8960c;padding:10px 14px;margin:14px 0;background:#1a1a10;font-style:italic;color:#bbb;border-radius:0 6px 6px 0}
|
| 811 |
-
.bdp-figure{margin:14px 0;text-align:center}
|
| 812 |
-
.bdp-figure img{max-width:100%;height:auto;border-radius:8px}
|
| 813 |
-
.bdp-figcap{color:#666;font-size:11.5px;margin-top:4px;font-style:italic}
|
| 814 |
-
.bdp-video-wrap{margin:14px 0;border-radius:10px;overflow:hidden;background:#000}
|
| 815 |
-
.bdp-video{width:100%;max-height:70vh;display:block;border-radius:10px}
|
| 816 |
-
.bdp-related{margin-top:24px;padding-top:14px;border-top:1px solid #2a2a2a}
|
| 817 |
-
.bdp-related h3{font-size:16px;color:#eee;margin:0 0 8px}
|
| 818 |
-
.bdp-related-item{padding:8px 10px;margin-bottom:5px;border:1px solid #262626;border-radius:8px;cursor:pointer;transition:background .15s}
|
| 819 |
-
.bdp-related-item:hover{background:#222}
|
| 820 |
-
.bdp-related-item span{font-size:13.5px;color:#5cb87a;font-weight:500}
|
| 821 |
-
.bdp-comments{margin-top:28px;padding-top:16px;border-top:1px solid #2a2a2a}
|
| 822 |
-
.bdp-comments h3{font-size:16px;color:#eee;margin:0 0 10px}
|
| 823 |
-
.bdp-cmt-item{background:#1a1a1a;border:1px solid #262626;border-radius:8px;padding:10px 12px;margin-bottom:8px}
|
| 824 |
-
.bdp-cmt-author{font-weight:700;color:#5cb87a;font-size:13px}
|
| 825 |
-
.bdp-cmt-date{color:#555;font-size:11px;margin-left:8px}
|
| 826 |
-
.bdp-cmt-body{color:#ccc;font-size:14px;margin-top:4px;line-height:1.5}
|
| 827 |
-
.bdp-cmt-form{margin-top:12px}
|
| 828 |
-
.bdp-cmt-input,.bdp-cmt-textarea{width:100%;padding:8px 10px;background:#1a1a1a;border:1px solid #333;border-radius:6px;color:#eee;font-size:13px;box-sizing:border-box}
|
| 829 |
-
.bdp-cmt-input{margin-bottom:6px}
|
| 830 |
-
.bdp-cmt-textarea{resize:vertical}
|
| 831 |
-
.bdp-cmt-submit{background:linear-gradient(135deg,#1a5c35,#2d8659);color:#fff;border:none;padding:8px 20px;border-radius:18px;font-size:13px;cursor:pointer;font-weight:600;margin-top:8px}
|
| 832 |
-
.bdp-cmt-submit:hover{opacity:.85}
|
| 833 |
-
.bdp-cmt-empty{color:#555;font-size:13px;font-style:italic;padding:8px 0}
|
| 834 |
-
.bdp-toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:#2d8659;color:#fff;padding:10px 22px;border-radius:22px;font-size:13px;z-index:9999;opacity:0;transition:opacity .3s;pointer-events:none;font-weight:500}
|
| 835 |
-
.bdp-toast.show{opacity:1}
|
| 836 |
-
.gr-group,.gr-box,.gr-panel{background:#111!important;border:none!important}
|
| 837 |
-
.label-wrap{background:#111!important}
|
| 838 |
-
|
| 839 |
-
/* ══ TikTok Fullscreen Video Feed ══ */
|
| 840 |
-
.tiktok-fullscreen-container{width:100%;background:#000;position:relative;height:calc(100vh - 60px);max-height:900px;min-height:500px;border-radius:0}
|
| 841 |
-
.tiktok-fullscreen-feed{height:100%;overflow-y:scroll;scroll-snap-type:y mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none}
|
| 842 |
-
.tiktok-fullscreen-feed::-webkit-scrollbar{display:none}
|
| 843 |
-
.tiktok-slide{height:calc(100vh - 60px);max-height:900px;min-height:500px;scroll-snap-align:start;scroll-snap-stop:always;position:relative;display:flex;align-items:center;justify-content:center;background:#000}
|
| 844 |
-
.tiktok-video{width:100%;height:100%;object-fit:contain;display:block}
|
| 845 |
-
.tiktok-bottom{position:absolute;bottom:0;left:0;right:0;padding:16px 14px 24px;background:linear-gradient(transparent,rgba(0,0,0,.85));z-index:3}
|
| 846 |
-
.tiktok-title{color:#fff;font-size:14px;font-weight:600;margin:4px 0 8px;line-height:1.4;text-shadow:0 1px 4px rgba(0,0,0,.8);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
| 847 |
-
.tiktok-actions{display:flex;gap:8px}
|
| 848 |
-
.tiktok-action-btn{background:rgba(255,255,255,.15);color:#fff;border:none;padding:6px 14px;border-radius:18px;font-size:12px;cursor:pointer;backdrop-filter:blur(6px);font-weight:500;transition:background .15s}
|
| 849 |
-
.tiktok-action-btn:hover{background:rgba(255,255,255,.3)}
|
| 850 |
-
.tiktok-counter{position:absolute;top:12px;left:12px;background:rgba(0,0,0,.5);color:#fff;font-size:11px;padding:3px 10px;border-radius:12px;z-index:4;backdrop-filter:blur(4px)}
|
| 851 |
-
.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}
|
| 852 |
-
.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}
|
| 853 |
-
.tiktok-slide.paused .tiktok-pause-icon{display:block}
|
| 854 |
-
.tiktok-seek-controls{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:flex;gap:40px;z-index:6;pointer-events:auto;opacity:0;transition:opacity .3s;pointer-events:none}
|
| 855 |
-
.tiktok-slide.show-controls .tiktok-seek-controls{opacity:1;pointer-events:auto}
|
| 856 |
-
.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;transition:opacity .2s}
|
| 857 |
-
.tiktok-seek-btn:hover{opacity:1}
|
| 858 |
-
.tiktok-seek-btn:active{transform:scale(.9)}
|
| 859 |
-
.vslide-badge{position:absolute;top:6px;left:6px;font-size:9px;padding:1px 6px;border-radius:3px;font-weight:700;z-index:2}
|
| 860 |
-
.vslide-badge-24h{background:#e67e22;color:#fff}
|
| 861 |
-
|
| 862 |
-
/* ══ Category Icon Grid ══ */
|
| 863 |
-
.cat-grid-wrap{padding:6px 8px 2px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;background:#111}
|
| 864 |
-
.cat-grid-wrap::-webkit-scrollbar{display:none}
|
| 865 |
-
.cat-grid{display:flex;gap:6px;min-width:max-content}
|
| 866 |
-
.cat-icon-btn{display:flex;flex-direction:column;align-items:center;justify-content:center;min-width:62px;padding:7px 4px 5px;border-radius:10px;cursor:pointer;transition:background .15s,transform .1s;background:#1a1a1a;border:1.5px solid transparent;user-select:none;-webkit-tap-highlight-color:transparent;text-decoration:none}
|
| 867 |
-
.cat-icon-btn:hover{background:#252525;transform:scale(1.04)}
|
| 868 |
-
.cat-icon-btn:active{transform:scale(.95)}
|
| 869 |
-
.cat-icon-btn.active{background:#1a3a2a;border-color:#5cb87a}
|
| 870 |
-
.cat-icon-emoji{font-size:22px;line-height:1.2}
|
| 871 |
-
.cat-icon-label{font-size:9.5px;color:#aaa;margin-top:2px;white-space:nowrap;font-weight:500;text-align:center;max-width:68px;overflow:hidden;text-overflow:ellipsis}
|
| 872 |
-
.cat-icon-btn.active .cat-icon-label{color:#5cb87a;font-weight:700}
|
| 873 |
-
@media(min-width:768px){.cat-icon-btn{min-width:72px;padding:8px 6px 6px}.cat-icon-emoji{font-size:25px}.cat-icon-label{font-size:10.5px;max-width:76px}}
|
| 874 |
-
"""
|
| 875 |
-
HEAD_META = """
|
| 876 |
-
<meta name="description" content="Tin tức tổng hợp nhanh nhất - VnExpress, BongDaPlus, 24h">
|
| 877 |
-
<meta property="og:title" content="Tin Tức Việt Nam - Tổng Hợp">
|
| 878 |
-
<meta property="og:description" content="Thời sự, thế giới, kinh doanh, công nghệ, thể thao, giải trí.">
|
| 879 |
-
<meta property="og:type" content="website">
|
| 880 |
-
<meta property="og:image" content="https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg">
|
| 881 |
-
<meta name="twitter:card" content="summary_large_image">
|
| 882 |
-
<link rel="canonical" href="https://bep40-bongdaplus-news.hf.space">
|
| 883 |
-
"""
|
| 884 |
-
|
| 885 |
-
JS_FUNC = """
|
| 886 |
-
function() {
|
| 887 |
-
var s=document.createElement('script');s.src='https://cdn.jsdelivr.net/npm/hls.js@1/dist/hls.min.js';document.head.appendChild(s);
|
| 888 |
-
var SPACE_BASE='""" + SPACE_URL + """';
|
| 889 |
-
|
| 890 |
-
window.bdpOpen=function(url,aid,sl){
|
| 891 |
-
window.location.hash='#/'+sl+'/'+aid;
|
| 892 |
-
try{localStorage.setItem('bdp_url_'+aid,url);}catch(e){}
|
| 893 |
-
var el=document.getElementById('article-url-input');
|
| 894 |
-
if(el){var ta=el.querySelector('textarea');if(ta){ta.value=url;ta.dispatchEvent(new Event('input',{bubbles:true}));}}
|
| 895 |
-
var btn=document.getElementById('btn-read-article');
|
| 896 |
-
if(btn){var b=btn.querySelector('button');if(b)b.click();else btn.click();}
|
| 897 |
-
window.scrollTo({top:0,behavior:'smooth'});
|
| 898 |
-
};
|
| 899 |
-
|
| 900 |
-
window.bdpShareHash=async function(title,sl,aid){
|
| 901 |
-
var hashUrl=SPACE_BASE+'/#/'+sl+'/'+aid;
|
| 902 |
-
var sd={title:title,url:hashUrl,text:title};
|
| 903 |
-
if(navigator.share){try{await navigator.share(sd);return;}catch(e){if(e.name==='AbortError')return;}}
|
| 904 |
-
if(navigator.clipboard&&navigator.clipboard.writeText){try{await navigator.clipboard.writeText(hashUrl);window.bdpToast('Đã sao chép liên kết!');return;}catch(e){}}
|
| 905 |
-
var ta=document.createElement('textarea');ta.value=hashUrl;ta.style.cssText='position:fixed;opacity:0';
|
| 906 |
-
document.body.appendChild(ta);ta.focus();ta.select();
|
| 907 |
-
try{document.execCommand('copy');window.bdpToast('Đã sao chép liên kết!');}catch(e){window.prompt('Sao chép:',hashUrl);}
|
| 908 |
-
document.body.removeChild(ta);
|
| 909 |
-
};
|
| 910 |
-
|
| 911 |
-
window.bdpShareTikTok=async function(title,sl,vid){
|
| 912 |
-
var hashUrl=SPACE_BASE+'/#/v/'+sl+'/'+vid;
|
| 913 |
-
var sd={title:title,url:hashUrl,text:title};
|
| 914 |
-
if(navigator.share){try{await navigator.share(sd);return;}catch(e){if(e.name==='AbortError')return;}}
|
| 915 |
-
if(navigator.clipboard&&navigator.clipboard.writeText){try{await navigator.clipboard.writeText(hashUrl);window.bdpToast('Đã sao chép liên kết!');return;}catch(e){}}
|
| 916 |
-
var ta=document.createElement('textarea');ta.value=hashUrl;ta.style.cssText='position:fixed;opacity:0';
|
| 917 |
-
document.body.appendChild(ta);ta.focus();ta.select();
|
| 918 |
-
try{document.execCommand('copy');window.bdpToast('Đã sao chép liên kết!');}catch(e){window.prompt('Sao chép:',hashUrl);}
|
| 919 |
-
document.body.removeChild(ta);
|
| 920 |
-
};
|
| 921 |
-
|
| 922 |
-
window.bdpToast=function(m){var e=document.getElementById('bdp-toast');if(!e){e=document.createElement('div');e.id='bdp-toast';e.className='bdp-toast';document.body.appendChild(e);}e.innerText=m;e.classList.add('show');setTimeout(function(){e.classList.remove('show');},2200);};
|
| 923 |
-
|
| 924 |
-
window.bdpSlideScroll=function(dir,trackId){
|
| 925 |
-
var track=document.getElementById(trackId||'vslide-track');
|
| 926 |
-
if(track){track.scrollBy({left:dir*260,behavior:'smooth'});}
|
| 927 |
-
};
|
| 928 |
-
|
| 929 |
-
/* ══ Category Icon Grid ══ */
|
| 930 |
-
window.bdpSelectCat=function(catKey,hashSlug){
|
| 931 |
-
window.location.hash='#cat/'+hashSlug;
|
| 932 |
-
/* Update active state visually */
|
| 933 |
-
document.querySelectorAll('.cat-icon-btn').forEach(function(b){
|
| 934 |
-
b.classList.toggle('active',b.getAttribute('data-cat')===catKey);
|
| 935 |
-
});
|
| 936 |
-
/* Trigger server-side category switch via hidden textbox+button */
|
| 937 |
-
window._bdpSetCat(catKey);
|
| 938 |
-
};
|
| 939 |
-
|
| 940 |
-
window._bdpSetCat=function(catKey){
|
| 941 |
-
var el=document.getElementById('cat-input');
|
| 942 |
-
if(el){var ta=el.querySelector('textarea')||el.querySelector('input');if(ta){ta.value=catKey;ta.dispatchEvent(new Event('input',{bubbles:true}));}}
|
| 943 |
-
var btn=document.getElementById('btn-switch-cat');
|
| 944 |
-
if(btn){var b=btn.querySelector('button');if(b)b.click();else btn.click();}
|
| 945 |
-
window.scrollTo({top:0,behavior:'smooth'});
|
| 946 |
-
};
|
| 947 |
-
|
| 948 |
-
function gc(a){try{return JSON.parse(localStorage.getItem('bdp_cmt_'+a))||[];}catch(e){return[];}}
|
| 949 |
-
function sc(a,c){try{localStorage.setItem('bdp_cmt_'+a,JSON.stringify(c));}catch(e){}}
|
| 950 |
-
window.bdpRenderCmt=function(a){var l=document.getElementById('cmt-list-'+a);if(!l)return;var c=gc(a);if(!c.length){l.innerHTML='<div class="bdp-cmt-empty">Chưa có bình luận. Hãy là người đầu tiên!</div>';return;}var h='';for(var i=c.length-1;i>=0;i--){var x=c[i];h+='<div class="bdp-cmt-item"><span class="bdp-cmt-author">'+x.name+'</span><span class="bdp-cmt-date">'+x.date+'</span><div class="bdp-cmt-body">'+x.text.replace(/</g,'<').replace(/>/g,'>')+'</div></div>';}l.innerHTML=h;};
|
| 951 |
-
window.bdpAddCmt=function(a){var n=document.getElementById('cmt-name-'+a),t=document.getElementById('cmt-text-'+a);if(!n||!t)return;var nm=n.value.trim(),tx=t.value.trim();if(!nm){window.bdpToast('Nhập tên');n.focus();return;}if(!tx){window.bdpToast('Nhập bình luận');t.focus();return;}var c=gc(a);c.push({name:nm,text:tx,date:new Date().toLocaleString('vi-VN')});sc(a,c);t.value='';window.bdpRenderCmt(a);window.bdpToast('Đã gửi!');};
|
| 952 |
-
|
| 953 |
-
function initHlsVideo(v){
|
| 954 |
-
if(v._hlsInit) return; v._hlsInit=true;
|
| 955 |
-
var src=v.getAttribute('data-hls-src'); if(!src) return;
|
| 956 |
-
if(v.canPlayType('application/vnd.apple.mpegURL')){v.src=src;}
|
| 957 |
-
else if(window.Hls&&Hls.isSupported()){var h=new Hls({maxBufferLength:30,maxMaxBufferLength:60});h.loadSource(src);h.attachMedia(v);h.on(Hls.Events.ERROR,function(e,data){if(data.fatal){v.src=src;}});}
|
| 958 |
-
else{v.src=src;}
|
| 959 |
-
}
|
| 960 |
-
|
| 961 |
-
/* ══ TikTok Fullscreen Feed ══ */
|
| 962 |
-
window.bdpSeek=function(btn,sec){
|
| 963 |
-
var slide=btn.closest('.tiktok-slide');
|
| 964 |
-
if(!slide) return;
|
| 965 |
-
var v=slide.querySelector('.tiktok-video');
|
| 966 |
-
if(!v) return;
|
| 967 |
-
v.currentTime=Math.max(0,Math.min(v.duration||9999,v.currentTime+sec));
|
| 968 |
-
};
|
| 969 |
-
|
| 970 |
-
window.bdpOpenTikTok=function(url,aid){
|
| 971 |
-
/* Switch to Video tab and scroll TikTok feed to matching video */
|
| 972 |
-
try{localStorage.setItem('bdp_url_'+aid,url);}catch(e){}
|
| 973 |
-
window.location.hash='#cat/video';
|
| 974 |
-
/* Update icon grid active state */
|
| 975 |
-
document.querySelectorAll('.cat-icon-btn').forEach(function(b){
|
| 976 |
-
b.classList.toggle('active',b.getAttribute('data-hash')==='video');
|
| 977 |
-
});
|
| 978 |
-
/* Trigger server-side switch to Video category */
|
| 979 |
-
var el=document.getElementById('cat-input');
|
| 980 |
-
if(el){var ta=el.querySelector('textarea')||el.querySelector('input');if(ta){ta.value='\U0001f3ac Video T\u1ed5ng H\u1ee3p';ta.dispatchEvent(new Event('input',{bubbles:true}));}}
|
| 981 |
-
var btn=document.getElementById('btn-switch-cat');
|
| 982 |
-
if(btn){var b=btn.querySelector('button');if(b)b.click();else btn.click();}
|
| 983 |
-
/* After content loads, scroll to the matching video in TikTok feed */
|
| 984 |
-
var attempts=0;
|
| 985 |
-
var finder=setInterval(function(){
|
| 986 |
-
attempts++;
|
| 987 |
-
var feed=document.querySelector('.tiktok-fullscreen-feed');
|
| 988 |
-
if(feed){
|
| 989 |
-
clearInterval(finder);
|
| 990 |
-
var slides=feed.querySelectorAll('.tiktok-slide');
|
| 991 |
-
var targetIdx=-1;
|
| 992 |
-
slides.forEach(function(sl,i){
|
| 993 |
-
if(targetIdx<0 && sl.getAttribute('data-aid')===aid) targetIdx=i;
|
| 994 |
-
});
|
| 995 |
-
if(targetIdx>=0 && slides[targetIdx]){
|
| 996 |
-
slides[targetIdx].scrollIntoView({behavior:'smooth'});
|
| 997 |
-
}
|
| 998 |
-
window.scrollTo({top:0,behavior:'smooth'});
|
| 999 |
-
}
|
| 1000 |
-
if(attempts>30) clearInterval(finder);
|
| 1001 |
-
},300);
|
| 1002 |
-
};
|
| 1003 |
-
window.bdpTikTokUnmute=function(hint){
|
| 1004 |
-
var feed=hint.closest('.tiktok-fullscreen-feed')||hint.closest('.tiktok-feed');
|
| 1005 |
-
var slide=hint.closest('.tiktok-slide');
|
| 1006 |
-
if(!slide) return;
|
| 1007 |
-
var video=slide.querySelector('.tiktok-video');
|
| 1008 |
-
if(!video) return;
|
| 1009 |
-
video.muted=!video.muted;
|
| 1010 |
-
var isMuted=video.muted;
|
| 1011 |
-
hint.textContent=isMuted?'🔇 Nhấn để bật tiếng':'🔊 Đang phát tiếng';
|
| 1012 |
-
if(feed){
|
| 1013 |
-
feed.querySelectorAll('.tiktok-video').forEach(function(v){v.muted=isMuted;});
|
| 1014 |
-
feed.querySelectorAll('.tiktok-unmute-hint').forEach(function(h){
|
| 1015 |
-
h.textContent=isMuted?'🔇 Nhấn để bật tiếng':'🔊 Đang phát tiếng';
|
| 1016 |
-
});
|
| 1017 |
-
}
|
| 1018 |
-
};
|
| 1019 |
-
|
| 1020 |
-
function initTikTokFullscreen(container){
|
| 1021 |
-
if(container._tikInit) return;
|
| 1022 |
-
container._tikInit=true;
|
| 1023 |
-
var feed=container.querySelector('.tiktok-fullscreen-feed');
|
| 1024 |
-
if(!feed) return;
|
| 1025 |
-
var slides=feed.querySelectorAll('.tiktok-slide');
|
| 1026 |
-
if(!slides.length) return;
|
| 1027 |
-
|
| 1028 |
-
/* Init all HLS videos first */
|
| 1029 |
-
slides.forEach(function(sl){
|
| 1030 |
-
var v=sl.querySelector('video[data-hls-src]');
|
| 1031 |
-
if(v) initHlsVideo(v);
|
| 1032 |
-
var v2=sl.querySelector('video:not([data-hls-src])');
|
| 1033 |
-
if(v2 && !v2._initDone){v2._initDone=true;v2.load();}
|
| 1034 |
-
});
|
| 1035 |
-
|
| 1036 |
-
var currentIdx=-1;
|
| 1037 |
-
|
| 1038 |
-
function tryPlay(v){
|
| 1039 |
-
var p=v.play();
|
| 1040 |
-
if(p&&p.catch) p.catch(function(){setTimeout(function(){v.play().catch(function(){});},500);});
|
| 1041 |
-
}
|
| 1042 |
-
|
| 1043 |
-
function activateSlide(idx){
|
| 1044 |
-
if(idx===currentIdx) return;
|
| 1045 |
-
slides.forEach(function(sl,i){
|
| 1046 |
-
var v=sl.querySelector('.tiktok-video');
|
| 1047 |
-
if(!v) return;
|
| 1048 |
-
if(i===idx){
|
| 1049 |
-
v.currentTime=0;
|
| 1050 |
-
tryPlay(v);
|
| 1051 |
-
sl.classList.remove('paused');
|
| 1052 |
-
} else {
|
| 1053 |
-
v.pause();
|
| 1054 |
-
sl.classList.remove('paused');
|
| 1055 |
-
}
|
| 1056 |
-
});
|
| 1057 |
-
currentIdx=idx;
|
| 1058 |
-
}
|
| 1059 |
-
|
| 1060 |
-
/* Use scroll event instead of IntersectionObserver for reliability inside Gradio */
|
| 1061 |
-
var scrollTimer=null;
|
| 1062 |
-
feed.addEventListener('scroll',function(){
|
| 1063 |
-
if(scrollTimer) clearTimeout(scrollTimer);
|
| 1064 |
-
scrollTimer=setTimeout(function(){
|
| 1065 |
-
var feedRect=feed.getBoundingClientRect();
|
| 1066 |
-
var feedCenter=feedRect.top+feedRect.height/2;
|
| 1067 |
-
var best=-1,bestDist=99999;
|
| 1068 |
-
slides.forEach(function(sl,i){
|
| 1069 |
-
var r=sl.getBoundingClientRect();
|
| 1070 |
-
var center=r.top+r.height/2;
|
| 1071 |
-
var dist=Math.abs(center-feedCenter);
|
| 1072 |
-
if(dist<bestDist){bestDist=dist;best=i;}
|
| 1073 |
-
});
|
| 1074 |
-
if(best>=0) activateSlide(best);
|
| 1075 |
-
},150);
|
| 1076 |
-
});
|
| 1077 |
-
|
| 1078 |
-
/* Start first video after short delay for HLS init */
|
| 1079 |
-
setTimeout(function(){activateSlide(0);},800);
|
| 1080 |
-
|
| 1081 |
-
/* Tap to pause/play + show/hide seek controls */
|
| 1082 |
-
slides.forEach(function(sl){
|
| 1083 |
-
var v=sl.querySelector('.tiktok-video');
|
| 1084 |
-
var hideTimer=null;
|
| 1085 |
-
function showSeekControls(){
|
| 1086 |
-
sl.classList.add('show-controls');
|
| 1087 |
-
if(hideTimer) clearTimeout(hideTimer);
|
| 1088 |
-
hideTimer=setTimeout(function(){sl.classList.remove('show-controls');},3000);
|
| 1089 |
-
}
|
| 1090 |
-
if(v){
|
| 1091 |
-
v.addEventListener('click',function(e){
|
| 1092 |
-
e.preventDefault();
|
| 1093 |
-
if(v.paused){tryPlay(v);sl.classList.remove('paused');}
|
| 1094 |
-
else{v.pause();sl.classList.add('paused');}
|
| 1095 |
-
showSeekControls();
|
| 1096 |
-
});
|
| 1097 |
-
v.addEventListener('touchstart',function(){showSeekControls();},{passive:true});
|
| 1098 |
-
}
|
| 1099 |
-
});
|
| 1100 |
-
}
|
| 1101 |
-
|
| 1102 |
-
new MutationObserver(function(muts){muts.forEach(function(m){m.addedNodes.forEach(function(n){
|
| 1103 |
-
if(!n.querySelectorAll) return;
|
| 1104 |
-
n.querySelectorAll('img.bdp-lazy-img').forEach(function(img){if(!img._errBound){img._errBound=true;img.addEventListener('error',function(){this.style.display='none';});}});
|
| 1105 |
-
n.querySelectorAll('video[data-hls-src]').forEach(initHlsVideo);
|
| 1106 |
-
n.querySelectorAll('video.bdp-video:not([data-hls-src])').forEach(function(v){if(!v._initDone){v._initDone=true;v.load();}});
|
| 1107 |
-
n.querySelectorAll('[id^="cmt-list-"]').forEach(function(d){window.bdpRenderCmt(d.id.replace('cmt-list-',''));});
|
| 1108 |
-
n.querySelectorAll('.tiktok-fullscreen-container').forEach(initTikTokFullscreen);
|
| 1109 |
-
if(n.classList && n.classList.contains('tiktok-fullscreen-container')) initTikTokFullscreen(n);
|
| 1110 |
-
});});}).observe(document.body,{childList:true,subtree:true});
|
| 1111 |
-
|
| 1112 |
-
setInterval(function(){
|
| 1113 |
-
document.querySelectorAll('video[data-hls-src]').forEach(initHlsVideo);
|
| 1114 |
-
document.querySelectorAll('video.bdp-video:not([data-hls-src])').forEach(function(v){if(!v._initDone){v._initDone=true;v.load();}});
|
| 1115 |
-
document.querySelectorAll('.tiktok-fullscreen-container').forEach(initTikTokFullscreen);
|
| 1116 |
-
},1500);
|
| 1117 |
-
|
| 1118 |
-
var hh=window.location.hash;
|
| 1119 |
-
if(hh&&hh.startsWith('#/v/')){
|
| 1120 |
-
/* TikTok video hash: #/v/slug/vid - switch to video page then scroll to vid */
|
| 1121 |
-
var vps=hh.slice(4).split('/');
|
| 1122 |
-
var vid=vps[vps.length-1];
|
| 1123 |
-
setTimeout(function(){
|
| 1124 |
-
/* Switch to Video category */
|
| 1125 |
-
window._bdpSetCat('\U0001f3ac Video T\u1ed5ng H\u1ee3p');
|
| 1126 |
-
document.querySelectorAll('.cat-icon-btn').forEach(function(b){
|
| 1127 |
-
b.classList.toggle('active',b.getAttribute('data-hash')==='video');
|
| 1128 |
-
});
|
| 1129 |
-
/* Poll for TikTok feed to appear then scroll to matching vid */
|
| 1130 |
-
var att=0;
|
| 1131 |
-
var ff=setInterval(function(){
|
| 1132 |
-
att++;
|
| 1133 |
-
var feed=document.querySelector('.tiktok-fullscreen-feed');
|
| 1134 |
-
if(feed){
|
| 1135 |
-
clearInterval(ff);
|
| 1136 |
-
var slides=feed.querySelectorAll('.tiktok-slide');
|
| 1137 |
-
slides.forEach(function(sl,i){
|
| 1138 |
-
if(sl.getAttribute('data-vid')===vid){
|
| 1139 |
-
sl.scrollIntoView({behavior:'smooth'});
|
| 1140 |
-
}
|
| 1141 |
-
});
|
| 1142 |
-
window.scrollTo({top:0,behavior:'smooth'});
|
| 1143 |
-
}
|
| 1144 |
-
if(att>30) clearInterval(ff);
|
| 1145 |
-
},300);
|
| 1146 |
-
},1500);
|
| 1147 |
-
} else if(hh&&hh.startsWith('#cat/')){
|
| 1148 |
-
/* Category hash: #cat/video, #cat/thoi-su, etc. */
|
| 1149 |
-
var catSlug=hh.slice(5);
|
| 1150 |
-
var catMap={""" + ",".join(f"'{ci[2]}':'{esc(CAT_KEYS[i])}'" for i,ci in enumerate(CAT_ICONS)) + """};
|
| 1151 |
-
if(catMap[catSlug]){
|
| 1152 |
-
setTimeout(function(){
|
| 1153 |
-
window._bdpSetCat(catMap[catSlug]);
|
| 1154 |
-
document.querySelectorAll('.cat-icon-btn').forEach(function(b){
|
| 1155 |
-
b.classList.toggle('active',b.getAttribute('data-hash')===catSlug);
|
| 1156 |
-
});
|
| 1157 |
-
},1500);
|
| 1158 |
-
}
|
| 1159 |
-
} else if(hh&&hh.startsWith('#/')){var ps=hh.slice(2).split('/');if(ps.length>=2){var aid=ps[ps.length-1];try{var url=localStorage.getItem('bdp_url_'+aid);if(url)setTimeout(function(){window.bdpOpen(url,aid,ps.slice(0,-1).join('/'));},2000);}catch(e){}}}
|
| 1160 |
-
}
|
| 1161 |
-
"""
|
| 1162 |
-
|
| 1163 |
-
# ══════════════════════════════════════════════════════════════════════════════
|
| 1164 |
-
def _build_cat_grid_html():
|
| 1165 |
-
"""Build the category icon grid HTML."""
|
| 1166 |
-
items = []
|
| 1167 |
-
for i, (icon, label, hslug) in enumerate(CAT_ICONS):
|
| 1168 |
-
cat_key = CAT_KEYS[i]
|
| 1169 |
-
active = " active" if i == 0 else ""
|
| 1170 |
-
click_js = f"window.bdpSelectCat('{esc(cat_key)}','{hslug}')"
|
| 1171 |
-
items.append(f'<div class="cat-icon-btn{active}" data-cat="{esc(cat_key)}" data-hash="{hslug}" onclick="{click_js}"><span class="cat-icon-emoji">{icon}</span><span class="cat-icon-label">{label}</span></div>')
|
| 1172 |
-
return f'<div class="cat-grid-wrap"><div class="cat-grid">{"".join(items)}</div></div>'
|
| 1173 |
-
|
| 1174 |
-
with gr.Blocks(title="Tin Tức Việt Nam",css=CSS,head=HEAD_META,js=JS_FUNC,theme=gr.themes.Base(),fill_width=True) as demo:
|
| 1175 |
-
gr.HTML('<div class="bdp-header"><h1>📰 Tin Tức Việt Nam</h1><p>VnExpress · BongDaPlus · 24h · Thời sự · Thế giới · Kinh doanh · Công nghệ · Thể thao · Giải trí �� Video</p></div>')
|
| 1176 |
-
gr.HTML(_build_cat_grid_html())
|
| 1177 |
-
article_url=gr.Textbox(value="",visible=False,elem_id="article-url-input")
|
| 1178 |
-
cat_input=gr.Textbox(value="",visible=False,elem_id="cat-input")
|
| 1179 |
-
back_btn=gr.Button("← Quay lại",variant="secondary",visible=False)
|
| 1180 |
-
news_list=gr.HTML()
|
| 1181 |
-
article_view=gr.HTML(visible=False)
|
| 1182 |
-
read_btn=gr.Button("Đọc",visible=False,elem_id="btn-read-article")
|
| 1183 |
-
cat_btn=gr.Button("Cat",visible=False,elem_id="btn-switch-cat")
|
| 1184 |
-
def show_article(url):
|
| 1185 |
-
if not url or url=="#" or len(url)<10:
|
| 1186 |
-
return gr.update(visible=True),gr.update(visible=False),gr.update(visible=False),""
|
| 1187 |
-
return (gr.update(visible=False),gr.update(value=read_article(url),visible=True),gr.update(visible=True),"")
|
| 1188 |
-
def switch_cat(cat_key):
|
| 1189 |
-
cat_key=cat_key.strip()
|
| 1190 |
-
if not cat_key or cat_key not in CATEGORIES:
|
| 1191 |
-
cat_key=list(CATEGORIES.keys())[0]
|
| 1192 |
-
return (gr.update(value=fetch_news_list(cat_key),visible=True),gr.update(visible=False),gr.update(visible=False),"")
|
| 1193 |
-
def show_list_home():
|
| 1194 |
-
return (gr.update(value=fetch_news_list(list(CATEGORIES.keys())[0]),visible=True),gr.update(visible=False),gr.update(visible=False))
|
| 1195 |
-
read_btn.click(fn=show_article,inputs=[article_url],outputs=[news_list,article_view,back_btn,article_url])
|
| 1196 |
-
cat_btn.click(fn=switch_cat,inputs=[cat_input],outputs=[news_list,article_view,back_btn,cat_input])
|
| 1197 |
-
back_btn.click(fn=show_list_home,inputs=[],outputs=[news_list,article_view,back_btn])
|
| 1198 |
-
timer=gr.Timer(value=REFRESH_SECONDS,active=True)
|
| 1199 |
-
timer.tick(fn=lambda: fetch_news_list(list(CATEGORIES.keys())[0]),inputs=None,outputs=news_list)
|
| 1200 |
-
demo.load(fn=lambda: fetch_news_list(list(CATEGORIES.keys())[0]),inputs=None,outputs=news_list)
|
| 1201 |
-
|
| 1202 |
-
if __name__=="__main__":
|
| 1203 |
-
demo.launch(ssr_mode=False)
|
|
|
|
| 1 |
+
"""Entry point: patches main_app.py to add 24h news shorts, then runs it."""
|
| 2 |
+
import os, sys
|
| 3 |
+
|
| 4 |
+
APP_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "main_app.py")
|
| 5 |
+
|
| 6 |
+
# Read and patch
|
| 7 |
+
with open(APP_PATH, "r", encoding="utf-8") as f:
|
| 8 |
+
code = f.read()
|
| 9 |
+
|
| 10 |
+
if "scrape_24h_news_shorts" not in code:
|
| 11 |
+
# 1. Add import
|
| 12 |
+
code = code.replace(
|
| 13 |
+
"from concurrent.futures import ThreadPoolExecutor, as_completed",
|
| 14 |
+
"from concurrent.futures import ThreadPoolExecutor, as_completed\nfrom shorts_scraper import scrape_24h_news_shorts"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
# 2. Modify fetch_homepage
|
| 18 |
+
code = code.replace(
|
| 19 |
+
"def fetch_homepage():\n all_articles=[]\n h24_videos=[]\n",
|
| 20 |
+
"def fetch_homepage():\n all_articles=[]\n h24_videos=[]\n h24_shorts=[]\n"
|
| 21 |
+
)
|
| 22 |
+
code = code.replace(
|
| 23 |
+
" with ThreadPoolExecutor(max_workers=6) as ex:\n ex.submit(_fetch_24h)",
|
| 24 |
+
" def _fetch_shorts():\n nonlocal h24_shorts\n try: h24_shorts=scrape_24h_news_shorts()[:15]\n except: pass\n with ThreadPoolExecutor(max_workers=7) as ex:\n ex.submit(_fetch_24h)\n ex.submit(_fetch_shorts)"
|
| 25 |
+
)
|
| 26 |
+
code = code.replace(
|
| 27 |
+
" return all_articles, h24_videos",
|
| 28 |
+
" return all_articles, h24_videos, h24_shorts"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# 3. Update fetch_news_list
|
| 32 |
+
code = code.replace(
|
| 33 |
+
" articles, h24_videos = fetch_homepage()\n return render_homepage_html(articles, h24_videos)",
|
| 34 |
+
" articles, h24_videos, h24_shorts = fetch_homepage()\n return render_homepage_html(articles, h24_videos, h24_shorts)"
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
# 4. Update render_homepage_html signature and body
|
| 38 |
+
code = code.replace(
|
| 39 |
+
"def render_homepage_html(articles, h24_videos=None):",
|
| 40 |
+
"def render_homepage_html(articles, h24_videos=None, h24_shorts=None):"
|
| 41 |
+
)
|
| 42 |
+
code = code.replace(
|
| 43 |
+
" video_carousel = render_video_carousel_html(h24_videos or [])\n news_carousel = render_featured_carousel_html(articles)",
|
| 44 |
+
" shorts_carousel = render_shorts_carousel_html(h24_shorts or [])\n video_carousel = render_video_carousel_html(h24_videos or [])\n news_carousel = render_featured_carousel_html(articles)"
|
| 45 |
+
)
|
| 46 |
+
code = code.replace(
|
| 47 |
+
"{video_carousel}{news_carousel}",
|
| 48 |
+
"{shorts_carousel}{video_carousel}{news_carousel}"
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# 5. Add render_shorts_carousel_html function
|
| 52 |
+
shorts_fn = '''def render_shorts_carousel_html(shorts):
|
| 53 |
+
"""Carousel shorts tin tuc 24h - slide ngang, click vao xem TikTok."""
|
| 54 |
+
vids_with_img = [v for v in shorts if v.get("img")]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
if not vids_with_img: return ""
|
| 56 |
items = []
|
| 57 |
for i, v in enumerate(vids_with_img[:15]):
|
|
|
|
| 59 |
link = v.get("link","#")
|
| 60 |
title = v.get("title","")
|
| 61 |
aid = make_id(link); sl = slug(title)
|
| 62 |
+
click_js = f"window.bdpOpenTikTok(\\'{esc(link)}\\',\\'{aid}\\')"
|
| 63 |
+
items.append(f\\'\\'\\'<div class="vslide-item" onclick="{click_js}">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
<div class="vslide-thumb"><img src="{img}" alt="" class="bdp-lazy-img">
|
| 65 |
+
<div class="vslide-play">\\u25b6</div><span class="vslide-badge vslide-badge-shorts">Shorts</span></div>
|
| 66 |
+
<p class="vslide-title">{title}</p></div>\\'\\'\\'\\')
|
| 67 |
+
return f\\'\\'\\'<div class="vslide-wrap">
|
| 68 |
+
<div class="vslide-header"><span class="vslide-label">\\U0001f4f1 Shorts Tin T\\u1ee9c</span>
|
| 69 |
+
<div class="vslide-nav"><button class="vslide-btn" onclick="window.bdpSlideScroll(-1,\\'vslide-shorts\\')">\\u25c0</button>
|
| 70 |
+
<button class="vslide-btn" onclick="window.bdpSlideScroll(1,\\'vslide-shorts\\')">\\u25b6</button></div></div>
|
| 71 |
+
<div class="vslide-track" id="vslide-shorts">{\\'\\'.join(items)}</div></div>\\'\\'\\'
|
| 72 |
+
|
| 73 |
+
'''
|
| 74 |
+
code = code.replace(
|
| 75 |
+
"def render_video_carousel_html(videos):",
|
| 76 |
+
shorts_fn + "def render_video_carousel_html(videos):"
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
# 6. Add CSS for shorts badge
|
| 80 |
+
code = code.replace(
|
| 81 |
+
".vslide-badge-24h{background:#e67e22;color:#fff}",
|
| 82 |
+
".vslide-badge-24h{background:#e67e22;color:#fff}\n.vslide-badge-shorts{background:#ff0050;color:#fff}"
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
# Write patched version back
|
| 86 |
+
with open(APP_PATH, "w", encoding="utf-8") as f:
|
| 87 |
+
f.write(code)
|
| 88 |
+
print("[patcher] Successfully patched main_app.py with shorts carousel")
|
| 89 |
+
else:
|
| 90 |
+
print("[patcher] Already patched, skipping")
|
| 91 |
+
|
| 92 |
+
# Now execute the patched app
|
| 93 |
+
exec(compile(open(APP_PATH, encoding="utf-8").read(), APP_PATH, "exec"), {"__name__": "__main__", "__file__": APP_PATH})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|