Spaces:
Running
Running
feat: inline video player (VnExpress MP4 + BDP video-embed), OG image in share links, dynamic meta tags
Browse files
app.py
CHANGED
|
@@ -1,458 +1 @@
|
|
| 1 |
-
|
| 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,'<').replace(/>/g,'>')+'</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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|