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

fix: 24h in slider + TikTok feed scroll-based playback

Browse files
Files changed (1) hide show
  1. app.py +1029 -1
app.py CHANGED
@@ -1 +1,1029 @@
1
- # Content from /app/app.py uploaded via script
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import re
4
+ import hashlib
5
+ import json
6
+ from urllib.parse import quote as urlquote
7
+ from bs4 import BeautifulSoup
8
+ from datetime import datetime
9
+ from concurrent.futures import ThreadPoolExecutor, as_completed
10
+
11
+ HEADERS = {
12
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
13
+ "Accept-Language": "vi-VN,vi;q=0.9,en;q=0.8",
14
+ }
15
+ BASE_BDP = "https://bongdaplus.vn"
16
+ BASE_24H = "https://www.24h.com.vn"
17
+ REFRESH_SECONDS = 300
18
+ SPACE_URL = "https://bep40-bongdaplus-news.hf.space"
19
+
20
+ CATEGORIES = {
21
+ "🏠 Trang Chủ (Nổi Bật)": "mix::home::Trang Chủ",
22
+ "🎬 Video Tổng Hợp": "mix::video::Video",
23
+ "📰 Thời Sự": "vne::https://vnexpress.net/thoi-su::Thời Sự",
24
+ "🌍 Thế Giới": "vne::https://vnexpress.net/the-gioi::Thế Giới",
25
+ "💰 Kinh Doanh": "vne::https://vnexpress.net/kinh-doanh::Kinh Doanh",
26
+ "💻 Công Nghệ": "vne::https://vnexpress.net/so-hoa::Công Nghệ",
27
+ "🔬 Khoa Học": "vne::https://vnexpress.net/khoa-hoc::Khoa Học",
28
+ "🎬 Giải Trí": "vne::https://vnexpress.net/giai-tri::Giải Trí",
29
+ "🏥 Sức Khỏe": "vne::https://vnexpress.net/suc-khoe::Sức Khỏe",
30
+ "🎓 Giáo Dục": "vne::https://vnexpress.net/giao-duc::Giáo Dục",
31
+ "✈️ Du Lịch": "vne::https://vnexpress.net/du-lich::Du Lịch",
32
+ "⚽ Thể Thao": "vne::https://vnexpress.net/the-thao::Thể Thao",
33
+ "⚽ Bóng Đá QT": "vne::https://vnexpress.net/the-thao/bong-da::Bóng Đá",
34
+ "🏴󠁧󠁢󠁥󠁮󠁧󠁿 Ngoại Hạng Anh": "bdp::https://bongdaplus.vn/ngoai-hang-anh::Bóng Đá",
35
+ "🇪🇸 La Liga": "bdp::https://bongdaplus.vn/la-liga::Bóng Đá",
36
+ "🏆 Champions League": "bdp::https://bongdaplus.vn/champions-league-cup-c1::Bóng Đá",
37
+ "🇻🇳 Bóng Đá VN": "bdp::https://bongdaplus.vn/bong-da-viet-nam::Bóng Đá",
38
+ "🔄 Chuyển Nhượng": "bdp::https://bongdaplus.vn/tin-chuyen-nhuong::Bóng Đá",
39
+ }
40
+ HOMEPAGE_SOURCES = [
41
+ ("vne","https://vnexpress.net/thoi-su","Thời Sự"),
42
+ ("vne","https://vnexpress.net/the-gioi","Thế Giới"),
43
+ ("vne","https://vnexpress.net/kinh-doanh","Kinh Doanh"),
44
+ ("vne","https://vnexpress.net/so-hoa","Công Nghệ"),
45
+ ("vne","https://vnexpress.net/the-thao","Thể Thao"),
46
+ ("vne","https://vnexpress.net/giai-tri","Giải Trí"),
47
+ ("bdp","https://bongdaplus.vn/tin-moi","Bóng Đá"),
48
+ ]
49
+
50
+ # ── Helpers ────────────────────────────────────────────────────────────────────
51
+ def strip_links(html):
52
+ return re.sub(r'</a>', '', re.sub(r'<a\s[^>]*>', '', html))
53
+
54
+ def esc(text):
55
+ return text.replace("\\","\\\\").replace("'","\\'").replace('"','\\"').replace("\n"," ")
56
+
57
+ def safe_url(url):
58
+ if not url: return ""
59
+ try:
60
+ return urlquote(url, safe=':/?#[]@!$&()*+,;=-._~%')
61
+ except Exception:
62
+ return url.replace(" ","%20").replace("'","%27").replace('"',"%22")
63
+
64
+ def make_id(url):
65
+ return hashlib.md5(url.encode()).hexdigest()[:12]
66
+
67
+ def slug(text):
68
+ s = text.lower().strip()
69
+ for p,r in [('[àáạảãâầấậẩẫăằắặẳẵ]','a'),('[èéẹẻẽêềếệểễ]','e'),('[ìíịỉĩ]','i'),
70
+ ('[òóọỏõôồốộổỗơờớợởỡ]','o'),('[ùúụủũưừứựửữ]','u'),('[ỳýỵỷỹ]','y'),('[đ]','d')]:
71
+ s = re.sub(p,r,s)
72
+ return re.sub(r'[\s-]+','-',re.sub(r'[^a-z0-9\s-]','',s)).strip('-')[:60]
73
+
74
+ def _extract_bdp_video_id(url):
75
+ m = re.search(r'-(\d{6,})\.html', url)
76
+ return m.group(1) if m else None
77
+
78
+ # ══════════════════════════════════════════════════════════════════════════════
79
+ # SCRAPERS
80
+ # ══════════════════════════════════════════════════════════════════════════════
81
+ def _get(url):
82
+ r = requests.get(url, headers=HEADERS, timeout=15); r.encoding="utf-8"
83
+ return BeautifulSoup(r.text, "lxml")
84
+
85
+ def _get_raw(url):
86
+ r = requests.get(url, headers=HEADERS, timeout=15); r.encoding="utf-8"
87
+ return r.text, BeautifulSoup(r.text, "lxml")
88
+
89
+ # ── BDP Video Embed Fetcher ────────────────────────────────────────────────────
90
+ def fetch_bdp_embed_data(video_id):
91
+ try:
92
+ embed_url = f"{BASE_BDP}/video-embed/{video_id}.html"
93
+ html, soup = _get_raw(embed_url)
94
+ video = soup.select_one("video#videoPlayer")
95
+ if not video: return None
96
+ source = video.find("source")
97
+ result = {"mp4": source.get("src","") if source else "", "poster": video.get("poster","")}
98
+ carousel_match = re.findall(r'vdo\.relatedCarousel\((\[.*?\])\)', html, re.DOTALL)
99
+ if carousel_match:
100
+ posters = re.findall(r"poster:\s*'([^']+)'", carousel_match[0])
101
+ vdorefs = re.findall(r"vdoref:\s*'([^']+)'", carousel_match[0])
102
+ titles = re.findall(r"title:\s*'([^']*)'", carousel_match[0])
103
+ result["related"] = [
104
+ {"poster": p, "link": BASE_BDP + v if not v.startswith("http") else v, "title": t}
105
+ for p, v, t in zip(posters, vdorefs, titles)
106
+ ]
107
+ return result
108
+ except: return None
109
+
110
+ # ── BDP Scrapers ───────────────────────────────────────────────────────────────
111
+ def scrape_bdp_list(url):
112
+ try:
113
+ soup = _get(url); articles,seen=[],set()
114
+ for sel,feat in [("div.news.fst",True),("div.sld-itm.news",True),("li.news",False)]:
115
+ for it in soup.select(sel):
116
+ tag = it.find("a",class_="title") or it.find("a",href=True)
117
+ if not tag: continue
118
+ t=tag.get_text(strip=True); lk=tag.get("href","")
119
+ if not t or len(t)<5: continue
120
+ if lk and not lk.startswith("http"): lk=BASE_BDP+lk
121
+ if lk in seen: continue
122
+ im=it.find("img"); img=(im.get("data-src") or im.get("src")) if im else None
123
+ sm=it.find("p",class_="summ"); tt=it.find("div",class_="in-time")
124
+ is_video = "/video/" in lk
125
+ articles.append({"title":t,"link":lk,"img":img,"summary":sm.get_text(strip=True) if sm else "",
126
+ "time":tt.get_text(strip=True) if tt else "","featured":feat,"source":"bdp","group":"","is_video":is_video})
127
+ seen.add(lk)
128
+ return articles
129
+ except Exception as e:
130
+ return [{"title":f"⚠️ {e}","link":"#","img":None,"summary":"","time":"","featured":False,"source":"bdp","group":"","is_video":False}]
131
+
132
+ def scrape_bdp_video_list():
133
+ try:
134
+ soup = _get(f"{BASE_BDP}/video")
135
+ articles, seen = [], set()
136
+ for a in soup.find_all("a", href=True):
137
+ href = a.get("href", "")
138
+ if "/video/" not in href: continue
139
+ if href in ("/video/", "/video/ban-thang-dep", "/video/highlight"): continue
140
+ if not href.startswith("http"): href = BASE_BDP + href
141
+ if href in seen: continue
142
+ title = a.get_text(strip=True)
143
+ title = re.sub(r'^\d{2}:\d{2}', '', title).strip()
144
+ if not title or len(title) < 5: continue
145
+ img_tag = a.find("img")
146
+ if not img_tag:
147
+ parent = a.parent
148
+ if parent: img_tag = parent.find("img")
149
+ img = None
150
+ if img_tag: img = img_tag.get("data-src") or img_tag.get("src") or img_tag.get("data-original")
151
+ articles.append({"title": title, "link": href, "img": img, "summary": "", "time": "",
152
+ "featured": len(articles) < 3, "source": "bdp", "group": "BongDaPlus Video", "is_video": True})
153
+ seen.add(href)
154
+ return articles[:30]
155
+ except Exception as e:
156
+ return [{"title": f"⚠️ {e}", "link": "#", "img": None, "summary": "", "time": "",
157
+ "featured": False, "source": "bdp", "group": "", "is_video": False}]
158
+
159
+ def scrape_bdp_article(url):
160
+ try:
161
+ soup = _get(url)
162
+ h1=soup.select_one(".lead-title h1") or soup.select_one("h1")
163
+ te=soup.select_one(".emobar .rgt"); se=soup.select_one("div.summary")
164
+ og=soup.find("meta",property="og:image"); og_img=og.get("content","") if og else ""
165
+ cd=soup.select_one("div.content#postContent") or soup.select_one("div.content")
166
+ body=_extract_body(cd) if cd else []
167
+ is_video_page = "/video/" in url
168
+ if is_video_page:
169
+ vid_id = _extract_bdp_video_id(url)
170
+ if vid_id:
171
+ embed = fetch_bdp_embed_data(vid_id)
172
+ if embed and embed.get("mp4"):
173
+ body.insert(0, {"type": "video", "src": embed["mp4"], "poster": embed.get("poster", ""), "vtype": "mp4"})
174
+ if not og_img or "logo" in og_img.lower(): og_img = embed.get("poster", og_img)
175
+ else:
176
+ for iframe in soup.select("iframe"):
177
+ src = iframe.get("src", "")
178
+ if "video-embed" in src:
179
+ vid = _fetch_bdp_video(src)
180
+ if vid: body.insert(0, vid)
181
+ return {"title":h1.get_text(strip=True) if h1 else "","time":te.get_text(strip=True) if te else "",
182
+ "summary":se.get_text(strip=True) if se else "","body":body,"related":_bdp_relates(soup),
183
+ "source_url":url,"source":"bdp","og_image":og_img}
184
+ except Exception as e:
185
+ return {"title":"⚠️ Lỗi","time":"","summary":str(e),"body":[],"related":[],"source_url":url,"source":"bdp","og_image":""}
186
+
187
+ def _fetch_bdp_video(embed_src):
188
+ try:
189
+ full = embed_src if embed_src.startswith("http") else BASE_BDP+embed_src
190
+ html, soup = _get_raw(full)
191
+ video = soup.select_one("video#videoPlayer") or soup.select_one("video")
192
+ if video:
193
+ source = video.find("source")
194
+ if source:
195
+ return {"type":"video","src":source.get("src",""),"poster":video.get("poster",""),"vtype":"mp4"}
196
+ except: pass
197
+ return None
198
+
199
+ # ── 24h.com.vn Scrapers ───────────────────────────────────────────────────────
200
+ def scrape_24h_video_list():
201
+ """Scrape 24h.com.vn video-highlight page for video articles."""
202
+ try:
203
+ url = f"{BASE_24H}/video-highlight-c953.html"
204
+ r = requests.get(url, headers=HEADERS, timeout=15); r.encoding="utf-8"
205
+ soup = BeautifulSoup(r.text, "lxml")
206
+ articles, seen = [], set()
207
+ for a in soup.find_all("a", href=True):
208
+ href = a.get("href","")
209
+ title = a.get("title","") or a.get_text(strip=True)
210
+ if not title or len(title) < 15: continue
211
+ if "javascript:" in href: continue
212
+ if not href.startswith("http"): href = BASE_24H + href
213
+ if href in seen: continue
214
+ if "video" not in href.lower() and "highlight" not in href.lower(): continue
215
+ if href.endswith("-c953.html"): continue # skip category page itself
216
+ seen.add(href)
217
+ img = a.find("img")
218
+ img_src = None
219
+ if img:
220
+ for attr in ["data-original", "data-src", "src"]:
221
+ v = img.get(attr, "")
222
+ if v and "base64" not in v:
223
+ img_src = v; break
224
+ articles.append({"title": title, "link": href, "img": img_src, "summary": "", "time": "",
225
+ "featured": len(articles) < 3, "source": "24h", "group": "24h Video", "is_video": True})
226
+ return articles[:25]
227
+ except Exception as e:
228
+ return []
229
+
230
+ def scrape_24h_article(url):
231
+ """Scrape a 24h.com.vn article - extract m3u8 video URL."""
232
+ try:
233
+ r = requests.get(url, headers=HEADERS, timeout=15); r.encoding="utf-8"
234
+ soup = BeautifulSoup(r.text, "lxml")
235
+ h1 = soup.select_one("h1")
236
+ title = h1.get_text(strip=True) if h1 else ""
237
+ og = soup.find("meta", property="og:image")
238
+ og_img = og.get("content","") if og else ""
239
+ desc_meta = soup.find("meta", property="og:description")
240
+ summary = desc_meta.get("content","") if desc_meta else ""
241
+ # Extract m3u8 video URLs
242
+ m3u8s = re.findall(r'(https?://cdn\.24h\.com\.vn/[^\s"\'\\]+\.m3u8)', r.text)
243
+ videos = [u for u in m3u8s if '_720p' not in u]
244
+ if not videos: videos = m3u8s
245
+ body = []
246
+ for vsrc in videos[:3]:
247
+ body.append({"type": "video", "src": vsrc, "poster": og_img, "vtype": "hls"})
248
+ # Extract text content
249
+ content_selectors = ["div.the-article-body", "div.nws-mainContent", "div.nwsCt",
250
+ "div#ctl00_mainContent_ctl00_divNewsContent", "div.detail-content"]
251
+ cd = None
252
+ for sel in content_selectors:
253
+ cd = soup.select_one(sel)
254
+ if cd: break
255
+ if cd:
256
+ for ch in cd.children:
257
+ if not hasattr(ch,'name') or not ch.name: continue
258
+ if ch.name == "p":
259
+ t = ch.get_text(strip=True)
260
+ if t: body.append({"type":"p","html":strip_links(''.join(str(c) for c in ch.children))})
261
+ elif ch.name in ("h2","h3","h4"):
262
+ body.append({"type":"heading","text":ch.get_text(strip=True)})
263
+ return {"title": title, "time": "", "summary": summary[:200], "body": body,
264
+ "related": [], "source_url": url, "source": "24h", "og_image": og_img}
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):
284
+ try:
285
+ soup=_get(url); articles,seen=[],set()
286
+ for i,it in enumerate(soup.select("article.item-news")):
287
+ a=it.select_one("h2.title-news a") or it.select_one("h3.title-news a") or it.find("a",href=True,title=True)
288
+ if not a: continue
289
+ t=a.get("title","") or a.get_text(strip=True); lk=a.get("href","")
290
+ if not t or len(t)<5 or lk in seen: continue
291
+ im=it.find("img"); img=None
292
+ if im:
293
+ img=im.get("data-src") or im.get("src")
294
+ if img and 'blank' in img:
295
+ src=it.find("source")
296
+ if src: img=src.get("srcset","").split(",")[0].strip().split(" ")[0]
297
+ desc=it.select_one("p.description")
298
+ articles.append({"title":t,"link":lk,"img":img,"summary":desc.get_text(strip=True)[:150] if desc else "",
299
+ "time":"","featured":i==0,"source":"vne","group":"","is_video":False})
300
+ seen.add(lk)
301
+ return articles
302
+ except Exception as e:
303
+ return [{"title":f"⚠️ {e}","link":"#","img":None,"summary":"","time":"","featured":False,"source":"vne","group":"","is_video":False}]
304
+
305
+ def scrape_vne_article(url):
306
+ try:
307
+ soup=_get(url)
308
+ h1=soup.select_one("h1.title-detail"); desc=soup.select_one("p.description"); dt=soup.select_one("span.date")
309
+ og=soup.find("meta",property="og:image"); og_img=og.get("content","") if og else ""
310
+ cd=soup.select_one("article.fck_detail"); body=[]
311
+ if cd:
312
+ for ch in cd.children:
313
+ if not hasattr(ch,'name') or not ch.name: continue
314
+ if ch.name=="p":
315
+ vid=ch.find("video")
316
+ if vid:
317
+ vsrc=vid.get("src","")
318
+ if vsrc:
319
+ vtype="hls" if ("m3u8" in vsrc or "mpegURL" in (vid.get("type","") or "")) else "mp4"
320
+ body.append({"type":"video","src":vsrc,"poster":vid.get("poster",""),"vtype":vtype})
321
+ continue
322
+ im=ch.find("img")
323
+ if im:
324
+ s=im.get("data-src") or im.get("src")
325
+ if s: body.append({"type":"img","src":s,"alt":im.get("alt","")})
326
+ t=ch.get_text(strip=True)
327
+ if t: body.append({"type":"p","html":strip_links(''.join(str(c) for c in ch.children))})
328
+ elif ch.name=="figure":
329
+ vid=ch.find("video")
330
+ if vid:
331
+ vsrc=vid.get("src","")
332
+ cap=ch.find("figcaption")
333
+ if vsrc:
334
+ vtype="hls" if ("m3u8" in vsrc or "mpegURL" in (vid.get("type","") or "")) else "mp4"
335
+ body.append({"type":"video","src":vsrc,"poster":vid.get("poster",""),"vtype":vtype,
336
+ "caption":cap.get_text(strip=True) if cap else ""})
337
+ continue
338
+ im=ch.find("img"); cap=ch.find("figcaption")
339
+ if im:
340
+ s=im.get("data-src") or im.get("src")
341
+ if s: body.append({"type":"img","src":s,"alt":cap.get_text(strip=True) if cap else ""})
342
+ elif ch.name in ("h2","h3","h4"):
343
+ body.append({"type":"heading","text":ch.get_text(strip=True)})
344
+ return {"title":h1.get_text(strip=True) if h1 else "","time":dt.get_text(strip=True) if dt else "",
345
+ "summary":desc.get_text(strip=True) if desc else "","body":body,"related":[],
346
+ "source_url":url,"source":"vne","og_image":og_img}
347
+ except Exception as e:
348
+ return {"title":"⚠️ Lỗi","time":"","summary":str(e),"body":[],"related":[],"source_url":url,"source":"vne","og_image":""}
349
+
350
+ def _extract_body(cd):
351
+ body=[]
352
+ for ch in cd.children:
353
+ if not hasattr(ch,'name') or not ch.name: continue
354
+ if ch.name=="p":
355
+ im=ch.find("img")
356
+ if im:
357
+ s=im.get("src") or im.get("data-src")
358
+ if s: body.append({"type":"img","src":s,"alt":im.get("alt","")})
359
+ t=ch.get_text(strip=True)
360
+ if t: body.append({"type":"p","html":strip_links(''.join(str(c) for c in ch.children))})
361
+ elif ch.name in ("h2","h3","h4"):
362
+ body.append({"type":"heading","text":ch.get_text(strip=True)})
363
+ elif ch.name=="blockquote":
364
+ body.append({"type":"quote","text":ch.get_text(strip=True)})
365
+ return body
366
+
367
+ def _bdp_relates(soup):
368
+ rels=[]
369
+ rd=soup.select_one("div.relates")
370
+ if rd:
371
+ for a in rd.find_all("a",href=True):
372
+ t=a.get_text(strip=True); h=a.get("href","")
373
+ if t and len(t)>5:
374
+ if not h.startswith("http"): h=BASE_BDP+h
375
+ rels.append({"title":t,"link":h})
376
+ return rels[:5]
377
+
378
+ # ── Video aggregator ───────────────────────────────────────────────────────────
379
+ def fetch_latest_videos():
380
+ """Fetch latest videos from BDP carousel + 24h highlights for the homepage slider."""
381
+ videos = []
382
+ # BDP carousel
383
+ try:
384
+ soup = _get(f"{BASE_BDP}/video")
385
+ first_link = None
386
+ for a in soup.find_all("a", href=True):
387
+ href = a.get("href", "")
388
+ if "/video/" in href and re.search(r'-\d{6,}\.html', href):
389
+ first_link = href if href.startswith("http") else BASE_BDP + href; break
390
+ if first_link:
391
+ vid_id = _extract_bdp_video_id(first_link)
392
+ if vid_id:
393
+ embed = fetch_bdp_embed_data(vid_id)
394
+ if embed:
395
+ for rel in embed.get("related", []):
396
+ videos.append({"title": rel["title"], "link": rel["link"], "img": rel["poster"], "source": "bdp"})
397
+ except: pass
398
+ # 24h highlights
399
+ try:
400
+ h24 = scrape_24h_video_list()[:10]
401
+ for a in h24:
402
+ if a.get("img"):
403
+ videos.append({"title": a["title"], "link": a["link"], "img": a["img"], "source": "24h"})
404
+ except: pass
405
+ return videos[:20]
406
+
407
+ def fetch_video_list():
408
+ """Fetch videos from BDP + 24h combined."""
409
+ bdp_videos = scrape_bdp_video_list()
410
+ try:
411
+ h24_videos = scrape_24h_video_list()
412
+ except:
413
+ h24_videos = []
414
+ # Interleave
415
+ combined = []
416
+ bi, hi = 0, 0
417
+ while bi < len(bdp_videos) or hi < len(h24_videos):
418
+ if bi < len(bdp_videos):
419
+ combined.append(bdp_videos[bi]); bi += 1
420
+ if bi < len(bdp_videos):
421
+ combined.append(bdp_videos[bi]); bi += 1
422
+ if hi < len(h24_videos):
423
+ combined.append(h24_videos[hi]); hi += 1
424
+ return combined
425
+
426
+ def fetch_tiktok_feed_videos():
427
+ """Pre-fetch video URLs for TikTok fullscreen feed. Returns list of {title, src, poster, vtype, source, link}."""
428
+ results = []
429
+
430
+ def _fetch_bdp_vid(art):
431
+ vid_id = _extract_bdp_video_id(art["link"])
432
+ if not vid_id: return None
433
+ embed = fetch_bdp_embed_data(vid_id)
434
+ if not embed or not embed.get("mp4"): return None
435
+ return {"title": art["title"], "src": embed["mp4"], "poster": embed.get("poster",""),
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]
446
+ h24_list = scrape_24h_video_list()[:12]
447
+
448
+ with ThreadPoolExecutor(max_workers=8) as ex:
449
+ bdp_futures = {ex.submit(_fetch_bdp_vid, a): a for a in bdp_list}
450
+ h24_futures = {ex.submit(_fetch_24h_vid, a): a for a in h24_list}
451
+ bdp_results, h24_results = [], []
452
+ for f in as_completed(bdp_futures):
453
+ try:
454
+ r = f.result()
455
+ if r: bdp_results.append(r)
456
+ except: pass
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
464
+ bi, hi = 0, 0
465
+ while bi < len(bdp_results) or hi < len(h24_results):
466
+ if bi < len(bdp_results):
467
+ results.append(bdp_results[bi]); bi += 1
468
+ if hi < len(h24_results):
469
+ results.append(h24_results[hi]); hi += 1
470
+ return results[:20]
471
+
472
+ # ── Dispatch ───────────────────────────────────────────────────────────────────
473
+ def fetch_homepage():
474
+ all_articles=[]
475
+ def _fetch(src,url,group):
476
+ arts=scrape_bdp_list(url) if src=="bdp" else scrape_vne_list(url)
477
+ for a in arts: a["group"]=group
478
+ return arts
479
+ with ThreadPoolExecutor(max_workers=5) as ex:
480
+ futures={ex.submit(_fetch,s,u,g):g for s,u,g in HOMEPAGE_SOURCES}
481
+ for f in as_completed(futures):
482
+ try: all_articles.extend(f.result())
483
+ except: pass
484
+ return all_articles
485
+
486
+ def fetch_news_list(category):
487
+ val=CATEGORIES.get(category,list(CATEGORIES.values())[0])
488
+ parts=val.split("::"); src,url_or_key,group=parts[0],parts[1],parts[2]
489
+ if src=="mix" and url_or_key=="home":
490
+ return render_homepage_html(fetch_homepage())
491
+ if src=="mix" and url_or_key=="video":
492
+ return render_video_page_html()
493
+ articles=scrape_bdp_list(url_or_key) if src=="bdp" else scrape_vne_list(url_or_key)
494
+ for a in articles: a["group"]=group
495
+ return render_list_html(articles,group)
496
+
497
+ def read_article(url):
498
+ if not url or url=="#" or len(url)<10: return "<p>Không tìm thấy bài viết.</p>"
499
+ if "vnexpress.net" in url: return render_article_html(scrape_vne_article(url))
500
+ if "24h.com.vn" in url: return render_article_html(scrape_24h_article(url))
501
+ return render_article_html(scrape_bdp_article(url))
502
+
503
+ # ══════════════════════════════════════════════════════════════════════════════
504
+ # HTML RENDERERS
505
+ # ══════════════════════════════════════════════════════════════════════════════
506
+ def render_video_slider_html(videos):
507
+ if not videos: return ""
508
+ items = []
509
+ for i, v in enumerate(videos):
510
+ img = safe_url(v.get("img", ""))
511
+ link = v.get("link", "#")
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>'
519
+ items.append(f'''<div class="vslide-item" onclick="{click_js}">
520
+ <div class="vslide-thumb"><img src="{img}" alt="" class="bdp-lazy-img"><div class="vslide-play">▶</div>{src_badge}</div>
521
+ <p class="vslide-title">{title}</p></div>''')
522
+ return f'''<div class="vslide-wrap">
523
+ <div class="vslide-header"><span class="vslide-label">🎬 Video Mới Nhất</span>
524
+ <div class="vslide-nav"><button class="vslide-btn" onclick="window.bdpSlideScroll(-1)">◀</button>
525
+ <button class="vslide-btn" onclick="window.bdpSlideScroll(1)">▶</button></div></div>
526
+ <div class="vslide-track" id="vslide-track">{''.join(items)}</div></div>'''
527
+
528
+ def render_homepage_html(articles):
529
+ if not articles: return "<p class='bdp-empty'>Không tìm thấy tin tức.</p>"
530
+ now=datetime.now().strftime("%H:%M:%S %d/%m/%Y")
531
+ try: videos = fetch_latest_videos()
532
+ except: videos = []
533
+ slider_html = render_video_slider_html(videos)
534
+ groups={}
535
+ for a in articles: groups.setdefault(a.get("group","Khác"),[]).append(a)
536
+ parts=[f'<div class="bdp-wrap">{slider_html}<div class="bdp-topbar"><span>⏱ {now}</span><span>📰 Tin nổi bật</span></div>']
537
+ for gn in ["Thời Sự","Thế Giới","Kinh Doanh","Công Nghệ","Thể Thao","Giải Trí","Bóng Đá"]:
538
+ arts=groups.get(gn,[])
539
+ if not arts: continue
540
+ feat=[a for a in arts if a.get("featured")][:2]
541
+ reg=[a for a in arts if not a.get("featured")][:4]
542
+ display=feat+reg
543
+ if not display: continue
544
+ parts.append(f'<div class="bdp-section"><h2 class="bdp-section-title">{gn}</h2><div class="bdp-grid bdp-grid-home">')
545
+ for i,art in enumerate(display[:6]): parts.append(_list_card(art,i<len(feat)))
546
+ parts.append('</div></div>')
547
+ parts.append('</div>')
548
+ return '\n'.join(parts)
549
+
550
+ def render_video_page_html():
551
+ """Render video page with fullscreen TikTok feed + grid list."""
552
+ articles = fetch_video_list()
553
+ tiktok_videos = fetch_tiktok_feed_videos()
554
+ now = datetime.now().strftime("%H:%M:%S %d/%m/%Y")
555
+
556
+ # Build TikTok fullscreen feed HTML
557
+ tiktok_html = ""
558
+ if tiktok_videos:
559
+ slides = []
560
+ for vi, v in enumerate(tiktok_videos):
561
+ poster = safe_url(v.get("poster",""))
562
+ poster_attr = f' poster="{poster}"' if poster else ""
563
+ vsrc = v["src"]
564
+ vtype = v.get("vtype","mp4")
565
+ title_esc = esc(v.get("title",""))
566
+ src_label = "24h" if v.get("source")=="24h" else "BDP"
567
+ badge_cls = "bdp-badge-24h" if v.get("source")=="24h" else "bdp-badge-bdp"
568
+ aid = make_id(v.get("link",""))
569
+ sl = slug(v.get("title",""))
570
+ open_js = f"event.stopPropagation();window.bdpOpen('{esc(v.get('link',''))}','{aid}','{sl}')"
571
+ share_js = f"event.stopPropagation();window.bdpShareHash('{title_esc}','{sl}','{aid}')"
572
+
573
+ if vtype == "hls":
574
+ video_tag = f'<video class="tiktok-video" playsinline preload="metadata"{poster_attr} data-hls-src="{vsrc}" muted loop></video>'
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>
584
+ <div class="tiktok-actions">
585
+ <button class="tiktok-action-btn" onclick="{open_js}">📰 Đọc</button>
586
+ <button class="tiktok-action-btn" onclick="{share_js}">📤</button>
587
+ </div>
588
+ </div>
589
+ <div class="tiktok-unmute-hint" onclick="window.bdpTikTokUnmute(this)">🔇 Nhấn để bật tiếng</div>
590
+ <span class="tiktok-counter">{vi+1}/{len(tiktok_videos)}</span>
591
+ </div>''')
592
+
593
+ tiktok_html = f'''<div class="tiktok-fullscreen-container" id="tiktok-fullscreen-feed">
594
+ <div class="tiktok-fullscreen-feed">{''.join(slides)}</div>
595
+ </div>'''
596
+
597
+ # Also render article grid below
598
+ grid_html = ""
599
+ if articles:
600
+ feat=[a for a in articles if a.get("featured")]
601
+ reg=[a for a in articles if not a.get("featured")]
602
+ grid_parts = ['<div class="bdp-wrap">']
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)
614
+
615
+ return tiktok_html + grid_html
616
+
617
+ def render_list_html(articles,group_name=""):
618
+ if not articles: return "<p class='bdp-empty'>Không tìm thấy tin tức.</p>"
619
+ now=datetime.now().strftime("%H:%M:%S %d/%m/%Y")
620
+ feat=[a for a in articles if a.get("featured")]
621
+ reg=[a for a in articles if not a.get("featured")]
622
+ parts=[f'<div class="bdp-wrap"><div class="bdp-topbar"><span>⏱ {now}</span><span>📰 {len(articles)} tin · {group_name}</span></div>']
623
+ if feat:
624
+ parts.append('<div class="bdp-grid bdp-grid-featured">')
625
+ for a in feat[:6]: parts.append(_list_card(a,True))
626
+ parts.append('</div>')
627
+ if reg:
628
+ parts.append('<div class="bdp-grid">')
629
+ for a in reg[:40]: parts.append(_list_card(a,False))
630
+ parts.append('</div>')
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"
638
+ is_video = art.get("is_video", False) or "/video/" in art.get("link","")
639
+ play_overlay = '<div class="bdp-play-overlay">▶</div>' if is_video else ""
640
+ img_html=f'<div class="{c}"><img src="{safe_url(art["img"])}" alt="" class="bdp-lazy-img">{play_overlay}</div>'
641
+ time_html=f'<span class="bdp-card-time">🕐 {art["time"]}</span>' if art.get("time") else ""
642
+ summ_html=f'<p class="bdp-card-summ">{art["summary"][:120]}...</p>' if art.get("summary") and len(art["summary"])>10 else ""
643
+ link=art.get("link","#"); aid=make_id(link)
644
+ tc="bdp-card-title bdp-card-title-big" if big else "bdp-card-title"
645
+ grp=art.get("group",""); badge=""
646
+ if art.get("source")=="vne": badge=f'<span class="bdp-badge bdp-badge-vne">{grp or "VnExpress"}</span>'
647
+ elif art.get("source")=="24h": badge=f'<span class="bdp-badge bdp-badge-24h">{grp or "24h"}</span>'
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}
655
+ <button class="bdp-share-btn" onclick="{share_js}" title="Chia sẻ">📤</button></div></div></div>"""
656
+
657
+ def render_article_html(article):
658
+ aid=make_id(article["source_url"]); sl=slug(article["title"])
659
+ src_url=article.get("source_url","")
660
+ og_img=safe_url(article.get("og_image",""))
661
+ share_js=f"window.bdpShareHash('{esc(article['title'])}','{sl}','{aid}')"
662
+ src_map={"vne":"VnExpress","bdp":"BongDaPlus","24h":"24h.com.vn"}
663
+ src_label=src_map.get(article.get("source",""),"")
664
+ 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>'
665
+
666
+ parts=[f"""{seo}<div class="bdp-article">
667
+ <h1 class="bdp-article-title">{article['title']}</h1>
668
+ <div class="bdp-article-meta"><span>🕐 {article['time']} · {src_label}</span>
669
+ <button class="bdp-share-article-btn" onclick="{share_js}">📤 Chia sẻ</button></div>"""]
670
+ if article.get("summary"):
671
+ parts.append(f'<div class="bdp-article-summary">{article["summary"]}</div>')
672
+
673
+ for item in article.get("body",[]):
674
+ if item["type"]=="video":
675
+ poster=safe_url(item.get("poster",""))
676
+ poster_attr=f' poster="{poster}"' if poster else ""
677
+ caption=item.get("caption","")
678
+ cap_html=f'<p class="bdp-figcap">{caption}</p>' if caption else ""
679
+ vtype=item.get("vtype","mp4")
680
+ vsrc=item["src"]
681
+ if vtype=="hls":
682
+ 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>')
683
+ else:
684
+ 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>')
685
+ elif item["type"]=="img":
686
+ alt=item.get("alt",""); cap=f'<figcaption class="bdp-figcap">{alt}</figcaption>' if alt else ""
687
+ parts.append(f'<figure class="bdp-figure"><img src="{safe_url(item["src"])}" alt="{alt}" class="bdp-lazy-img">{cap}</figure>')
688
+ elif item["type"]=="p":
689
+ parts.append(f'<p class="bdp-article-p">{item["html"]}</p>')
690
+ elif item["type"]=="heading":
691
+ parts.append(f'<h2 class="bdp-article-h2">{item["text"]}</h2>')
692
+ elif item["type"]=="quote":
693
+ parts.append(f'<blockquote class="bdp-quote">{item["text"]}</blockquote>')
694
+ if article.get("related"):
695
+ parts.append('<div class="bdp-related"><h3>📰 Tin liên quan</h3>')
696
+ for rel in article["related"]:
697
+ rid=make_id(rel["link"]); rs=slug(rel["title"])
698
+ parts.append(f'<div class="bdp-related-item" onclick="window.bdpOpen(\'{esc(rel["link"])}\',\'{rid}\',\'{rs}\')"><span>▸ {rel["title"]}</span></div>')
699
+ parts.append('</div>')
700
+ parts.append(f"""<div class="bdp-comments" id="comments-{aid}"><h3>💬 Bình luận</h3>
701
+ <div id="cmt-list-{aid}"></div><div class="bdp-cmt-form">
702
+ <input id="cmt-name-{aid}" class="bdp-cmt-input" placeholder="Tên của bạn..." maxlength="50">
703
+ <textarea id="cmt-text-{aid}" class="bdp-cmt-textarea" placeholder="Viết bình luận..." rows="3" maxlength="500"></textarea>
704
+ <button class="bdp-cmt-submit" onclick="window.bdpAddCmt('{aid}')">Gửi bình luận</button></div></div>""")
705
+ parts.append('</div>')
706
+ return '\n'.join(parts)
707
+
708
+ # ══════════════════════════════════════════════════════════════════════════════
709
+ CSS = """
710
+ body,html{margin:0!important;padding:0!important;overflow-x:hidden;background:#111!important}
711
+ .gradio-container{max-width:100%!important;width:100%!important;margin:0!important;padding:0!important;border-radius:0!important;background:#111!important}
712
+ .main,.contain{max-width:100%!important;width:100%!important;padding:0!important;margin:0!important}
713
+ .gradio-container>.main>.contain{padding-top:0!important}
714
+ .gap{gap:0!important}
715
+ footer,.built-with{display:none!important}
716
+ #article-url-input,#btn-read-article{display:none!important;height:0!important;overflow:hidden!important}
717
+ .bdp-header{background:linear-gradient(135deg,#0d1117,#1a3a2a 50%,#8b7500);padding:14px 16px;text-align:center}
718
+ .bdp-header h1{color:#fff;font-size:20px;margin:0;font-weight:800;text-shadow:0 2px 6px rgba(0,0,0,.4)}
719
+ .bdp-header p{color:rgba(255,255,255,.6);font-size:11px;margin:2px 0 0}
720
+ @media(min-width:768px){.bdp-header h1{font-size:26px}.bdp-header{padding:20px}}
721
+ .controls-row{padding:0 8px!important;background:#111!important}
722
+ .controls-row>div{background:#111!important;border:none!important}
723
+ .controls-row label{color:#aaa!important;font-size:12px!important}
724
+ .controls-row .wrap{border-color:#333!important;background:#1a1a1a!important}
725
+ .controls-row input,.controls-row select,.controls-row button{color:#eee!important}
726
+ .vslide-wrap{margin:8px;background:#1a1a1a;border-radius:12px;overflow:hidden;border:1px solid #2a2a2a}
727
+ .vslide-header{display:flex;justify-content:space-between;align-items:center;padding:10px 14px 6px}
728
+ .vslide-label{color:#f0c040;font-size:15px;font-weight:700}
729
+ .vslide-nav{display:flex;gap:6px}
730
+ .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}
731
+ .vslide-btn:hover{background:#555}
732
+ .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}
733
+ .vslide-track::-webkit-scrollbar{display:none}
734
+ .vslide-item{flex:0 0 200px;scroll-snap-align:start;cursor:pointer;transition:transform .15s}
735
+ .vslide-item:hover{transform:scale(1.03)}
736
+ @media(min-width:768px){.vslide-item{flex:0 0 240px}}
737
+ .vslide-thumb{position:relative;width:100%;aspect-ratio:16/9;border-radius:8px;overflow:hidden;background:#222}
738
+ .vslide-thumb img{width:100%;height:100%;object-fit:cover}
739
+ .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}
740
+ .vslide-item:hover .vslide-play{background:rgba(220,50,50,.8)}
741
+ .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}
742
+ .bdp-wrap{padding:8px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
743
+ .bdp-topbar{display:flex;justify-content:space-between;padding:4px 4px 8px;color:#666;font-size:11px}
744
+ .bdp-empty{text-align:center;color:#666;padding:60px 20px}
745
+ .bdp-section{margin-bottom:16px}
746
+ .bdp-section-title{font-size:16px;font-weight:700;color:#5cb87a;margin:4px 0 8px;border-left:3px solid #5cb87a;padding-left:8px}
747
+ @media(min-width:768px){.bdp-section-title{font-size:18px}}
748
+ .bdp-grid{display:grid;grid-template-columns:1fr;gap:8px}
749
+ .bdp-grid-featured,.bdp-grid-home{margin-bottom:4px}
750
+ @media(min-width:420px){.bdp-grid{grid-template-columns:repeat(2,1fr)}}
751
+ @media(min-width:768px){.bdp-grid{grid-template-columns:repeat(3,1fr);gap:10px}}
752
+ @media(min-width:1100px){.bdp-grid{grid-template-columns:repeat(4,1fr)}}
753
+ .bdp-card{background:#1a1a1a;border-radius:10px;overflow:hidden;cursor:pointer;transition:transform .15s,box-shadow .15s;border:1px solid #222}
754
+ .bdp-card:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.5)}
755
+ .bdp-card:active{transform:scale(.98)}
756
+ .bdp-card-img{width:100%;height:130px;overflow:hidden;background:#222;position:relative}
757
+ .bdp-card-img-big{height:170px}
758
+ .bdp-card-img img{width:100%;height:100%;object-fit:cover}
759
+ @media(min-width:768px){.bdp-card-img{height:150px}.bdp-card-img-big{height:190px}}
760
+ .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}
761
+ .bdp-card-body{padding:8px 10px 6px}
762
+ .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}
763
+ .bdp-card-title-big{font-size:14.5px}
764
+ @media(min-width:768px){.bdp-card-title{font-size:13.5px}.bdp-card-title-big{font-size:15px}}
765
+ .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}
766
+ .bdp-card-footer{display:flex;justify-content:space-between;align-items:center;margin-top:6px}
767
+ .bdp-card-time{color:#555;font-size:10.5px}
768
+ .bdp-share-btn{background:none;border:none;cursor:pointer;font-size:15px;padding:3px 5px;border-radius:6px;transition:background .15s;color:#777}
769
+ .bdp-share-btn:hover{background:#333}
770
+ .bdp-badge{font-size:9px;padding:1px 6px;border-radius:3px;font-weight:700;display:inline-block;margin-bottom:4px}
771
+ .bdp-badge-vne{background:#c0392b;color:#fff}
772
+ .bdp-badge-bdp{background:#1a5c35;color:#fff}
773
+ .bdp-badge-24h{background:#e67e22;color:#fff}
774
+ .bdp-article{padding:14px 12px 30px;max-width:720px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
775
+ @media(min-width:768px){.bdp-article{padding:20px 16px 50px}}
776
+ .bdp-article-title{font-size:21px;font-weight:800;color:#f0f0f0;line-height:1.3;margin:0 0 8px}
777
+ @media(min-width:768px){.bdp-article-title{font-size:27px}}
778
+ .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}
779
+ .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}
780
+ .bdp-share-article-btn:hover{opacity:.85}
781
+ .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}
782
+ .bdp-article-p{font-size:15.5px;line-height:1.75;color:#ccc;margin:0 0 12px}
783
+ @media(min-width:768px){.bdp-article-p{font-size:16.5px}}
784
+ .bdp-article-h2{font-size:19px;font-weight:700;color:#eee;margin:24px 0 10px}
785
+ .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}
786
+ .bdp-figure{margin:14px 0;text-align:center}
787
+ .bdp-figure img{max-width:100%;height:auto;border-radius:8px}
788
+ .bdp-figcap{color:#666;font-size:11.5px;margin-top:4px;font-style:italic}
789
+ .bdp-video-wrap{margin:14px 0;border-radius:10px;overflow:hidden;background:#000}
790
+ .bdp-video{width:100%;max-height:70vh;display:block;border-radius:10px}
791
+ .bdp-related{margin-top:24px;padding-top:14px;border-top:1px solid #2a2a2a}
792
+ .bdp-related h3{font-size:16px;color:#eee;margin:0 0 8px}
793
+ .bdp-related-item{padding:8px 10px;margin-bottom:5px;border:1px solid #262626;border-radius:8px;cursor:pointer;transition:background .15s}
794
+ .bdp-related-item:hover{background:#222}
795
+ .bdp-related-item span{font-size:13.5px;color:#5cb87a;font-weight:500}
796
+ .bdp-comments{margin-top:28px;padding-top:16px;border-top:1px solid #2a2a2a}
797
+ .bdp-comments h3{font-size:16px;color:#eee;margin:0 0 10px}
798
+ .bdp-cmt-item{background:#1a1a1a;border:1px solid #262626;border-radius:8px;padding:10px 12px;margin-bottom:8px}
799
+ .bdp-cmt-author{font-weight:700;color:#5cb87a;font-size:13px}
800
+ .bdp-cmt-date{color:#555;font-size:11px;margin-left:8px}
801
+ .bdp-cmt-body{color:#ccc;font-size:14px;margin-top:4px;line-height:1.5}
802
+ .bdp-cmt-form{margin-top:12px}
803
+ .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}
804
+ .bdp-cmt-input{margin-bottom:6px}
805
+ .bdp-cmt-textarea{resize:vertical}
806
+ .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}
807
+ .bdp-cmt-submit:hover{opacity:.85}
808
+ .bdp-cmt-empty{color:#555;font-size:13px;font-style:italic;padding:8px 0}
809
+ .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}
810
+ .bdp-toast.show{opacity:1}
811
+ .gr-group,.gr-box,.gr-panel{background:#111!important;border:none!important}
812
+ .label-wrap{background:#111!important}
813
+
814
+ /* ══ TikTok Fullscreen Video Feed ══ */
815
+ .tiktok-fullscreen-container{width:100%;background:#000;position:relative;height:calc(100vh - 60px);max-height:900px;min-height:500px;border-radius:0}
816
+ .tiktok-fullscreen-feed{height:100%;overflow-y:scroll;scroll-snap-type:y mandatory;-webkit-overflow-scrolling:touch;scrollbar-width:none}
817
+ .tiktok-fullscreen-feed::-webkit-scrollbar{display:none}
818
+ .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}
819
+ .tiktok-video{width:100%;height:100%;object-fit:contain;display:block}
820
+ .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}
821
+ .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}
822
+ .tiktok-actions{display:flex;gap:8px}
823
+ .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}
824
+ .tiktok-action-btn:hover{background:rgba(255,255,255,.3)}
825
+ .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)}
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
+ """
832
+
833
+ # ══════════════════════════════════════════════════════════════════════════════
834
+ HEAD_META = """
835
+ <meta name="description" content="Tin tức tổng hợp nhanh nhất - VnExpress, BongDaPlus, 24h">
836
+ <meta property="og:title" content="Tin Tức Việt Nam - Tổng Hợp">
837
+ <meta property="og:description" content="Thời sự, thế giới, kinh doanh, công nghệ, thể thao, giải trí.">
838
+ <meta property="og:type" content="website">
839
+ <meta property="og:image" content="https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg">
840
+ <meta name="twitter:card" content="summary_large_image">
841
+ <link rel="canonical" href="https://bep40-bongdaplus-news.hf.space">
842
+ """
843
+
844
+ JS_FUNC = """
845
+ function() {
846
+ var s=document.createElement('script');s.src='https://cdn.jsdelivr.net/npm/hls.js@1/dist/hls.min.js';document.head.appendChild(s);
847
+ var SPACE_BASE='""" + SPACE_URL + """';
848
+
849
+ window.bdpOpen=function(url,aid,sl){
850
+ window.location.hash='#/'+sl+'/'+aid;
851
+ try{localStorage.setItem('bdp_url_'+aid,url);}catch(e){}
852
+ var el=document.getElementById('article-url-input');
853
+ if(el){var ta=el.querySelector('textarea');if(ta){ta.value=url;ta.dispatchEvent(new Event('input',{bubbles:true}));}}
854
+ var btn=document.getElementById('btn-read-article');
855
+ if(btn){var b=btn.querySelector('button');if(b)b.click();else btn.click();}
856
+ window.scrollTo({top:0,behavior:'smooth'});
857
+ };
858
+
859
+ window.bdpShareHash=async function(title,sl,aid){
860
+ var hashUrl=SPACE_BASE+'/#/'+sl+'/'+aid;
861
+ var sd={title:title,url:hashUrl,text:title};
862
+ if(navigator.share){try{await navigator.share(sd);return;}catch(e){if(e.name==='AbortError')return;}}
863
+ if(navigator.clipboard&&navigator.clipboard.writeText){try{await navigator.clipboard.writeText(hashUrl);window.bdpToast('Đã sao chép liên kết!');return;}catch(e){}}
864
+ var ta=document.createElement('textarea');ta.value=hashUrl;ta.style.cssText='position:fixed;opacity:0';
865
+ document.body.appendChild(ta);ta.focus();ta.select();
866
+ try{document.execCommand('copy');window.bdpToast('Đã sao chép liên kết!');}catch(e){window.prompt('Sao chép:',hashUrl);}
867
+ document.body.removeChild(ta);
868
+ };
869
+
870
+ 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);};
871
+
872
+ window.bdpSlideScroll=function(dir){
873
+ var track=document.getElementById('vslide-track');
874
+ if(track){track.scrollBy({left:dir*260,behavior:'smooth'});}
875
+ };
876
+
877
+ function gc(a){try{return JSON.parse(localStorage.getItem('bdp_cmt_'+a))||[];}catch(e){return[];}}
878
+ function sc(a,c){try{localStorage.setItem('bdp_cmt_'+a,JSON.stringify(c));}catch(e){}}
879
+ 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,'&lt;').replace(/>/g,'&gt;')+'</div></div>';}l.innerHTML=h;};
880
+ 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!');};
881
+
882
+ function initHlsVideo(v){
883
+ if(v._hlsInit) return; v._hlsInit=true;
884
+ var src=v.getAttribute('data-hls-src'); if(!src) return;
885
+ if(v.canPlayType('application/vnd.apple.mpegURL')){v.src=src;}
886
+ 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;}});}
887
+ else{v.src=src;}
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');
894
+ if(!slide) return;
895
+ var video=slide.querySelector('.tiktok-video');
896
+ if(!video) return;
897
+ video.muted=!video.muted;
898
+ var isMuted=video.muted;
899
+ hint.textContent=isMuted?'🔇 Nhấn để bật tiếng':'🔊 Đang phát tiếng';
900
+ if(feed){
901
+ feed.querySelectorAll('.tiktok-video').forEach(function(v){v.muted=isMuted;});
902
+ feed.querySelectorAll('.tiktok-unmute-hint').forEach(function(h){
903
+ h.textContent=isMuted?'🔇 Nhấn để bật tiếng':'🔊 Đang phát tiếng';
904
+ });
905
+ }
906
+ };
907
+
908
+ function initTikTokFullscreen(container){
909
+ if(container._tikInit) return;
910
+ container._tikInit=true;
911
+ var feed=container.querySelector('.tiktok-fullscreen-feed');
912
+ if(!feed) return;
913
+ var slides=feed.querySelectorAll('.tiktok-slide');
914
+ if(!slides.length) return;
915
+
916
+ /* Init all HLS videos first */
917
+ slides.forEach(function(sl){
918
+ var v=sl.querySelector('video[data-hls-src]');
919
+ if(v) initHlsVideo(v);
920
+ var v2=sl.querySelector('video:not([data-hls-src])');
921
+ if(v2 && !v2._initDone){v2._initDone=true;v2.load();}
922
+ });
923
+
924
+ var currentIdx=-1;
925
+
926
+ function tryPlay(v){
927
+ var p=v.play();
928
+ if(p&&p.catch) p.catch(function(){setTimeout(function(){v.play().catch(function(){});},500);});
929
+ }
930
+
931
+ function activateSlide(idx){
932
+ if(idx===currentIdx) return;
933
+ slides.forEach(function(sl,i){
934
+ var v=sl.querySelector('.tiktok-video');
935
+ if(!v) return;
936
+ if(i===idx){
937
+ v.currentTime=0;
938
+ tryPlay(v);
939
+ sl.classList.remove('paused');
940
+ } else {
941
+ v.pause();
942
+ sl.classList.remove('paused');
943
+ }
944
+ });
945
+ currentIdx=idx;
946
+ }
947
+
948
+ /* Use scroll event instead of IntersectionObserver for reliability inside Gradio */
949
+ var scrollTimer=null;
950
+ feed.addEventListener('scroll',function(){
951
+ if(scrollTimer) clearTimeout(scrollTimer);
952
+ scrollTimer=setTimeout(function(){
953
+ var feedRect=feed.getBoundingClientRect();
954
+ var feedCenter=feedRect.top+feedRect.height/2;
955
+ var best=-1,bestDist=99999;
956
+ slides.forEach(function(sl,i){
957
+ var r=sl.getBoundingClientRect();
958
+ var center=r.top+r.height/2;
959
+ var dist=Math.abs(center-feedCenter);
960
+ if(dist<bestDist){bestDist=dist;best=i;}
961
+ });
962
+ if(best>=0) activateSlide(best);
963
+ },150);
964
+ });
965
+
966
+ /* Start first video after short delay for HLS init */
967
+ setTimeout(function(){activateSlide(0);},800);
968
+
969
+ /* Tap to pause/play */
970
+ slides.forEach(function(sl){
971
+ var v=sl.querySelector('.tiktok-video');
972
+ if(v){
973
+ v.addEventListener('click',function(e){
974
+ e.preventDefault();
975
+ if(v.paused){tryPlay(v);sl.classList.remove('paused');}
976
+ else{v.pause();sl.classList.add('paused');}
977
+ });
978
+ }
979
+ });
980
+ }
981
+
982
+ new MutationObserver(function(muts){muts.forEach(function(m){m.addedNodes.forEach(function(n){
983
+ if(!n.querySelectorAll) return;
984
+ n.querySelectorAll('img.bdp-lazy-img').forEach(function(img){if(!img._errBound){img._errBound=true;img.addEventListener('error',function(){this.style.display='none';});}});
985
+ n.querySelectorAll('video[data-hls-src]').forEach(initHlsVideo);
986
+ n.querySelectorAll('video.bdp-video:not([data-hls-src])').forEach(function(v){if(!v._initDone){v._initDone=true;v.load();}});
987
+ n.querySelectorAll('[id^="cmt-list-"]').forEach(function(d){window.bdpRenderCmt(d.id.replace('cmt-list-',''));});
988
+ n.querySelectorAll('.tiktok-fullscreen-container').forEach(initTikTokFullscreen);
989
+ if(n.classList && n.classList.contains('tiktok-fullscreen-container')) initTikTokFullscreen(n);
990
+ });});}).observe(document.body,{childList:true,subtree:true});
991
+
992
+ setInterval(function(){
993
+ document.querySelectorAll('video[data-hls-src]').forEach(initHlsVideo);
994
+ document.querySelectorAll('video.bdp-video:not([data-hls-src])').forEach(function(v){if(!v._initDone){v._initDone=true;v.load();}});
995
+ document.querySelectorAll('.tiktok-fullscreen-container').forEach(initTikTokFullscreen);
996
+ },1500);
997
+
998
+ var hh=window.location.hash;
999
+ 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){}}}
1000
+ }
1001
+ """
1002
+
1003
+ # ══════════════════════════════════════════════════════════════════════════════
1004
+ 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:
1005
+ 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>')
1006
+ article_url=gr.Textbox(value="",visible=False,elem_id="article-url-input")
1007
+ with gr.Row(elem_classes=["controls-row"]):
1008
+ cat=gr.Dropdown(choices=list(CATEGORIES.keys()),value="🏠 Trang Chủ (Nổi Bật)",label="Chuyên mục",scale=3,interactive=True)
1009
+ ref_btn=gr.Button("🔄 Làm mới",variant="primary",scale=1)
1010
+ back_btn=gr.Button("← Quay lại",variant="secondary",scale=1,visible=False)
1011
+ news_list=gr.HTML()
1012
+ article_view=gr.HTML(visible=False)
1013
+ read_btn=gr.Button("Đọc",visible=False,elem_id="btn-read-article")
1014
+ def show_article(url):
1015
+ if not url or url=="#" or len(url)<10:
1016
+ return gr.update(visible=True),gr.update(visible=False),gr.update(visible=False),gr.update(visible=False),""
1017
+ return (gr.update(visible=False),gr.update(value=read_article(url),visible=True),gr.update(visible=False),gr.update(visible=True),"")
1018
+ def show_list(c):
1019
+ return (gr.update(value=fetch_news_list(c),visible=True),gr.update(visible=False),gr.update(visible=True),gr.update(visible=False))
1020
+ read_btn.click(fn=show_article,inputs=[article_url],outputs=[news_list,article_view,ref_btn,back_btn,article_url])
1021
+ back_btn.click(fn=show_list,inputs=[cat],outputs=[news_list,article_view,ref_btn,back_btn])
1022
+ ref_btn.click(fn=show_list,inputs=[cat],outputs=[news_list,article_view,ref_btn,back_btn])
1023
+ cat.change(fn=show_list,inputs=[cat],outputs=[news_list,article_view,ref_btn,back_btn])
1024
+ timer=gr.Timer(value=REFRESH_SECONDS,active=True)
1025
+ timer.tick(fn=fetch_news_list,inputs=cat,outputs=news_list)
1026
+ demo.load(fn=fetch_news_list,inputs=cat,outputs=news_list)
1027
+
1028
+ if __name__=="__main__":
1029
+ demo.launch()