bep40 commited on
Commit
d165dec
·
verified ·
1 Parent(s): c212cd8

ROOT CAUSE FIX: New clean entry point that imports main.py directly, bypasses ALL injection layers"

Browse files
Files changed (1) hide show
  1. app_v2_entry.py +214 -0
app_v2_entry.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ VNEWS v2 Entry Point - CLEAN.
3
+ Imports ONLY main.py (core scrapers + livescore APIs).
4
+ Does NOT import app_run/app_final/ai_runtime_final6 (the injection chain).
5
+ This guarantees index_v2.html is served without any old JS injections.
6
+ """
7
+ import sys, os
8
+
9
+ # Import the core app from main.py which has all scrapers + livescore
10
+ from main import app, HEADERS, BONGDA_HEADERS, fetch_bongda_api
11
+
12
+ # Now import FastAPI utilities
13
+ from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
14
+ from fastapi.staticfiles import StaticFiles
15
+ from fastapi import Query, Request
16
+ import requests as req
17
+ from urllib.parse import quote
18
+ from bs4 import BeautifulSoup
19
+ import re, html as html_lib, json, threading, time
20
+ from concurrent.futures import ThreadPoolExecutor, as_completed
21
+
22
+ STATIC_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'static')
23
+
24
+ def _clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or ""))).strip()
25
+ _STOP_WORDS=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ì'.split())
26
+
27
+ def _relevance_score(topic, title):
28
+ topic_lower = topic.lower().strip();title_lower = (title or '').lower()
29
+ if topic_lower in title_lower: return 10
30
+ topic_words = [w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+', topic_lower) if len(w) > 1 and w not in _STOP_WORDS]
31
+ if not topic_words: return 0
32
+ matched = sum(1 for w in topic_words if w in title_lower)
33
+ ratio = matched / len(topic_words) if topic_words else 0
34
+ return int(ratio * 8) if ratio >= 0.6 else 0
35
+
36
+ def _search_vnexpress(topic,limit=8):
37
+ items=[]
38
+ try:
39
+ r=req.get(f"https://timkiem.vnexpress.net/?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
40
+ for art in soup.select('article.item-news')[:limit]:
41
+ a=art.select_one('h2 a, h3 a')
42
+ if a and a.get('href'):items.append({'title':_clean(a.get('title','') or a.get_text(strip=True)),'url':a['href'],'via':'VnExpress'})
43
+ except:pass
44
+ return items
45
+ def _search_dantri(topic,limit=8):
46
+ items=[]
47
+ try:
48
+ r=req.get(f"https://dantri.com.vn/tim-kiem/{quote(topic)}.htm",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
49
+ for a in soup.select('h3 a[href], .article-title a[href]')[:limit*2]:
50
+ t=_clean(a.get_text(strip=True));href=a.get('href','')
51
+ if t and len(t)>15:
52
+ if not href.startswith('http'):href='https://dantri.com.vn'+href
53
+ if 'dantri.com.vn' in href:items.append({'title':t,'url':href,'via':'Dân Trí'})
54
+ if len(items)>=limit:break
55
+ except:pass
56
+ return items
57
+ def _search_vietnamnet(topic,limit=6):
58
+ items=[]
59
+ try:
60
+ r=req.get(f"https://vietnamnet.vn/tim-kiem?q={quote(topic)}",headers={'User-Agent':'Mozilla/5.0'},timeout=10);soup=BeautifulSoup(r.text,'lxml')
61
+ for a in soup.select('h3 a[href], .horizontalPost__main-title a')[:limit*2]:
62
+ t=_clean(a.get_text(strip=True));href=a.get('href','')
63
+ if t and len(t)>15:
64
+ if not href.startswith('http'):href='https://vietnamnet.vn'+href
65
+ if 'vietnamnet.vn' in href:items.append({'title':t,'url':href,'via':'VietNamNet'})
66
+ if len(items)>=limit:break
67
+ except:pass
68
+ return items
69
+ def _search_all(topic, limit=40):
70
+ all_items=[]
71
+ with ThreadPoolExecutor(3) as ex:
72
+ futs=[ex.submit(_search_vnexpress,topic,10),ex.submit(_search_dantri,topic,10),ex.submit(_search_vietnamnet,topic,8)]
73
+ for f in as_completed(futs,timeout=12):
74
+ try:all_items.extend(f.result())
75
+ except:pass
76
+ seen=set();unique=[]
77
+ for i in all_items:
78
+ if i.get('url') and i['url'] not in seen:seen.add(i['url']);unique.append(i)
79
+ return unique[:limit]
80
+
81
+ # ============================================================
82
+ # OVERRIDE the old '/' route from main.py
83
+ # ============================================================
84
+ app.router.routes = [r for r in app.router.routes if not (
85
+ getattr(r, 'path', None) == '/' and 'GET' in getattr(r, 'methods', set())
86
+ )]
87
+
88
+ # ============================================================
89
+ # NEW ROUTES
90
+ # ============================================================
91
+ @app.get('/api/hashtag/sources')
92
+ def _ht(topic:str=Query(...), page:int=Query(default=0)):
93
+ all_items=_search_all(topic, 40)
94
+ scored = [(s,item) for item in all_items if (s:=_relevance_score(topic, item.get('title','')))>0]
95
+ scored.sort(key=lambda x: x[0], reverse=True)
96
+ filtered = [item for _, item in scored]
97
+ if len(filtered) < 3: filtered = all_items
98
+ per_page=6;start=page*per_page;end=start+per_page
99
+ return JSONResponse({'sources':filtered[start:end],'topic':topic,'page':page,'has_more':end<len(filtered),'total':len(filtered)})
100
+
101
+ @app.get('/api/categories')
102
+ def _categories():return JSONResponse([])
103
+
104
+ @app.get('/api/storage_status')
105
+ def _storage():return JSONResponse({'persistent':os.path.isdir('/data') and os.access('/data', os.W_OK)})
106
+
107
+ @app.get('/s')
108
+ async def _share(url:str='',title:str='',img:str=''):
109
+ return HTMLResponse(f'<!DOCTYPE html><html><head><meta property="og:title" content="{_clean(title)}"><meta property="og:image" content="{_clean(img)}"><meta http-equiv="refresh" content="0;url={_clean(url) or "/"}"></head><body>Redirecting...</body></html>')
110
+
111
+ # ============================================================
112
+ # INTERACTIONS + COMMENTS (persistent /data)
113
+ # ============================================================
114
+ DATA_DIR = '/data' if os.path.isdir('/data') else os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
115
+ os.makedirs(DATA_DIR, exist_ok=True)
116
+ INTERACTIONS_FILE = os.path.join(DATA_DIR, 'interactions_v2.json')
117
+ COMMENTS_FILE = os.path.join(DATA_DIR, 'comments_v2.json')
118
+ _interact_lock = threading.Lock()
119
+ _comment_lock = threading.Lock()
120
+ def _load_json(path):
121
+ try:
122
+ if os.path.exists(path):
123
+ with open(path,'r',encoding='utf-8') as f:return json.load(f)
124
+ except:pass
125
+ return {}
126
+ def _save_json(path, data):
127
+ try:
128
+ tmp=path+'.tmp'
129
+ with open(tmp,'w',encoding='utf-8') as f:json.dump(data,f,ensure_ascii=False)
130
+ os.replace(tmp,path)
131
+ except:pass
132
+
133
+ @app.post('/api/v2/interact')
134
+ async def api_interact(request:Request):
135
+ body=await request.json();vid=str(body.get('id','')).strip();itype=str(body.get('type','')).strip()
136
+ if not vid or itype not in('view','like'):return JSONResponse({'error':'invalid'},status_code=400)
137
+ with _interact_lock:
138
+ db=_load_json(INTERACTIONS_FILE)
139
+ if vid not in db:db[vid]={'views':0,'likes':0,'comments':0}
140
+ db[vid][itype+'s']=db[vid].get(itype+'s',0)+1
141
+ _save_json(INTERACTIONS_FILE,db);return JSONResponse(db[vid])
142
+ @app.get('/api/v2/interactions')
143
+ def api_get_interactions(id:str=Query(...)):
144
+ with _interact_lock:return JSONResponse(_load_json(INTERACTIONS_FILE).get(id.strip(),{'views':0,'likes':0,'comments':0}))
145
+ @app.get('/api/v2/comments')
146
+ def api_get_comments(id:str=Query(...)):
147
+ with _comment_lock:return JSONResponse({'comments':_load_json(COMMENTS_FILE).get(id.strip(),[])})
148
+ @app.post('/api/v2/comment')
149
+ async def api_post_comment(request:Request):
150
+ body=await request.json();vid=str(body.get('id','')).strip();text=str(body.get('text','')).strip()[:500]
151
+ if not vid or not text:return JSONResponse({'error':'invalid'},status_code=400)
152
+ comment={'text':text,'time':time.strftime('%H:%M %d/%m',time.localtime()),'ts':int(time.time())}
153
+ with _comment_lock:
154
+ db=_load_json(COMMENTS_FILE)
155
+ if vid not in db:db[vid]=[]
156
+ db[vid].append(comment)
157
+ if len(db[vid])>200:db[vid]=db[vid][-200:]
158
+ _save_json(COMMENTS_FILE,db);comments=db[vid]
159
+ with _interact_lock:
160
+ idb=_load_json(INTERACTIONS_FILE)
161
+ if vid not in idb:idb[vid]={'views':0,'likes':0,'comments':0}
162
+ idb[vid]['comments']=len(comments);_save_json(INTERACTIONS_FILE,idb)
163
+ return JSONResponse({'comments':comments})
164
+
165
+ # ============================================================
166
+ # WORLD CUP 2026 API
167
+ # ============================================================
168
+ from wc2026_scraper import (
169
+ scrape_summary, scrape_fixtures, scrape_standings, scrape_stats,
170
+ scrape_wc_news, scrape_road_to_wc, get_wc2026_all,
171
+ scrape_history, scrape_h2h, scrape_lineups, scrape_match_detail
172
+ )
173
+ @app.get('/api/wc2026')
174
+ def api_wc2026_all():return JSONResponse(get_wc2026_all())
175
+ @app.get('/api/wc2026/fixtures')
176
+ def api_wc2026_fixtures():return JSONResponse(scrape_fixtures())
177
+ @app.get('/api/wc2026/standings')
178
+ def api_wc2026_standings():return JSONResponse(scrape_standings())
179
+ @app.get('/api/wc2026/stats')
180
+ def api_wc2026_stats():return JSONResponse(scrape_stats())
181
+ @app.get('/api/wc2026/history')
182
+ def api_wc2026_history():return JSONResponse(scrape_history())
183
+ @app.get('/api/wc2026/news')
184
+ def api_wc2026_news():return JSONResponse(scrape_wc_news())
185
+ @app.get('/api/wc2026/road')
186
+ def api_wc2026_road():return JSONResponse(scrape_road_to_wc())
187
+ @app.get('/api/wc2026/h2h/{event_id}')
188
+ def api_wc2026_h2h(event_id:int):return JSONResponse(scrape_h2h(event_id))
189
+ @app.get('/api/wc2026/lineups/{event_id}')
190
+ def api_wc2026_lineups(event_id:int):return JSONResponse(scrape_lineups(event_id))
191
+ @app.get('/api/wc2026/match/{event_id}')
192
+ def api_wc2026_match(event_id:int):return JSONResponse(scrape_match_detail(event_id))
193
+
194
+ def _wc2026_bg():
195
+ time.sleep(15)
196
+ while True:
197
+ try:get_wc2026_all()
198
+ except:pass
199
+ time.sleep(90)
200
+ threading.Thread(target=_wc2026_bg,daemon=True).start()
201
+
202
+ # ============================================================
203
+ # SERVE V2 FRONTEND - THE ONLY '/' ROUTE
204
+ # ============================================================
205
+ @app.get('/')
206
+ async def serve_index():
207
+ """Serve index_v2.html - the clean frontend."""
208
+ p = os.path.join(STATIC_DIR, 'index_v2.html')
209
+ if os.path.exists(p):
210
+ return FileResponse(p, media_type='text/html')
211
+ return HTMLResponse('<h1>VNEWS</h1><p>Rebuilding...</p>')
212
+
213
+ # Static files mount MUST be last
214
+ app.mount('/static', StaticFiles(directory=STATIC_DIR), name='vnews_static')