bep40 commited on
Commit
ae576e1
·
verified ·
1 Parent(s): 6a2f1c9

Update app_v2_entry.py

Browse files
Files changed (1) hide show
  1. app_v2_entry.py +46 -28
app_v2_entry.py CHANGED
@@ -17,7 +17,12 @@ import re, html as html_lib, json, threading, time
17
  from concurrent.futures import ThreadPoolExecutor, as_completed
18
  from urllib.parse import quote
19
 
20
- HL_LEAGUES['friendly'] = {"path": "giai-khac/friendly", "name": "Giao hữu", "emoji": "🤝"}
 
 
 
 
 
21
 
22
  STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
23
  app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/' and hasattr(r,'methods') and 'GET' in getattr(r,'methods',set()))]
@@ -26,27 +31,55 @@ app.router.routes=[r for r in app.router.routes if not isinstance(r, Mount)]
26
 
27
  def _clean(s): return re.sub(r"\s+"," ",html_lib.unescape(str(s or""))).strip()
28
 
29
- # Cache for match details (5 min TTL)
30
  _match_cache = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  # === FAST BONGDA PROXY ENDPOINT ===
33
  def _get_match_detail(event_id, slug=None):
34
- """Internal function to scrape match detail from bongda.com.vn"""
35
  headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html", "Referer": "https://bongda.com.vn/"}
36
-
37
  if slug:
38
  url = f"https://bongda.com.vn/tran-dau/{event_id}/centre/{slug}"
39
  else:
40
  url = f"https://bongda.com.vn/tran-dau/{event_id}"
41
-
42
  resp = req.get(url, headers=headers, timeout=15, allow_redirects=True)
43
  if resp.status_code != 200:
44
  return None
45
-
46
  soup = BeautifulSoup(resp.text, 'html.parser')
47
  result = {"event_id": event_id, "found": False, "sections": []}
48
  info = {}
49
-
50
  tel = soup.select_one('.teams')
51
  if tel:
52
  he = tel.select_one('.team.home')
@@ -68,27 +101,22 @@ def _get_match_detail(event_id, slug=None):
68
  if len(parts) >= 2: info['score'] = f"{parts[0]} - {parts[1]}"
69
  lb = sc.select_one('.label')
70
  if lb: info['status_label'] = _clean(lb.get_text())
71
-
72
  if info.get('home_team') and info.get('away_team'):
73
  result['info'] = info
74
  result['found'] = True
75
  result['sections'].append('info')
76
-
77
  events = []
78
  for ev in soup.select('.events .period .event'):
79
  ev_cls = ' '.join(ev.get('class', []))
80
  ev_data = {'team': 'home' if 'home' in ev_cls else 'away', 'period': '', 'type': 'unknown', 'time': '', 'players': ''}
81
-
82
  parent = ev.parent
83
  if parent:
84
  h2 = parent.find('h2')
85
  if h2: ev_data['period'] = _clean(h2.get_text())
86
-
87
  if ev.select_one('[class*="goal"]'): ev_data['type'] = 'goal'
88
  elif ev.select_one('[class*="redcard"]'): ev_data['type'] = 'redcard'
89
  elif ev.select_one('[class*="yellowcard"]'): ev_data['type'] = 'yellowcard'
90
  elif ev.select_one('[class*="substitution"]'): ev_data['type'] = 'substitution'
91
-
92
  players_el = ev.select_one('.players')
93
  if players_el:
94
  pl_text = _clean(players_el.get_text(' ', strip=True))
@@ -99,11 +127,9 @@ def _get_match_detail(event_id, slug=None):
99
  else:
100
  ev_data['players'] = pl_text
101
  events.append(ev_data)
102
-
103
  if events:
104
  result['events'] = events
105
  result['sections'].append('events')
106
-
107
  pred = soup.select_one('.prediction-card')
108
  if pred:
109
  team_info = pred.select_one('.team-info')
@@ -118,7 +144,6 @@ def _get_match_detail(event_id, slug=None):
118
  vc = pred.select_one('.vote-count')
119
  if vc: pred_data['vote_count'] = _clean(vc.get_text())
120
  result['prediction'] = pred_data
121
-
122
  recent = []
123
  ml = soup.select_one('.matches-list')
124
  if ml:
@@ -133,7 +158,6 @@ def _get_match_detail(event_id, slug=None):
133
  if recent:
134
  result['recent_matches'] = recent
135
  result['sections'].append('recent')
136
-
137
  try:
138
  api_h = {"User-Agent": "Mozilla/5.0", "Accept": "application/json", "X-Requested-With": "XMLHttpRequest", "Referer": "https://bongda.com.vn/"}
139
  ar = req.get(f"https://bongda.com.vn/api/fixtures/h2h-stats?event_id={event_id}", headers=api_h, timeout=10)
@@ -151,20 +175,17 @@ def _get_match_detail(event_id, slug=None):
151
  result['h2h_stats_parsed'] = ast
152
  result['sections'].append('h2h_stats')
153
  except: pass
154
-
155
  return result
156
 
157
  @app.get('/api/proxy/bongda')
158
  def proxy_bongda(event_id: int = Query(default=None), slug: str = Query(default=None)):
159
  if event_id is None:
160
  return JSONResponse({'error': 'event_id required'}, status_code=400)
161
-
162
  cache_key = f"{event_id}_{slug}"
163
  now = time.time()
164
  cached = _match_cache.get(cache_key)
165
  if cached and now - cached.get('_ts', 0) < 300:
166
  return JSONResponse(cached)
167
-
168
  try:
169
  result = _get_match_detail(event_id, slug)
170
  if result:
@@ -175,26 +196,21 @@ def proxy_bongda(event_id: int = Query(default=None), slug: str = Query(default=
175
  err = {"event_id": event_id, "found": False, "error": str(e), "_ts": now}
176
  _match_cache[cache_key] = err
177
  return JSONResponse(err)
178
-
179
  return JSONResponse({"event_id": event_id, "found": False})
180
 
181
  @app.get('/api/match/{event_id}/detail')
182
  def api_match_detail(event_id: int, url: str = Query(default=None)):
183
- # Try to extract slug from url if provided
184
  slug = None
185
  if url:
186
  m = re.match(r'.+/tran-dau/\d+/(?:centre|preview)/(.+)', url)
187
  if m:
188
  slug = m.group(1)
189
-
190
  cache_key = f"{event_id}_{slug or ''}"
191
  now = time.time()
192
  cached = _match_cache.get(cache_key)
193
  if cached and now - cached.get('_ts', 0) < 300:
194
  return JSONResponse(cached)
195
-
196
  try:
197
- # If no slug, try to find it from homepage
198
  if not slug:
199
  try:
200
  home_r = req.get("https://bongda.com.vn/", headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
@@ -208,7 +224,6 @@ def api_match_detail(event_id: int, url: str = Query(default=None)):
208
  cache_key = f"{event_id}_{slug}"
209
  break
210
  except: pass
211
-
212
  result = _get_match_detail(event_id, slug)
213
  if result:
214
  result['_ts'] = now
@@ -218,10 +233,13 @@ def api_match_detail(event_id: int, url: str = Query(default=None)):
218
  err = {"event_id": event_id, "found": False, "error": str(e), "_ts": now}
219
  _match_cache[cache_key] = err
220
  return JSONResponse(err)
221
-
222
  return JSONResponse({"event_id": event_id, "found": False})
223
 
224
- # === Rest of endpoints (existing) ===
 
 
 
 
225
  _STOP=set('và của các những một được trong với cho tại sau trước khi không người việt nam hôm nay mới nhất nóng tin tức cập nhật theo từ đến là có thì này đã để'.split())
226
 
227
  def _has_kw(topic,title):
@@ -498,4 +516,4 @@ def _bg():
498
  time.sleep(90)
499
  threading.Thread(target=_bg,daemon=True).start()
500
 
501
- app.mount('/static',StaticFiles(directory=STATIC_DIR),name='vnews_static')
 
17
  from concurrent.futures import ThreadPoolExecutor, as_completed
18
  from urllib.parse import quote
19
 
20
+ # Update world-cup path to the actual xemlaibongda page (not qualifiers)
21
+ HL_LEAGUES['world-cup'] = {"path": "the-gioi/world-cup", "name": "World Cup", "emoji": "🌍"}
22
+
23
+ # Add friendly back
24
+ if 'friendly' not in HL_LEAGUES:
25
+ HL_LEAGUES['friendly'] = {"path": "giai-khac/friendly", "name": "Giao hữu", "emoji": "🤝"}
26
 
27
  STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
28
  app.router.routes=[r for r in app.router.routes if not(getattr(r,'path',None)=='/' and hasattr(r,'methods') and 'GET' in getattr(r,'methods',set()))]
 
31
 
32
  def _clean(s): return re.sub(r"\s+"," ",html_lib.unescape(str(s or""))).strip()
33
 
 
34
  _match_cache = {}
35
+ _vtvnambo_cache = {'t': 0, 'd': []}
36
+ _VTVNAMBO_CACHE_TTL = 1800 # 30 min
37
+
38
+ def _yt_channel_shorts_fast(channel, count=15):
39
+ """Fast scrape YouTube shorts without yt-dlp."""
40
+ try:
41
+ url=f"https://www.youtube.com/@{channel}/shorts"
42
+ r=req.get(url,headers={**HEADERS,"Accept-Language":"vi,en;q=0.8"},timeout=15)
43
+ if r.status_code!=200:return[]
44
+ html=r.text
45
+ ids=[];items=[]
46
+ for m in re.finditer(r'"videoId":"([A-Za-z0-9_-]{11})"',html):
47
+ vid=m.group(1)
48
+ if vid in ids:continue
49
+ ids.append(vid)
50
+ snip=html[max(0,m.start()-900):m.start()+1600]
51
+ title=""
52
+ mt=re.search(r'"title":\{"runs":\[\{"text":"([^"]+)"',snip)
53
+ if mt:title=html_lib.unescape(mt.group(1)).replace('\n',' ').strip()
54
+ if not title:title="YouTube Short"
55
+ items.append({"title":title,"link":f"https://www.youtube.com/watch?v={vid}","img":f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg","source":"yt","id":vid,"channel":channel})
56
+ if len(items)>=count:break
57
+ return items
58
+ except:return[]
59
+
60
+ def _get_vtvnambo_shorts():
61
+ """Cached VTVNamBo shorts."""
62
+ now = time.time()
63
+ if _vtvnambo_cache['d'] and now - _vtvnambo_cache['t'] < _VTVNAMBO_CACHE_TTL:
64
+ return _vtvnambo_cache['d']
65
+ items = _yt_channel_shorts_fast("vtvnambo", 24)
66
+ _vtvnambo_cache['t'] = now
67
+ _vtvnambo_cache['d'] = items
68
+ return items
69
 
70
  # === FAST BONGDA PROXY ENDPOINT ===
71
  def _get_match_detail(event_id, slug=None):
 
72
  headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html", "Referer": "https://bongda.com.vn/"}
 
73
  if slug:
74
  url = f"https://bongda.com.vn/tran-dau/{event_id}/centre/{slug}"
75
  else:
76
  url = f"https://bongda.com.vn/tran-dau/{event_id}"
 
77
  resp = req.get(url, headers=headers, timeout=15, allow_redirects=True)
78
  if resp.status_code != 200:
79
  return None
 
80
  soup = BeautifulSoup(resp.text, 'html.parser')
81
  result = {"event_id": event_id, "found": False, "sections": []}
82
  info = {}
 
83
  tel = soup.select_one('.teams')
84
  if tel:
85
  he = tel.select_one('.team.home')
 
101
  if len(parts) >= 2: info['score'] = f"{parts[0]} - {parts[1]}"
102
  lb = sc.select_one('.label')
103
  if lb: info['status_label'] = _clean(lb.get_text())
 
104
  if info.get('home_team') and info.get('away_team'):
105
  result['info'] = info
106
  result['found'] = True
107
  result['sections'].append('info')
 
108
  events = []
109
  for ev in soup.select('.events .period .event'):
110
  ev_cls = ' '.join(ev.get('class', []))
111
  ev_data = {'team': 'home' if 'home' in ev_cls else 'away', 'period': '', 'type': 'unknown', 'time': '', 'players': ''}
 
112
  parent = ev.parent
113
  if parent:
114
  h2 = parent.find('h2')
115
  if h2: ev_data['period'] = _clean(h2.get_text())
 
116
  if ev.select_one('[class*="goal"]'): ev_data['type'] = 'goal'
117
  elif ev.select_one('[class*="redcard"]'): ev_data['type'] = 'redcard'
118
  elif ev.select_one('[class*="yellowcard"]'): ev_data['type'] = 'yellowcard'
119
  elif ev.select_one('[class*="substitution"]'): ev_data['type'] = 'substitution'
 
120
  players_el = ev.select_one('.players')
121
  if players_el:
122
  pl_text = _clean(players_el.get_text(' ', strip=True))
 
127
  else:
128
  ev_data['players'] = pl_text
129
  events.append(ev_data)
 
130
  if events:
131
  result['events'] = events
132
  result['sections'].append('events')
 
133
  pred = soup.select_one('.prediction-card')
134
  if pred:
135
  team_info = pred.select_one('.team-info')
 
144
  vc = pred.select_one('.vote-count')
145
  if vc: pred_data['vote_count'] = _clean(vc.get_text())
146
  result['prediction'] = pred_data
 
147
  recent = []
148
  ml = soup.select_one('.matches-list')
149
  if ml:
 
158
  if recent:
159
  result['recent_matches'] = recent
160
  result['sections'].append('recent')
 
161
  try:
162
  api_h = {"User-Agent": "Mozilla/5.0", "Accept": "application/json", "X-Requested-With": "XMLHttpRequest", "Referer": "https://bongda.com.vn/"}
163
  ar = req.get(f"https://bongda.com.vn/api/fixtures/h2h-stats?event_id={event_id}", headers=api_h, timeout=10)
 
175
  result['h2h_stats_parsed'] = ast
176
  result['sections'].append('h2h_stats')
177
  except: pass
 
178
  return result
179
 
180
  @app.get('/api/proxy/bongda')
181
  def proxy_bongda(event_id: int = Query(default=None), slug: str = Query(default=None)):
182
  if event_id is None:
183
  return JSONResponse({'error': 'event_id required'}, status_code=400)
 
184
  cache_key = f"{event_id}_{slug}"
185
  now = time.time()
186
  cached = _match_cache.get(cache_key)
187
  if cached and now - cached.get('_ts', 0) < 300:
188
  return JSONResponse(cached)
 
189
  try:
190
  result = _get_match_detail(event_id, slug)
191
  if result:
 
196
  err = {"event_id": event_id, "found": False, "error": str(e), "_ts": now}
197
  _match_cache[cache_key] = err
198
  return JSONResponse(err)
 
199
  return JSONResponse({"event_id": event_id, "found": False})
200
 
201
  @app.get('/api/match/{event_id}/detail')
202
  def api_match_detail(event_id: int, url: str = Query(default=None)):
 
203
  slug = None
204
  if url:
205
  m = re.match(r'.+/tran-dau/\d+/(?:centre|preview)/(.+)', url)
206
  if m:
207
  slug = m.group(1)
 
208
  cache_key = f"{event_id}_{slug or ''}"
209
  now = time.time()
210
  cached = _match_cache.get(cache_key)
211
  if cached and now - cached.get('_ts', 0) < 300:
212
  return JSONResponse(cached)
 
213
  try:
 
214
  if not slug:
215
  try:
216
  home_r = req.get("https://bongda.com.vn/", headers={"User-Agent": "Mozilla/5.0"}, timeout=10)
 
224
  cache_key = f"{event_id}_{slug}"
225
  break
226
  except: pass
 
227
  result = _get_match_detail(event_id, slug)
228
  if result:
229
  result['_ts'] = now
 
233
  err = {"event_id": event_id, "found": False, "error": str(e), "_ts": now}
234
  _match_cache[cache_key] = err
235
  return JSONResponse(err)
 
236
  return JSONResponse({"event_id": event_id, "found": False})
237
 
238
+ # === VTVNamBo Shorts API ===
239
+ @app.get('/api/shorts/vtvnambo')
240
+ def api_shorts_vtvnambo():
241
+ return JSONResponse(_get_vtvnambo_shorts())
242
+
243
  _STOP=set('và của các những một được trong với cho tại sau trước khi không người việt nam hôm nay mới nhất nóng tin tức cập nhật theo từ đến là có thì này đã để'.split())
244
 
245
  def _has_kw(topic,title):
 
516
  time.sleep(90)
517
  threading.Thread(target=_bg,daemon=True).start()
518
 
519
+ app.mount('/static',StaticFiles(directory=STATIC_DIR),name='vnews_static')