bep40 commited on
Commit
8fc079e
·
verified ·
1 Parent(s): 7b6fe03

feat: inline video player (VnExpress MP4 + BDP video-embed), OG image in share links, dynamic meta tags

Browse files
Files changed (1) hide show
  1. app.py +1 -458
app.py CHANGED
@@ -1,458 +1 @@
1
- import gradio as gr
2
- import requests
3
- import re
4
- import hashlib
5
- from bs4 import BeautifulSoup
6
- from datetime import datetime
7
- from concurrent.futures import ThreadPoolExecutor, as_completed
8
-
9
- HEADERS = {
10
- "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",
11
- "Accept-Language": "vi-VN,vi;q=0.9,en;q=0.8",
12
- }
13
- BASE_BDP = "https://bongdaplus.vn"
14
- REFRESH_SECONDS = 300
15
-
16
- CATEGORIES = {
17
- "🏠 Trang Chủ (Nổi Bật)": "mix::home::Trang Chủ",
18
- "📰 Thời Sự": "vne::https://vnexpress.net/thoi-su::Thời Sự",
19
- "🌍 Thế Giới": "vne::https://vnexpress.net/the-gioi::Thế Giới",
20
- "💰 Kinh Doanh": "vne::https://vnexpress.net/kinh-doanh::Kinh Doanh",
21
- "💻 Công Nghệ": "vne::https://vnexpress.net/so-hoa::Công Nghệ",
22
- "🔬 Khoa Học": "vne::https://vnexpress.net/khoa-hoc::Khoa Học",
23
- "🎬 Giải Trí": "vne::https://vnexpress.net/giai-tri::Giải Trí",
24
- "🏥 Sức Khỏe": "vne::https://vnexpress.net/suc-khoe::Sức Khỏe",
25
- "🎓 Giáo Dục": "vne::https://vnexpress.net/giao-duc::Giáo Dục",
26
- "✈️ Du Lịch": "vne::https://vnexpress.net/du-lich::Du Lịch",
27
- "⚽ Thể Thao": "vne::https://vnexpress.net/the-thao::Thể Thao",
28
- "⚽ Bóng Đá QT": "vne::https://vnexpress.net/the-thao/bong-da::Bóng Đá",
29
- "🏴󠁧󠁢󠁥󠁮󠁧󠁿 Ngoại Hạng Anh": "bdp::https://bongdaplus.vn/ngoai-hang-anh::Bóng Đá",
30
- "🇪🇸 La Liga": "bdp::https://bongdaplus.vn/la-liga::Bóng Đá",
31
- "🏆 Champions League": "bdp::https://bongdaplus.vn/champions-league-cup-c1::Bóng Đá",
32
- "🇻🇳 Bóng Đá VN": "bdp::https://bongdaplus.vn/bong-da-viet-nam::Bóng Đá",
33
- "🔄 Chuyển Nhượng": "bdp::https://bongdaplus.vn/tin-chuyen-nhuong::Bóng Đá",
34
- }
35
-
36
- HOMEPAGE_SOURCES = [
37
- ("vne", "https://vnexpress.net/thoi-su", "Thời Sự"),
38
- ("vne", "https://vnexpress.net/the-gioi", "Thế Giới"),
39
- ("vne", "https://vnexpress.net/kinh-doanh", "Kinh Doanh"),
40
- ("vne", "https://vnexpress.net/so-hoa", "Công Nghệ"),
41
- ("vne", "https://vnexpress.net/the-thao", "Thể Thao"),
42
- ("vne", "https://vnexpress.net/giai-tri", "Giải Trí"),
43
- ("bdp", "https://bongdaplus.vn/tin-moi", "Bóng Đá"),
44
- ]
45
-
46
- def strip_links(html):
47
- return re.sub(r'</a>', '', re.sub(r'<a\s[^>]*>', '', html))
48
-
49
- def esc(text):
50
- return text.replace("\\", "\\\\").replace("'", "\\'").replace('"', '\\"').replace("\n", " ")
51
-
52
- def make_id(url):
53
- return hashlib.md5(url.encode()).hexdigest()[:12]
54
-
55
- def slug(text):
56
- s = text.lower().strip()
57
- s = re.sub(r'[àáạảãâầấậẩẫăằắặẳẵ]', 'a', s)
58
- s = re.sub(r'[èéẹẻẽêềếệểễ]', 'e', s)
59
- s = re.sub(r'[ìíịỉĩ]', 'i', s)
60
- s = re.sub(r'[òóọỏõôồốộổỗơờớợởỡ]', 'o', s)
61
- s = re.sub(r'[ùúụủũưừứựửữ]', 'u', s)
62
- s = re.sub(r'[ỳýỵỷỹ]', 'y', s)
63
- s = re.sub(r'[đ]', 'd', s)
64
- s = re.sub(r'[^a-z0-9\s-]', '', s)
65
- s = re.sub(r'[\s-]+', '-', s).strip('-')
66
- return s[:60]
67
-
68
- def scrape_bdp_list(url):
69
- try:
70
- r = requests.get(url, headers=HEADERS, timeout=15); r.encoding = "utf-8"
71
- soup = BeautifulSoup(r.text, "lxml")
72
- articles, seen = [], set()
73
- for sel, feat in [("div.news.fst", True), ("div.sld-itm.news", True), ("li.news", False)]:
74
- for item in soup.select(sel):
75
- art = _parse_bdp(item, feat)
76
- if art and art["link"] not in seen:
77
- articles.append(art); seen.add(art["link"])
78
- return articles
79
- except Exception as e:
80
- return [_err(f"BDP: {e}")]
81
-
82
- def _parse_bdp(item, featured):
83
- tag = item.find("a", class_="title") or item.find("a", href=True)
84
- if not tag: return None
85
- title = tag.get_text(strip=True)
86
- if not title or len(title) < 5: return None
87
- link = tag.get("href", "")
88
- if link and not link.startswith("http"): link = BASE_BDP + link
89
- img_tag = item.find("img")
90
- img = (img_tag.get("data-src") or img_tag.get("src")) if img_tag else None
91
- summ = item.find("p", class_="summ")
92
- tt = item.find("div", class_="in-time")
93
- return {"title": title, "link": link, "img": img, "summary": summ.get_text(strip=True) if summ else "",
94
- "time": tt.get_text(strip=True) if tt else "", "featured": featured, "source": "bdp", "group": ""}
95
-
96
- def scrape_bdp_article(url):
97
- try:
98
- r = requests.get(url, headers=HEADERS, timeout=15); r.encoding = "utf-8"
99
- soup = BeautifulSoup(r.text, "lxml")
100
- h1 = soup.select_one(".lead-title h1") or soup.select_one("h1")
101
- te = soup.select_one(".emobar .rgt"); se = soup.select_one("div.summary")
102
- cd = soup.select_one("div.content#postContent") or soup.select_one("div.content")
103
- og_img = ""; og = soup.find("meta", property="og:image")
104
- if og: og_img = og.get("content", "")
105
- body = _extract_body(cd) if cd else []
106
- return {"title": h1.get_text(strip=True) if h1 else "", "time": te.get_text(strip=True) if te else "",
107
- "summary": se.get_text(strip=True) if se else "", "body": body, "related": _bdp_relates(soup),
108
- "source_url": url, "source": "bdp", "og_image": og_img}
109
- except Exception as e:
110
- return _art_err(e, url, "bdp")
111
-
112
- def scrape_vne_list(url):
113
- try:
114
- r = requests.get(url, headers=HEADERS, timeout=15); r.encoding = "utf-8"
115
- soup = BeautifulSoup(r.text, "lxml")
116
- articles, seen = [], set()
117
- for i, item in enumerate(soup.select("article.item-news")):
118
- a = item.select_one("h2.title-news a") or item.select_one("h3.title-news a") or item.find("a", href=True, title=True)
119
- if not a: continue
120
- title = a.get("title", "") or a.get_text(strip=True)
121
- link = a.get("href", "")
122
- if not title or len(title) < 5 or link in seen: continue
123
- img = _vne_img(item)
124
- desc = item.select_one("p.description")
125
- articles.append({"title": title, "link": link, "img": img,
126
- "summary": desc.get_text(strip=True)[:150] if desc else "",
127
- "time": "", "featured": i == 0, "source": "vne", "group": ""})
128
- seen.add(link)
129
- return articles
130
- except Exception as e:
131
- return [_err(f"VNE: {e}")]
132
-
133
- def _vne_img(item):
134
- img_tag = item.find("img")
135
- if not img_tag: return None
136
- img = img_tag.get("data-src") or img_tag.get("src")
137
- if img and 'blank' in img:
138
- src = item.find("source")
139
- if src: img = src.get("srcset", "").split(",")[0].strip().split(" ")[0]
140
- return img
141
-
142
- def scrape_vne_article(url):
143
- try:
144
- r = requests.get(url, headers=HEADERS, timeout=15); r.encoding = "utf-8"
145
- soup = BeautifulSoup(r.text, "lxml")
146
- h1 = soup.select_one("h1.title-detail"); desc = soup.select_one("p.description")
147
- dt = soup.select_one("span.date"); cd = soup.select_one("article.fck_detail")
148
- og_img = ""; og = soup.find("meta", property="og:image")
149
- if og: og_img = og.get("content", "")
150
- body = []
151
- if cd:
152
- for ch in cd.children:
153
- if not hasattr(ch, 'name') or not ch.name: continue
154
- if ch.name == "p":
155
- im = ch.find("img")
156
- if im:
157
- s = im.get("data-src") or im.get("src")
158
- if s: body.append({"type": "img", "src": s, "alt": im.get("alt", "")})
159
- t = ch.get_text(strip=True)
160
- if t: body.append({"type": "p", "html": strip_links(''.join(str(c) for c in ch.children))})
161
- elif ch.name == "figure":
162
- im = ch.find("img"); cap = ch.find("figcaption")
163
- if im:
164
- s = im.get("data-src") or im.get("src")
165
- if s: body.append({"type": "img", "src": s, "alt": cap.get_text(strip=True) if cap else ""})
166
- elif ch.name in ("h2", "h3", "h4"):
167
- body.append({"type": "heading", "text": ch.get_text(strip=True)})
168
- return {"title": h1.get_text(strip=True) if h1 else "", "time": dt.get_text(strip=True) if dt else "",
169
- "summary": desc.get_text(strip=True) if desc else "", "body": body, "related": [],
170
- "source_url": url, "source": "vne", "og_image": og_img}
171
- except Exception as e:
172
- return _art_err(e, url, "vne")
173
-
174
- def _extract_body(cd):
175
- body = []
176
- for ch in cd.children:
177
- if not hasattr(ch, 'name') or not ch.name: continue
178
- if ch.name == "p":
179
- im = ch.find("img")
180
- if im:
181
- s = im.get("src") or im.get("data-src")
182
- if s: body.append({"type": "img", "src": s, "alt": im.get("alt", "")})
183
- t = ch.get_text(strip=True)
184
- if t: body.append({"type": "p", "html": strip_links(''.join(str(c) for c in ch.children))})
185
- elif ch.name in ("h2", "h3", "h4"):
186
- body.append({"type": "heading", "text": ch.get_text(strip=True)})
187
- elif ch.name == "blockquote":
188
- body.append({"type": "quote", "text": ch.get_text(strip=True)})
189
- return body
190
-
191
- def _bdp_relates(soup):
192
- rels = []
193
- rd = soup.select_one("div.relates")
194
- if rd:
195
- for a in rd.find_all("a", href=True):
196
- t = a.get_text(strip=True); h = a.get("href", "")
197
- if t and len(t) > 5:
198
- if not h.startswith("http"): h = BASE_BDP + h
199
- rels.append({"title": t, "link": h})
200
- return rels[:5]
201
-
202
- def _err(msg):
203
- return {"title": f"⚠️ {msg}", "link": "#", "img": None, "summary": "", "time": "", "featured": False, "source": "", "group": ""}
204
-
205
- def _art_err(e, url, src):
206
- return {"title": "⚠️ Lỗi", "time": "", "summary": str(e), "body": [], "related": [], "source_url": url, "source": src, "og_image": ""}
207
-
208
- def fetch_homepage():
209
- all_articles = []
210
- def _fetch(src, url, group):
211
- arts = scrape_bdp_list(url) if src == "bdp" else scrape_vne_list(url)
212
- for a in arts: a["group"] = group
213
- return arts
214
- with ThreadPoolExecutor(max_workers=5) as ex:
215
- futures = {ex.submit(_fetch, s, u, g): g for s, u, g in HOMEPAGE_SOURCES}
216
- for f in as_completed(futures):
217
- try: all_articles.extend(f.result())
218
- except: pass
219
- return all_articles
220
-
221
- def fetch_news_list(category):
222
- val = CATEGORIES.get(category, list(CATEGORIES.values())[0])
223
- parts = val.split("::"); src, url_or_key, group = parts[0], parts[1], parts[2]
224
- if src == "mix":
225
- return render_homepage_html(fetch_homepage())
226
- articles = scrape_bdp_list(url_or_key) if src == "bdp" else scrape_vne_list(url_or_key)
227
- for a in articles: a["group"] = group
228
- return render_list_html(articles, group)
229
-
230
- def read_article(url):
231
- if not url or url == "#" or len(url) < 10: return "<p>Không tìm thấy bài viết.</p>"
232
- if "vnexpress.net" in url: return render_article_html(scrape_vne_article(url))
233
- return render_article_html(scrape_bdp_article(url))
234
-
235
- def render_homepage_html(articles):
236
- if not articles: return "<p class='bdp-empty'>Không tìm thấy tin tức.</p>"
237
- now = datetime.now().strftime("%H:%M:%S %d/%m/%Y")
238
- groups = {}
239
- for a in articles: groups.setdefault(a.get("group", "Khác"), []).append(a)
240
- parts = [f'<div class="bdp-wrap"><div class="bdp-topbar"><span>⏱ {now}</span><span>📰 Tin nổi bật từ nhiều nguồn</span></div>']
241
- for gn in ["Thời Sự", "Thế Giới", "Kinh Doanh", "Công Nghệ", "Thể Thao", "Giải Trí", "Bóng Đá"]:
242
- arts = groups.get(gn, [])
243
- if not arts: continue
244
- feat = [a for a in arts if a.get("featured")][:2]
245
- reg = [a for a in arts if not a.get("featured")][:4]
246
- display = feat + reg
247
- if not display: continue
248
- parts.append(f'<div class="bdp-section"><h2 class="bdp-section-title">{gn}</h2><div class="bdp-grid bdp-grid-home">')
249
- for i, art in enumerate(display[:6]): parts.append(_list_card(art, i < len(feat)))
250
- parts.append('</div></div>')
251
- parts.append('</div>')
252
- return '\n'.join(parts)
253
-
254
- def render_list_html(articles, group_name=""):
255
- if not articles: return "<p class='bdp-empty'>Không tìm thấy tin tức.</p>"
256
- now = datetime.now().strftime("%H:%M:%S %d/%m/%Y")
257
- featured = [a for a in articles if a.get("featured")]
258
- regular = [a for a in articles if not a.get("featured")]
259
- parts = [f'<div class="bdp-wrap"><div class="bdp-topbar"><span>⏱ {now}</span><span>📰 {len(articles)} tin · {group_name}</span></div>']
260
- if featured:
261
- parts.append('<div class="bdp-grid bdp-grid-featured">')
262
- for art in featured[:6]: parts.append(_list_card(art, True))
263
- parts.append('</div>')
264
- if regular:
265
- parts.append('<div class="bdp-grid">')
266
- for art in regular[:40]: parts.append(_list_card(art, False))
267
- parts.append('</div>')
268
- parts.append('</div>')
269
- return '\n'.join(parts)
270
-
271
- def _list_card(art, big):
272
- img_html = ""
273
- if art.get("img"):
274
- c = "bdp-card-img bdp-card-img-big" if big else "bdp-card-img"
275
- img_html = f'<div class="{c}"><img src="{art["img"]}" alt="{esc(art["title"])}" onerror="this.parentElement.style.display=\'none\'"></div>'
276
- time_html = f'<span class="bdp-card-time">🕐 {art["time"]}</span>' if art.get("time") else ""
277
- summ_html = f'<p class="bdp-card-summ">{art["summary"][:120]}...</p>' if art.get("summary") and len(art["summary"]) > 10 else ""
278
- link = art.get("link", "#"); aid = make_id(link)
279
- tc = "bdp-card-title bdp-card-title-big" if big else "bdp-card-title"
280
- grp = art.get("group", "")
281
- badge = ""
282
- if art.get("source") == "vne": badge = f'<span class="bdp-badge bdp-badge-vne">{grp or "VnExpress"}</span>'
283
- elif art.get("source") == "bdp": badge = f'<span class="bdp-badge bdp-badge-bdp">{grp or "BongDaPlus"}</span>'
284
- title_slug = slug(art["title"])
285
- share_js = f"event.stopPropagation();window.bdpShare('{esc(art['title'])}','{aid}','{title_slug}')"
286
- click_js = f"window.bdpOpen('{esc(link)}','{aid}','{title_slug}')"
287
- return f"""<div class="bdp-card" onclick="{click_js}">
288
- {img_html}<div class="bdp-card-body">{badge}<h3 class="{tc}">{art['title']}</h3>
289
- {summ_html}<div class="bdp-card-footer">{time_html}
290
- <button class="bdp-share-btn" onclick="{share_js}" title="Chia sẻ">📤</button></div></div></div>"""
291
-
292
- def render_article_html(article):
293
- aid = make_id(article["source_url"]); title_slug = slug(article["title"])
294
- share_js = f"window.bdpShare('{esc(article['title'])}','{aid}','{title_slug}')"
295
- src_label = "VnExpress" if article.get("source") == "vne" else "BongDaPlus"
296
- og_img = article.get("og_image", "")
297
- seo = f'<div style="display:none" itemscope itemtype="https://schema.org/NewsArticle"><meta itemprop="headline" content="{esc(article["title"])}"><meta itemprop="datePublished" content="{article.get("time","")}"><meta itemprop="publisher" content="{src_label}"><meta itemprop="image" content="{og_img}"><meta itemprop="description" content="{esc(article.get("summary","")[:160])}"></div>'
298
- parts = [f"""{seo}<div class="bdp-article">
299
- <h1 class="bdp-article-title">{article['title']}</h1>
300
- <div class="bdp-article-meta"><span>🕐 {article['time']} · {src_label}</span>
301
- <button class="bdp-share-article-btn" onclick="{share_js}">📤 Chia sẻ</button></div>"""]
302
- if article.get("summary"): parts.append(f'<div class="bdp-article-summary">{article["summary"]}</div>')
303
- for item in article.get("body", []):
304
- if item["type"] == "img":
305
- alt = item.get("alt", ""); cap = f'<figcaption class="bdp-figcap">{alt}</figcaption>' if alt else ""
306
- parts.append(f'<figure class="bdp-figure"><img src="{item["src"]}" alt="{alt}" onerror="this.parentElement.style.display=\'none\'">{cap}</figure>')
307
- elif item["type"] == "p": parts.append(f'<p class="bdp-article-p">{item["html"]}</p>')
308
- elif item["type"] == "heading": parts.append(f'<h2 class="bdp-article-h2">{item["text"]}</h2>')
309
- elif item["type"] == "quote": parts.append(f'<blockquote class="bdp-quote">{item["text"]}</blockquote>')
310
- if article.get("related"):
311
- parts.append('<div class="bdp-related"><h3>📰 Tin liên quan</h3>')
312
- for rel in article["related"]:
313
- rid = make_id(rel["link"]); rs = slug(rel["title"])
314
- parts.append(f'<div class="bdp-related-item" onclick="window.bdpOpen(\'{esc(rel["link"])}\',\'{rid}\',\'{rs}\')"><span>▸ {rel["title"]}</span></div>')
315
- parts.append('</div>')
316
- parts.append(f"""<div class="bdp-comments" id="comments-{aid}"><h3>💬 Bình luận</h3>
317
- <div id="cmt-list-{aid}"></div><div class="bdp-cmt-form">
318
- <input id="cmt-name-{aid}" class="bdp-cmt-input" placeholder="Tên của bạn..." maxlength="50">
319
- <textarea id="cmt-text-{aid}" class="bdp-cmt-textarea" placeholder="Viết bình luận..." rows="3" maxlength="500"></textarea>
320
- <button class="bdp-cmt-submit" onclick="window.bdpAddCmt('{aid}')">Gửi bình luận</button></div></div>""")
321
- parts.append('</div>')
322
- return '\n'.join(parts)
323
-
324
- CSS = """
325
- body,html{margin:0!important;padding:0!important;overflow-x:hidden;background:#111!important}
326
- .gradio-container{max-width:100%!important;width:100%!important;margin:0!important;padding:0!important;border-radius:0!important;background:#111!important}
327
- .main,.contain{max-width:100%!important;width:100%!important;padding:0!important;margin:0!important}
328
- .gradio-container>.main>.contain{padding-top:0!important}
329
- .gap{gap:0!important}
330
- footer,.built-with{display:none!important}
331
- #article-url-input,#btn-read-article{display:none!important;height:0!important;overflow:hidden!important}
332
- .bdp-header{background:linear-gradient(135deg,#0d1117,#1a3a2a 50%,#8b7500);padding:14px 16px;text-align:center}
333
- .bdp-header h1{color:#fff;font-size:20px;margin:0;font-weight:800;text-shadow:0 2px 6px rgba(0,0,0,.4)}
334
- .bdp-header p{color:rgba(255,255,255,.6);font-size:11px;margin:2px 0 0}
335
- @media(min-width:768px){.bdp-header h1{font-size:26px}.bdp-header{padding:20px}}
336
- .controls-row{padding:0 8px!important;background:#111!important}
337
- .controls-row>div{background:#111!important;border:none!important}
338
- .controls-row label{color:#aaa!important;font-size:12px!important}
339
- .controls-row .wrap{border-color:#333!important;background:#1a1a1a!important}
340
- .controls-row input,.controls-row select,.controls-row button{color:#eee!important}
341
- .bdp-wrap{padding:8px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
342
- .bdp-topbar{display:flex;justify-content:space-between;padding:4px 4px 8px;color:#666;font-size:11px}
343
- .bdp-empty{text-align:center;color:#666;padding:60px 20px}
344
- .bdp-section{margin-bottom:16px}
345
- .bdp-section-title{font-size:16px;font-weight:700;color:#5cb87a;margin:4px 0 8px;border-left:3px solid #5cb87a;padding-left:8px}
346
- @media(min-width:768px){.bdp-section-title{font-size:18px}}
347
- .bdp-grid{display:grid;grid-template-columns:1fr;gap:8px}
348
- .bdp-grid-featured,.bdp-grid-home{margin-bottom:4px}
349
- @media(min-width:420px){.bdp-grid{grid-template-columns:repeat(2,1fr)}}
350
- @media(min-width:768px){.bdp-grid{grid-template-columns:repeat(3,1fr);gap:10px}}
351
- @media(min-width:1100px){.bdp-grid{grid-template-columns:repeat(4,1fr)}}
352
- .bdp-card{background:#1a1a1a;border-radius:10px;overflow:hidden;cursor:pointer;transition:transform .15s,box-shadow .15s;border:1px solid #222}
353
- .bdp-card:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.5)}
354
- .bdp-card:active{transform:scale(.98)}
355
- .bdp-card-img{width:100%;height:130px;overflow:hidden;background:#222}
356
- .bdp-card-img-big{height:170px}
357
- .bdp-card-img img{width:100%;height:100%;object-fit:cover}
358
- @media(min-width:768px){.bdp-card-img{height:150px}.bdp-card-img-big{height:190px}}
359
- .bdp-card-body{padding:8px 10px 6px}
360
- .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}
361
- .bdp-card-title-big{font-size:14.5px}
362
- @media(min-width:768px){.bdp-card-title{font-size:13.5px}.bdp-card-title-big{font-size:15px}}
363
- .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}
364
- .bdp-card-footer{display:flex;justify-content:space-between;align-items:center;margin-top:6px}
365
- .bdp-card-time{color:#555;font-size:10.5px}
366
- .bdp-share-btn{background:none;border:none;cursor:pointer;font-size:15px;padding:3px 5px;border-radius:6px;transition:background .15s;color:#777}
367
- .bdp-share-btn:hover{background:#333}
368
- .bdp-badge{font-size:9px;padding:1px 6px;border-radius:3px;font-weight:700;display:inline-block;margin-bottom:4px}
369
- .bdp-badge-vne{background:#c0392b;color:#fff}
370
- .bdp-badge-bdp{background:#1a5c35;color:#fff}
371
- .bdp-article{padding:14px 12px 30px;max-width:720px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
372
- @media(min-width:768px){.bdp-article{padding:20px 16px 50px}}
373
- .bdp-article-title{font-size:21px;font-weight:800;color:#f0f0f0;line-height:1.3;margin:0 0 8px}
374
- @media(min-width:768px){.bdp-article-title{font-size:27px}}
375
- .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}
376
- .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}
377
- .bdp-share-article-btn:hover{opacity:.85}
378
- .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}
379
- .bdp-article-p{font-size:15.5px;line-height:1.75;color:#ccc;margin:0 0 12px}
380
- @media(min-width:768px){.bdp-article-p{font-size:16.5px}}
381
- .bdp-article-h2{font-size:19px;font-weight:700;color:#eee;margin:24px 0 10px}
382
- .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}
383
- .bdp-figure{margin:14px 0;text-align:center}
384
- .bdp-figure img{max-width:100%;height:auto;border-radius:8px}
385
- .bdp-figcap{color:#666;font-size:11.5px;margin-top:4px;font-style:italic}
386
- .bdp-related{margin-top:24px;padding-top:14px;border-top:1px solid #2a2a2a}
387
- .bdp-related h3{font-size:16px;color:#eee;margin:0 0 8px}
388
- .bdp-related-item{padding:8px 10px;margin-bottom:5px;border:1px solid #262626;border-radius:8px;cursor:pointer;transition:background .15s}
389
- .bdp-related-item:hover{background:#222}
390
- .bdp-related-item span{font-size:13.5px;color:#5cb87a;font-weight:500}
391
- .bdp-comments{margin-top:28px;padding-top:16px;border-top:1px solid #2a2a2a}
392
- .bdp-comments h3{font-size:16px;color:#eee;margin:0 0 10px}
393
- .bdp-cmt-item{background:#1a1a1a;border:1px solid #262626;border-radius:8px;padding:10px 12px;margin-bottom:8px}
394
- .bdp-cmt-author{font-weight:700;color:#5cb87a;font-size:13px}
395
- .bdp-cmt-date{color:#555;font-size:11px;margin-left:8px}
396
- .bdp-cmt-body{color:#ccc;font-size:14px;margin-top:4px;line-height:1.5}
397
- .bdp-cmt-form{margin-top:12px}
398
- .bdp-cmt-input{width:100%;padding:8px 10px;background:#1a1a1a;border:1px solid #333;border-radius:6px;color:#eee;font-size:13px;margin-bottom:6px;box-sizing:border-box}
399
- .bdp-cmt-textarea{width:100%;padding:8px 10px;background:#1a1a1a;border:1px solid #333;border-radius:6px;color:#eee;font-size:13px;resize:vertical;box-sizing:border-box}
400
- .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}
401
- .bdp-cmt-submit:hover{opacity:.85}
402
- .bdp-cmt-empty{color:#555;font-size:13px;font-style:italic;padding:8px 0}
403
- .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}
404
- .bdp-toast.show{opacity:1}
405
- .gr-group,.gr-box,.gr-panel{background:#111!important;border:none!important}
406
- .label-wrap{background:#111!important}
407
- """
408
-
409
- JS_HEAD = """
410
- <meta name="description" content="Tin tức tổng hợp nhanh nhất từ VnExpress, BongDaPlus - Thời sự, Thế giới, Kinh doanh, Công nghệ, Thể thao, Giải trí">
411
- <meta property="og:title" content="Tin Tức Việt Nam - Tổng Hợp Nhanh Nhất">
412
- <meta property="og:description" content="Đọc tin tức từ VnExpress, BongDaPlus. Thời sự, thế giới, kinh doanh, công nghệ, thể thao, giải trí. Cập nhật tự động.">
413
- <meta property="og:type" content="website">
414
- <meta property="og:image" content="https://bongdaplus.vn/Assets/img/logo.png">
415
- <meta name="twitter:card" content="summary_large_image">
416
- <link rel="canonical" href="https://bep40-bongdaplus-news.hf.space">
417
- <script>
418
- (function(){
419
- window.bdpOpen=function(url,aid,sl){window.location.hash='#/'+sl+'/'+aid;try{localStorage.setItem('bdp_url_'+aid,url);}catch(e){}var el=document.getElementById('article-url-input');if(el){var ta=el.querySelector('textarea');if(ta){ta.value=url;ta.dispatchEvent(new Event('input',{bubbles:true}));}}document.getElementById('btn-read-article').click();window.scrollTo({top:0,behavior:'smooth'});};
420
- window.bdpShare=async function(title,aid,sl){var u=window.location.origin+window.location.pathname+'#/'+sl+'/'+aid;var sd={title:title,url:u,text:title+' - Tin tức tổng hợp'};if(navigator.share){try{await navigator.share(sd);return;}catch(e){if(e.name==='AbortError')return;}}if(navigator.clipboard&&navigator.clipboard.writeText){try{await navigator.clipboard.writeText(u);window.bdpToast('✅ Đã sao chép liên kết!');return;}catch(e){}}var ta=document.createElement('textarea');ta.value=u;ta.style.cssText='position:fixed;opacity:0';document.body.appendChild(ta);ta.focus();ta.select();try{document.execCommand('copy');window.bdpToast('✅ Đã sao chép liên kết!');}catch(e){window.prompt('Sao chép:',u);}document.body.removeChild(ta);};
421
- 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);};
422
- function gc(a){try{return JSON.parse(localStorage.getItem('bdp_cmt_'+a))||[];}catch(e){return[];}}
423
- function sc(a,c){try{localStorage.setItem('bdp_cmt_'+a,JSON.stringify(c));}catch(e){}}
424
- 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;};
425
- 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!');};
426
- new MutationObserver(function(ms){ms.forEach(function(m){m.addedNodes.forEach(function(n){if(n.querySelectorAll){n.querySelectorAll('[id^="cmt-list-"]').forEach(function(d){window.bdpRenderCmt(d.id.replace('cmt-list-',''));});}});});}).observe(document.body,{childList:true,subtree:true});
427
- window.addEventListener('load',function(){var h=window.location.hash;if(h&&h.startsWith('#/')){var ps=h.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('/'));},1200);}catch(e){}}}});
428
- })();
429
- </script>
430
- """
431
-
432
- with gr.Blocks(title="Tin Tức Việt Nam - VnExpress, BongDaPlus", css=CSS, head=JS_HEAD, theme=gr.themes.Base(), fill_width=True) as demo:
433
- gr.HTML("""<div class="bdp-header"><h1>📰 Tin Tức Việt Nam</h1>
434
- <p>VnExpress · BongDaPlus · Thời sự · Thế giới · Kinh doanh · Công nghệ · Thể thao · Giải trí</p></div>""")
435
- article_url = gr.Textbox(value="", visible=False, elem_id="article-url-input")
436
- with gr.Row(elem_classes=["controls-row"]):
437
- cat = gr.Dropdown(choices=list(CATEGORIES.keys()), value="🏠 Trang Chủ (Nổi Bật)", label="Chuyên mục", scale=3, interactive=True)
438
- ref_btn = gr.Button("🔄 Làm mới", variant="primary", scale=1)
439
- back_btn = gr.Button("← Quay lại", variant="secondary", scale=1, visible=False)
440
- news_list = gr.HTML()
441
- article_view = gr.HTML(visible=False)
442
- read_btn = gr.Button("Đọc", visible=False, elem_id="btn-read-article")
443
- def show_article(url):
444
- if not url or url == "#" or len(url) < 10:
445
- return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), ""
446
- return (gr.update(visible=False), gr.update(value=read_article(url), visible=True), gr.update(visible=False), gr.update(visible=True), "")
447
- def show_list(c):
448
- return (gr.update(value=fetch_news_list(c), visible=True), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False))
449
- read_btn.click(fn=show_article, inputs=[article_url], outputs=[news_list, article_view, ref_btn, back_btn, article_url])
450
- back_btn.click(fn=show_list, inputs=[cat], outputs=[news_list, article_view, ref_btn, back_btn])
451
- ref_btn.click(fn=show_list, inputs=[cat], outputs=[news_list, article_view, ref_btn, back_btn])
452
- cat.change(fn=show_list, inputs=[cat], outputs=[news_list, article_view, ref_btn, back_btn])
453
- timer = gr.Timer(value=REFRESH_SECONDS, active=True)
454
- timer.tick(fn=fetch_news_list, inputs=cat, outputs=news_list)
455
- demo.load(fn=fetch_news_list, inputs=cat, outputs=news_list)
456
-
457
- if __name__ == "__main__":
458
- demo.launch()
 
1
+ PLACEHOLDER