bep40 commited on
Commit
dbda048
·
verified ·
1 Parent(s): 90cb310

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +279 -0
app.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ from bs4 import BeautifulSoup
4
+ from datetime import datetime
5
+ import time
6
+
7
+ # ── Constants ──────────────────────────────────────────────────────────────────
8
+ HEADERS = {
9
+ "User-Agent": (
10
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
11
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
12
+ "Chrome/120.0.0.0 Safari/537.36"
13
+ ),
14
+ "Accept-Language": "vi-VN,vi;q=0.9,en;q=0.8",
15
+ }
16
+
17
+ CATEGORIES = {
18
+ "🏠 Tin Mới Nhất": "https://bongdaplus.vn/tin-moi",
19
+ "🏴󠁧󠁢󠁥󠁮󠁧󠁿 Ngoại Hạng Anh": "https://bongdaplus.vn/ngoai-hang-anh",
20
+ "🇪🇸 La Liga": "https://bongdaplus.vn/la-liga",
21
+ "🇮🇹 Serie A": "https://bongdaplus.vn/serie-a",
22
+ "🇩🇪 Bundesliga": "https://bongdaplus.vn/bundesliga",
23
+ "🇫🇷 Ligue 1": "https://bongdaplus.vn/ligue-1",
24
+ "🏆 Champions League": "https://bongdaplus.vn/champions-league-cup-c1",
25
+ "🏆 Europa League": "https://bongdaplus.vn/europa-league",
26
+ "🇻🇳 Bóng Đá Việt Nam": "https://bongdaplus.vn/bong-da-viet-nam",
27
+ "🇻🇳 V-League": "https://bongdaplus.vn/v-league",
28
+ "⚽ Chuyển Nhượng": "https://bongdaplus.vn/tin-chuyen-nhuong",
29
+ "🌍 Bóng Đá Thế Giới": "https://bongdaplus.vn/bong-da-the-gioi",
30
+ }
31
+
32
+ BASE_URL = "https://bongdaplus.vn"
33
+ REFRESH_SECONDS = 300 # 5 minutes
34
+
35
+
36
+ # ── Scraper ────────────────────────────────────────────────────────────────────
37
+ def scrape_news(url: str) -> list[dict]:
38
+ """Scrape news articles from bongdaplus.vn pages."""
39
+ try:
40
+ r = requests.get(url, headers=HEADERS, timeout=15)
41
+ r.encoding = "utf-8"
42
+ soup = BeautifulSoup(r.text, "lxml")
43
+ articles = []
44
+ seen_links = set()
45
+
46
+ # ─── Strategy 1: Featured news (div.news.fst) ───
47
+ for item in soup.select("div.news.fst"):
48
+ art = _parse_news_item(item)
49
+ if art and art["link"] not in seen_links:
50
+ art["featured"] = True
51
+ articles.append(art)
52
+ seen_links.add(art["link"])
53
+
54
+ # ─── Strategy 2: Slider items (div.sld-itm.news) ───
55
+ for item in soup.select("div.sld-itm.news"):
56
+ art = _parse_news_item(item)
57
+ if art and art["link"] not in seen_links:
58
+ art["featured"] = True
59
+ articles.append(art)
60
+ seen_links.add(art["link"])
61
+
62
+ # ─── Strategy 3: List items (li.news) — the main content ───
63
+ for item in soup.select("li.news"):
64
+ art = _parse_news_item(item)
65
+ if art and art["link"] not in seen_links:
66
+ art["featured"] = False
67
+ articles.append(art)
68
+ seen_links.add(art["link"])
69
+
70
+ return articles
71
+
72
+ except Exception as e:
73
+ return [{"title": f"⚠️ Lỗi tải tin: {e}", "link": "#", "img": None,
74
+ "summary": "", "time": "", "featured": False}]
75
+
76
+
77
+ def _parse_news_item(item) -> dict | None:
78
+ """Extract title, link, image, summary, time from a news item element."""
79
+ # Title
80
+ title_tag = item.find("a", class_="title")
81
+ if not title_tag:
82
+ title_tag = item.find("a", href=True)
83
+ if not title_tag:
84
+ return None
85
+
86
+ title = title_tag.get_text(strip=True)
87
+ if not title or len(title) < 5:
88
+ return None
89
+
90
+ # Link
91
+ link = title_tag.get("href", "")
92
+ if link and not link.startswith("http"):
93
+ link = BASE_URL + link
94
+
95
+ # Image
96
+ img_tag = item.find("img")
97
+ img = None
98
+ if img_tag:
99
+ img = img_tag.get("data-src") or img_tag.get("src")
100
+
101
+ # Summary
102
+ summ_tag = item.find("p", class_="summ")
103
+ summary = summ_tag.get_text(strip=True) if summ_tag else ""
104
+
105
+ # Time
106
+ time_tag = item.find("div", class_="in-time")
107
+ time_text = time_tag.get_text(strip=True) if time_tag else ""
108
+
109
+ return {
110
+ "title": title,
111
+ "link": link,
112
+ "img": img,
113
+ "summary": summary,
114
+ "time": time_text,
115
+ "featured": False,
116
+ }
117
+
118
+
119
+ # ── HTML Renderer ──────────────────────────────────────────────────────────────
120
+ def render_html(articles: list[dict]) -> str:
121
+ """Render articles as HTML cards."""
122
+ if not articles:
123
+ return "<p style='text-align:center;color:#888;padding:40px;'>Không tìm thấy tin tức.</p>"
124
+
125
+ now = datetime.now().strftime("%H:%M:%S %d/%m/%Y")
126
+
127
+ # Featured articles (big cards)
128
+ featured = [a for a in articles if a.get("featured")]
129
+ regular = [a for a in articles if not a.get("featured")]
130
+
131
+ html_parts = []
132
+
133
+ # Header
134
+ html_parts.append(f"""
135
+ <div style="font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;max-width:1200px;margin:0 auto;">
136
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
137
+ <span style="color:#666;font-size:12px;">⏱ Cập nhật lúc: {now}</span>
138
+ <span style="color:#666;font-size:12px;">📰 {len(articles)} tin</span>
139
+ </div>
140
+ """)
141
+
142
+ # Featured section
143
+ if featured:
144
+ html_parts.append('<div style="margin-bottom:24px;">')
145
+ html_parts.append('<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px;">')
146
+ for art in featured[:6]:
147
+ html_parts.append(_card_html(art, big=True))
148
+ html_parts.append('</div></div>')
149
+
150
+ # Regular news
151
+ if regular:
152
+ html_parts.append('<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;">')
153
+ for art in regular[:30]:
154
+ html_parts.append(_card_html(art, big=False))
155
+ html_parts.append('</div>')
156
+
157
+ html_parts.append('</div>')
158
+ return '\n'.join(html_parts)
159
+
160
+
161
+ def _card_html(art: dict, big: bool = False) -> str:
162
+ """Render a single article card."""
163
+ img_height = "200px" if big else "160px"
164
+ title_size = "16px" if big else "14px"
165
+
166
+ img_html = ""
167
+ if art.get("img"):
168
+ img_html = f"""
169
+ <div style="width:100%;height:{img_height};overflow:hidden;background:#f0f0f0;">
170
+ <img src="{art['img']}" alt=""
171
+ style="width:100%;height:100%;object-fit:cover;transition:transform 0.3s;"
172
+ onerror="this.style.display='none'">
173
+ </div>"""
174
+
175
+ time_html = ""
176
+ if art.get("time"):
177
+ time_html = f'<span style="color:#999;font-size:11px;">🕐 {art["time"]}</span>'
178
+
179
+ summ_html = ""
180
+ if art.get("summary"):
181
+ summ_html = f'<p style="font-size:12px;color:#666;margin:6px 0 0 0;line-height:1.4;">{art["summary"][:150]}{"..." if len(art["summary"]) > 150 else ""}</p>'
182
+
183
+ return f"""
184
+ <div style="border:1px solid #e8e8e8;border-radius:10px;overflow:hidden;
185
+ background:#fff;box-shadow:0 2px 8px rgba(0,0,0,0.06);
186
+ transition:box-shadow 0.2s,transform 0.2s;"
187
+ onmouseover="this.style.boxShadow='0 4px 16px rgba(0,0,0,0.12)';this.style.transform='translateY(-2px)'"
188
+ onmouseout="this.style.boxShadow='0 2px 8px rgba(0,0,0,0.06)';this.style.transform='translateY(0)'">
189
+ <a href="{art['link']}" target="_blank" rel="noopener"
190
+ style="text-decoration:none;color:inherit;display:block;">
191
+ {img_html}
192
+ <div style="padding:12px 14px;">
193
+ <h3 style="font-size:{title_size};font-weight:600;color:#1a1a1a;
194
+ margin:0;line-height:1.45;">
195
+ {art['title']}
196
+ </h3>
197
+ {summ_html}
198
+ <div style="margin-top:8px;">{time_html}</div>
199
+ </div>
200
+ </a>
201
+ </div>"""
202
+
203
+
204
+ # ── Main fetch function ───────────────────────────────────────────────────────
205
+ def fetch_news(category: str) -> str:
206
+ """Fetch and render news for a given category."""
207
+ url = CATEGORIES.get(category, CATEGORIES["🏠 Tin Mới Nhất"])
208
+ articles = scrape_news(url)
209
+ return render_html(articles)
210
+
211
+
212
+ # ── Gradio App ─────────────────────────────────────────────────────────────────
213
+ CUSTOM_CSS = """
214
+ .gradio-container {
215
+ max-width: 1280px !important;
216
+ background: #fafafa !important;
217
+ }
218
+ .header-banner {
219
+ background: linear-gradient(135deg, #1a472a 0%, #2d8659 50%, #d4af37 100%);
220
+ padding: 24px 32px;
221
+ border-radius: 12px;
222
+ margin-bottom: 16px;
223
+ text-align: center;
224
+ }
225
+ .header-banner h1 {
226
+ color: #fff !important;
227
+ font-size: 28px !important;
228
+ margin: 0 !important;
229
+ text-shadow: 0 2px 4px rgba(0,0,0,0.2);
230
+ }
231
+ .header-banner p {
232
+ color: rgba(255,255,255,0.85) !important;
233
+ font-size: 14px !important;
234
+ margin: 4px 0 0 0 !important;
235
+ }
236
+ footer { display: none !important; }
237
+ """
238
+
239
+ with gr.Blocks(
240
+ title="⚽ BongDaPlus - Tin Tức Bóng Đá",
241
+ ) as demo:
242
+
243
+ gr.HTML("""
244
+ <div class="header-banner">
245
+ <h1>⚽ BongDaPlus — Tin Tức Bóng Đá</h1>
246
+ <p>Tự động cập nhật tin tức từ bongdaplus.vn mỗi 5 phút</p>
247
+ </div>
248
+ """)
249
+
250
+ with gr.Row():
251
+ cat_dropdown = gr.Dropdown(
252
+ choices=list(CATEGORIES.keys()),
253
+ value="🏠 Tin Mới Nhất",
254
+ label="📂 Chuyên mục",
255
+ scale=4,
256
+ interactive=True,
257
+ )
258
+ refresh_btn = gr.Button(
259
+ "🔄 Làm mới",
260
+ variant="primary",
261
+ scale=1,
262
+ )
263
+
264
+ news_display = gr.HTML(label="Tin tức")
265
+
266
+ # Auto-refresh every 5 minutes
267
+ timer = gr.Timer(value=REFRESH_SECONDS, active=True)
268
+ timer.tick(fn=fetch_news, inputs=cat_dropdown, outputs=news_display)
269
+
270
+ # Manual interactions
271
+ refresh_btn.click(fn=fetch_news, inputs=cat_dropdown, outputs=news_display)
272
+ cat_dropdown.change(fn=fetch_news, inputs=cat_dropdown, outputs=news_display)
273
+
274
+ # Load on startup
275
+ demo.load(fn=fetch_news, inputs=cat_dropdown, outputs=news_display)
276
+
277
+
278
+ if __name__ == "__main__":
279
+ demo.launch(css=CUSTOM_CSS, theme=gr.themes.Soft())