Spaces:
Running
Running
Fix: use VnExpress/Dantri/Vietnamnet direct search for real URLs, not Google News"
Browse files- app_run.py +86 -110
app_run.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"""Wrapper:
|
| 2 |
from app_final import *
|
| 3 |
from app_final import app, f6, f5, rt, PATCH_INJECT, UNIFIED_INJECT_FIXED, HIGHLIGHT_FULL_OVERRIDE, EXTRA_WALL_FIX
|
| 4 |
from fastapi.responses import HTMLResponse, JSONResponse
|
|
@@ -7,29 +7,80 @@ import requests as req
|
|
| 7 |
from urllib.parse import quote
|
| 8 |
from bs4 import BeautifulSoup
|
| 9 |
import re, html as html_lib
|
|
|
|
| 10 |
|
| 11 |
def _clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or ""))).strip()
|
|
|
|
| 12 |
|
| 13 |
-
def
|
|
|
|
|
|
|
| 14 |
try:
|
| 15 |
-
r=req.
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
def _scrape_any_article(url):
|
| 22 |
-
if 'news.google.com' in url or 'google.com/rss' in url:url=_follow_redirect(url)
|
| 23 |
try:
|
| 24 |
-
r=req.get(url,headers=
|
| 25 |
-
|
| 26 |
for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript','iframe']):tag.decompose()
|
| 27 |
h1=soup.find('h1');ogt=soup.find('meta',property='og:title')
|
| 28 |
title=(h1.get_text(' ',strip=True) if h1 else '') or (ogt.get('content','') if ogt else '') or (soup.title.get_text(strip=True) if soup.title else '')
|
| 29 |
ogd=soup.find('meta',property='og:description') or soup.find('meta',attrs={'name':'description'})
|
| 30 |
summary=ogd.get('content','') if ogd else ''
|
| 31 |
-
ogi=soup.find('meta',property='og:image')
|
| 32 |
-
og_image=ogi.get('content','') if ogi else ''
|
| 33 |
if og_image and og_image.startswith('//'):og_image='https:'+og_image
|
| 34 |
selectors=['article','main','.article-content','.detail-content','.singular-content','.fck_detail','.entry-content','.story-body','.knc-content','.cms-body']
|
| 35 |
block=None
|
|
@@ -37,19 +88,15 @@ def _scrape_any_article(url):
|
|
| 37 |
el=soup.select_one(sel)
|
| 38 |
if el and len(el.find_all('p'))>=2:block=el;break
|
| 39 |
if not block:
|
| 40 |
-
best=None;
|
| 41 |
for el in soup.find_all(['article','main','section','div']):
|
| 42 |
-
ps=el.find_all('p');
|
| 43 |
-
if
|
| 44 |
block=best or soup.body or soup
|
| 45 |
body=[]
|
| 46 |
for el in block.find_all(['p','h2','h3','figure','img'],recursive=True):
|
| 47 |
-
if el.name=='p':
|
| 48 |
-
|
| 49 |
-
if len(t)>30:body.append({'type':'p','text':t})
|
| 50 |
-
elif el.name in('h2','h3'):
|
| 51 |
-
t=_clean(el.get_text(' ',strip=True))
|
| 52 |
-
if t:body.append({'type':'heading','text':t})
|
| 53 |
elif el.name in('figure','img'):
|
| 54 |
im=el if el.name=='img' else el.find('img')
|
| 55 |
if im:
|
|
@@ -61,52 +108,6 @@ def _scrape_any_article(url):
|
|
| 61 |
return {'title':_clean(title),'summary':_clean(summary),'og_image':og_image,'body':body[:50],'source':'generic','url':url}
|
| 62 |
except:return None
|
| 63 |
|
| 64 |
-
def _google_news_search(topic, limit=20):
|
| 65 |
-
"""Search Google News RSS — returns newest first, strict relevance."""
|
| 66 |
-
items=[]
|
| 67 |
-
try:
|
| 68 |
-
# Use exact topic as search query for better relevance
|
| 69 |
-
url='https://news.google.com/rss/search?q='+quote('"'+topic+'"')+'&hl=vi&gl=VN&ceid=VN:vi'
|
| 70 |
-
r=req.get(url,headers={'User-Agent':'Mozilla/5.0'},timeout=10);r.encoding='utf-8'
|
| 71 |
-
soup=BeautifulSoup(r.text,'xml')
|
| 72 |
-
for it in soup.find_all('item')[:limit*3]:
|
| 73 |
-
title=_clean(it.find('title').get_text(' ',strip=True) if it.find('title') else '')
|
| 74 |
-
link=_clean(it.find('link').get_text(strip=True) if it.find('link') else '')
|
| 75 |
-
src=_clean(it.find('source').get_text(' ',strip=True) if it.find('source') else '')
|
| 76 |
-
pub=_clean(it.find('pubDate').get_text(strip=True) if it.find('pubDate') else '')
|
| 77 |
-
if not title or not link:continue
|
| 78 |
-
# Strict relevance: topic phrase or majority of words must be in title
|
| 79 |
-
topic_lower=topic.lower();title_lower=title.lower()
|
| 80 |
-
topic_words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',topic_lower) if len(w)>1]
|
| 81 |
-
if topic_lower in title_lower:
|
| 82 |
-
pass # exact match — always include
|
| 83 |
-
else:
|
| 84 |
-
match_count=sum(1 for w in topic_words if w in title_lower)
|
| 85 |
-
if len(topic_words)>0 and match_count<max(1,len(topic_words)//2):continue
|
| 86 |
-
items.append({'title':title,'url':link,'via':src,'snippet':'','pub':pub})
|
| 87 |
-
if len(items)>=limit:break
|
| 88 |
-
except:pass
|
| 89 |
-
# Also try without quotes for broader results if too few
|
| 90 |
-
if len(items)<5:
|
| 91 |
-
try:
|
| 92 |
-
url2='https://news.google.com/rss/search?q='+quote(topic+' Việt Nam')+'&hl=vi&gl=VN&ceid=VN:vi'
|
| 93 |
-
r2=req.get(url2,headers={'User-Agent':'Mozilla/5.0'},timeout=8);r2.encoding='utf-8'
|
| 94 |
-
soup2=BeautifulSoup(r2.text,'xml')
|
| 95 |
-
seen_urls={s['url'] for s in items}
|
| 96 |
-
for it in soup2.find_all('item')[:limit]:
|
| 97 |
-
title=_clean(it.find('title').get_text(' ',strip=True) if it.find('title') else '')
|
| 98 |
-
link=_clean(it.find('link').get_text(strip=True) if it.find('link') else '')
|
| 99 |
-
src=_clean(it.find('source').get_text(' ',strip=True) if it.find('source') else '')
|
| 100 |
-
if not title or not link or link in seen_urls:continue
|
| 101 |
-
topic_lower=topic.lower();title_lower=title.lower()
|
| 102 |
-
topic_words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',topic_lower) if len(w)>1]
|
| 103 |
-
match_count=sum(1 for w in topic_words if w in title_lower)
|
| 104 |
-
if match_count==0:continue
|
| 105 |
-
seen_urls.add(link);items.append({'title':title,'url':link,'via':src,'snippet':''})
|
| 106 |
-
if len(items)>=limit:break
|
| 107 |
-
except:pass
|
| 108 |
-
return items
|
| 109 |
-
|
| 110 |
# Override endpoints
|
| 111 |
app.router.routes=[r for r in app.router.routes if not (
|
| 112 |
(getattr(r,'path',None)=='/api/hashtag/sources' and 'GET' in getattr(r,'methods',set())) or
|
|
@@ -115,7 +116,7 @@ app.router.routes=[r for r in app.router.routes if not (
|
|
| 115 |
)]
|
| 116 |
|
| 117 |
@app.get('/api/article')
|
| 118 |
-
def
|
| 119 |
data=_scrape_any_article(url)
|
| 120 |
if data and data.get('body'):return JSONResponse(data)
|
| 121 |
from main import scrape_vne_article,scrape_bbc_article,scrape_dantri_article,scrape_genk_article,scrape_ttvh_article
|
|
@@ -129,24 +130,20 @@ def _article_universal(url:str=Query(...)):
|
|
| 129 |
return JSONResponse({'error':'Không đọc được bài viết','url':url})
|
| 130 |
|
| 131 |
@app.get('/api/hashtag/sources')
|
| 132 |
-
def
|
| 133 |
-
"""Return relevant sources with
|
| 134 |
-
all_sources=
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
for s in all_sources:
|
| 138 |
-
if s['url'] not in seen:seen.add(s['url']);unique.append(s)
|
| 139 |
-
total=len(unique)
|
| 140 |
-
page=unique[offset:offset+limit]
|
| 141 |
has_more=offset+limit<total
|
| 142 |
-
return JSONResponse({'sources':page,'topic':topic,'total':total,'offset':offset,'has_more':has_more})
|
| 143 |
|
| 144 |
FAST_HASHTAG_JS = r'''
|
| 145 |
<style>
|
| 146 |
.hashtag-loading{display:flex;align-items:center;gap:8px;padding:12px;color:#888;font-size:12px}
|
| 147 |
.hashtag-spinner{width:16px;height:16px;border:2px solid #333;border-top-color:#5cb87a;border-radius:50%;animation:ht-spin .8s linear infinite}
|
| 148 |
@keyframes ht-spin{to{transform:rotate(360deg)}}
|
| 149 |
-
.hashtag-load-more{width:100%;margin-top:8px;background:#222;border:1px solid #333;color:#ccc;padding:9px;border-radius:10px;font-size:12px;cursor:pointer;text-align:center}
|
| 150 |
</style>
|
| 151 |
<script>
|
| 152 |
(function(){
|
|
@@ -170,57 +167,36 @@ window.readArticle=async function(url){
|
|
| 170 |
window.doRewriteArticle=async function(btn){var url=(window._currentArticle&&window._currentArticle.url)||'';if(!url){alert('Không có URL');return;}var ctx=document.querySelector('.article-view')?.innerText?.slice(0,14000)||'';btn.disabled=true;btn.textContent='Đang rewrite...';try{var r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:url,context:ctx})});var j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Đã đăng Tường AI!');}catch(e){alert(e.message);}finally{btn.disabled=false;btn.textContent='🤖 Rewrite AI đăng tường';}};
|
| 171 |
window.askArticleAI=async function(){var q=document.getElementById('article-ai-question')?.value.trim();if(!q)return alert('Nhập câu hỏi');var a=document.getElementById('article-ai-answer');a.textContent='Đang hỏi...';var url=(window._currentArticle&&window._currentArticle.url)||'';var ctx=document.querySelector('.article-view')?.innerText?.slice(0,12000)||'';try{var r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:url,question:q,context:ctx})});var j=await r.json();a.textContent=j.answer||'Không trả lời được';}catch(e){a.textContent='Lỗi: '+e.message}};
|
| 172 |
|
| 173 |
-
// === HASHTAG SOURCES with pagination ===
|
| 174 |
function renderSources(sources,append){
|
| 175 |
-
var list=document.getElementById('hashtag-src-list');if(!list)return;
|
| 176 |
-
var
|
| 177 |
-
sources.forEach(function(s){
|
| 178 |
-
var idx=_htImgIdx++;
|
| 179 |
h+='<div class="hashtag-src-item" onclick="readArticle(\''+esc(s.url||'')+'\')">';
|
| 180 |
h+='<div class="hashtag-src-img" id="ht-img-'+idx+'"></div>';
|
| 181 |
-
h+='<div class="hashtag-src-text"><div class="hashtag-src-title">'+esc(s.title)+'</div><div class="hashtag-src-via">'+esc(s.via||'')+'</div></div>';
|
| 182 |
-
|
| 183 |
-
// Lazy load image
|
| 184 |
-
if(s.url)fetch('/api/article?url='+encodeURIComponent(s.url)).then(function(r){return r.json()}).then(function(d){if(d&&(d.og_image||d.img)){var el=document.getElementById('ht-img-'+idx);if(el)el.innerHTML='<img src="'+esc(d.og_image||d.img)+'" onerror="this.style.display=\'none\'" loading="lazy">';}}).catch(function(){});
|
| 185 |
});
|
| 186 |
if(append)list.insertAdjacentHTML('beforeend',h);else list.innerHTML=h;
|
| 187 |
}
|
| 188 |
-
|
| 189 |
window.showHashtagSources=async function(topic){
|
| 190 |
_htTopic=topic;_htOffset=0;_htImgIdx=0;
|
| 191 |
var home=document.getElementById('view-home');if(!home)return;
|
| 192 |
document.getElementById('hashtag-sources-box')?.remove();
|
| 193 |
var box=document.createElement('div');box.id='hashtag-sources-box';box.className='hashtag-sources';
|
| 194 |
box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div class="hashtag-loading"><div class="hashtag-spinner"></div>Đang tìm bài viết mới nhất...</div>';
|
| 195 |
-
var compose=home.querySelector('.ai-compose');
|
| 196 |
-
if(compose)compose.after(box);else home.prepend(box);
|
| 197 |
box.scrollIntoView({behavior:'smooth',block:'start'});
|
| 198 |
try{
|
| 199 |
var r=await fetch('/api/hashtag/sources?topic='+encodeURIComponent(topic)+'&offset=0&limit=6');
|
| 200 |
var j=await r.json();var sources=j.sources||[];
|
| 201 |
-
if(!sources.length){box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#888;font-size:12px;padding:8px">Không tìm được bài viết
|
| 202 |
_htOffset=sources.length;
|
| 203 |
-
var h='<h3>🔍 '+esc(topic)+' <span style="font-size:10px;color:#888">('+j.total+' bài)</span></h3>';
|
| 204 |
-
h+='<div id="hashtag-src-list"></div>';
|
| 205 |
if(j.has_more)h+='<button class="hashtag-load-more" id="ht-load-more" onclick="loadMoreHashtag()">Tải thêm bài viết...</button>';
|
| 206 |
h+='<button class="hashtag-rewrite-btn" onclick="rewriteHashtagTopic(\''+esc(topic)+'\')">🤖 Rewrite AI tổng hợp & đăng tường</button>';
|
| 207 |
-
box.innerHTML=h;
|
| 208 |
-
|
| 209 |
-
}catch(e){box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#e74c3c;font-size:12px;padding:8px">Lỗi: '+esc(e.message)+'</div>';}
|
| 210 |
-
};
|
| 211 |
-
|
| 212 |
-
window.loadMoreHashtag=async function(){
|
| 213 |
-
var btn=document.getElementById('ht-load-more');if(btn){btn.textContent='Đang tải...';btn.disabled=true;}
|
| 214 |
-
try{
|
| 215 |
-
var r=await fetch('/api/hashtag/sources?topic='+encodeURIComponent(_htTopic)+'&offset='+_htOffset+'&limit=6');
|
| 216 |
-
var j=await r.json();var sources=j.sources||[];
|
| 217 |
-
_htOffset+=sources.length;
|
| 218 |
-
renderSources(sources,true);
|
| 219 |
-
if(!j.has_more&&btn)btn.remove();
|
| 220 |
-
else if(btn){btn.textContent='Tải thêm bài viết...';btn.disabled=false;}
|
| 221 |
-
}catch(e){if(btn){btn.textContent='Lỗi, thử lại';btn.disabled=false;}}
|
| 222 |
};
|
| 223 |
-
|
| 224 |
window.rewriteHashtagTopic=async function(topic){var btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tổng hợp...';}try{var r=await fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic:topic})});var j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');if(btn)btn.textContent='✅ Đã đăng!';setTimeout(function(){document.getElementById('hashtag-sources-box')?.remove();},2000);}catch(e){if(btn){btn.disabled=false;btn.textContent='❌ '+e.message;}}};
|
| 225 |
window.createTopicPost=function(){var inp=document.getElementById('ai-topic-input');var topic=(inp&&inp.value||'').trim();if(!topic){alert('Nhập chủ đề');return;}showHashtagSources(topic);if(inp)inp.value='';};
|
| 226 |
window.createTopicPostFinal5=function(){var inp=document.getElementById('ai-topic-input-final5')||document.getElementById('ai-topic-input');var topic=(inp&&inp.value||'').trim();if(!topic){alert('Nhập chủ đề');return;}showHashtagSources(topic);if(inp)inp.value='';};
|
|
|
|
| 1 |
+
"""Wrapper: real article URLs from VN news search, universal scrape, pagination."""
|
| 2 |
from app_final import *
|
| 3 |
from app_final import app, f6, f5, rt, PATCH_INJECT, UNIFIED_INJECT_FIXED, HIGHLIGHT_FULL_OVERRIDE, EXTRA_WALL_FIX
|
| 4 |
from fastapi.responses import HTMLResponse, JSONResponse
|
|
|
|
| 7 |
from urllib.parse import quote
|
| 8 |
from bs4 import BeautifulSoup
|
| 9 |
import re, html as html_lib
|
| 10 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 11 |
|
| 12 |
def _clean(s):return re.sub(r"\s+"," ",html_lib.unescape(str(s or ""))).strip()
|
| 13 |
+
UA={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36','Accept-Language':'vi-VN,vi;q=0.9'}
|
| 14 |
|
| 15 |
+
def _vne_search(topic, limit=10):
|
| 16 |
+
"""Search VnExpress — returns real URLs."""
|
| 17 |
+
items=[]
|
| 18 |
try:
|
| 19 |
+
r=req.get(f'https://timkiem.vnexpress.net/?q={quote(topic)}',headers=UA,timeout=8);r.encoding='utf-8'
|
| 20 |
+
soup=BeautifulSoup(r.text,'lxml')
|
| 21 |
+
for a in soup.select('article.item-news h2 a, article.item-news h3 a, h2.title-news a, h3.title-news a')[:limit]:
|
| 22 |
+
title=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
|
| 23 |
+
if title and href and href.startswith('http'):items.append({'title':title,'url':href,'via':'VnExpress'})
|
| 24 |
+
except:pass
|
| 25 |
+
return items
|
| 26 |
+
|
| 27 |
+
def _dantri_search(topic, limit=8):
|
| 28 |
+
"""Search Dantri — returns real URLs."""
|
| 29 |
+
items=[]
|
| 30 |
+
try:
|
| 31 |
+
r=req.get(f'https://dantri.com.vn/tim-kiem/{quote(topic)}.htm',headers=UA,timeout=8);r.encoding='utf-8'
|
| 32 |
+
soup=BeautifulSoup(r.text,'lxml')
|
| 33 |
+
for a in soup.select('h3.article-title a, h2.article-title a, .article-item a[href]')[:limit]:
|
| 34 |
+
title=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
|
| 35 |
+
if not href.startswith('http'):href='https://dantri.com.vn'+href
|
| 36 |
+
if title and len(title)>15 and href.endswith('.htm'):items.append({'title':title,'url':href,'via':'Dân trí'})
|
| 37 |
+
except:pass
|
| 38 |
+
return items
|
| 39 |
+
|
| 40 |
+
def _vietnamnet_search(topic, limit=8):
|
| 41 |
+
"""Search Vietnamnet — returns real URLs."""
|
| 42 |
+
items=[]
|
| 43 |
+
try:
|
| 44 |
+
r=req.get(f'https://vietnamnet.vn/tim-kiem?q={quote(topic)}',headers=UA,timeout=8);r.encoding='utf-8'
|
| 45 |
+
soup=BeautifulSoup(r.text,'lxml')
|
| 46 |
+
for a in soup.select('h3 a[href], .verticalPost__main a[href], .horizontalPost__main a[href]')[:limit]:
|
| 47 |
+
title=_clean(a.get('title','') or a.get_text(strip=True));href=a.get('href','')
|
| 48 |
+
if not href.startswith('http'):href='https://vietnamnet.vn'+href
|
| 49 |
+
if title and len(title)>15:items.append({'title':title,'url':href,'via':'Vietnamnet'})
|
| 50 |
+
except:pass
|
| 51 |
+
return items
|
| 52 |
+
|
| 53 |
+
def _multi_search(topic, limit=20):
|
| 54 |
+
"""Search multiple VN news sources in parallel for real URLs."""
|
| 55 |
+
all_items=[];seen=set()
|
| 56 |
+
with ThreadPoolExecutor(3) as ex:
|
| 57 |
+
futs=[ex.submit(_vne_search,topic,10),ex.submit(_dantri_search,topic,8),ex.submit(_vietnamnet_search,topic,8)]
|
| 58 |
+
for f in as_completed(futs,timeout=12):
|
| 59 |
+
try:
|
| 60 |
+
for item in f.result():
|
| 61 |
+
if item['url'] not in seen:seen.add(item['url']);all_items.append(item)
|
| 62 |
+
except:pass
|
| 63 |
+
# Filter relevance
|
| 64 |
+
topic_lower=topic.lower()
|
| 65 |
+
topic_words=[w for w in re.findall(r'[A-Za-zÀ-ỹ0-9]+',topic_lower) if len(w)>1]
|
| 66 |
+
relevant=[]
|
| 67 |
+
for item in all_items:
|
| 68 |
+
title_lower=item['title'].lower()
|
| 69 |
+
if topic_lower in title_lower:relevant.append(item);continue
|
| 70 |
+
match=sum(1 for w in topic_words if w in title_lower)
|
| 71 |
+
if match>=max(1,len(topic_words)//2):relevant.append(item)
|
| 72 |
+
return relevant[:limit]
|
| 73 |
|
| 74 |
def _scrape_any_article(url):
|
|
|
|
| 75 |
try:
|
| 76 |
+
r=req.get(url,headers=UA,timeout=15,allow_redirects=True);r.encoding='utf-8'
|
| 77 |
+
soup=BeautifulSoup(r.text,'lxml')
|
| 78 |
for tag in soup.find_all(['script','style','nav','footer','aside','form','noscript','iframe']):tag.decompose()
|
| 79 |
h1=soup.find('h1');ogt=soup.find('meta',property='og:title')
|
| 80 |
title=(h1.get_text(' ',strip=True) if h1 else '') or (ogt.get('content','') if ogt else '') or (soup.title.get_text(strip=True) if soup.title else '')
|
| 81 |
ogd=soup.find('meta',property='og:description') or soup.find('meta',attrs={'name':'description'})
|
| 82 |
summary=ogd.get('content','') if ogd else ''
|
| 83 |
+
ogi=soup.find('meta',property='og:image');og_image=ogi.get('content','') if ogi else ''
|
|
|
|
| 84 |
if og_image and og_image.startswith('//'):og_image='https:'+og_image
|
| 85 |
selectors=['article','main','.article-content','.detail-content','.singular-content','.fck_detail','.entry-content','.story-body','.knc-content','.cms-body']
|
| 86 |
block=None
|
|
|
|
| 88 |
el=soup.select_one(sel)
|
| 89 |
if el and len(el.find_all('p'))>=2:block=el;break
|
| 90 |
if not block:
|
| 91 |
+
best=None;bs=0
|
| 92 |
for el in soup.find_all(['article','main','section','div']):
|
| 93 |
+
ps=el.find_all('p');sc=len(ps)*100+sum(len(p.get_text())for p in ps[:10])
|
| 94 |
+
if sc>bs:best=el;bs=sc
|
| 95 |
block=best or soup.body or soup
|
| 96 |
body=[]
|
| 97 |
for el in block.find_all(['p','h2','h3','figure','img'],recursive=True):
|
| 98 |
+
if el.name=='p':t=_clean(el.get_text(' ',strip=True));(body.append({'type':'p','text':t}) if len(t)>30 else None)
|
| 99 |
+
elif el.name in('h2','h3'):t=_clean(el.get_text(' ',strip=True));(body.append({'type':'heading','text':t}) if t else None)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
elif el.name in('figure','img'):
|
| 101 |
im=el if el.name=='img' else el.find('img')
|
| 102 |
if im:
|
|
|
|
| 108 |
return {'title':_clean(title),'summary':_clean(summary),'og_image':og_image,'body':body[:50],'source':'generic','url':url}
|
| 109 |
except:return None
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
# Override endpoints
|
| 112 |
app.router.routes=[r for r in app.router.routes if not (
|
| 113 |
(getattr(r,'path',None)=='/api/hashtag/sources' and 'GET' in getattr(r,'methods',set())) or
|
|
|
|
| 116 |
)]
|
| 117 |
|
| 118 |
@app.get('/api/article')
|
| 119 |
+
def _article(url:str=Query(...)):
|
| 120 |
data=_scrape_any_article(url)
|
| 121 |
if data and data.get('body'):return JSONResponse(data)
|
| 122 |
from main import scrape_vne_article,scrape_bbc_article,scrape_dantri_article,scrape_genk_article,scrape_ttvh_article
|
|
|
|
| 130 |
return JSONResponse({'error':'Không đọc được bài viết','url':url})
|
| 131 |
|
| 132 |
@app.get('/api/hashtag/sources')
|
| 133 |
+
def _hashtag(topic:str=Query(...),offset:int=Query(default=0),limit:int=Query(default=6)):
|
| 134 |
+
"""Return relevant sources from VN news search with real URLs. Supports pagination."""
|
| 135 |
+
all_sources=_multi_search(topic,20)
|
| 136 |
+
total=len(all_sources)
|
| 137 |
+
page=all_sources[offset:offset+limit]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
has_more=offset+limit<total
|
| 139 |
+
return JSONResponse({'sources':[{'title':s['title'],'url':s['url'],'via':s['via']} for s in page],'topic':topic,'total':total,'offset':offset,'has_more':has_more})
|
| 140 |
|
| 141 |
FAST_HASHTAG_JS = r'''
|
| 142 |
<style>
|
| 143 |
.hashtag-loading{display:flex;align-items:center;gap:8px;padding:12px;color:#888;font-size:12px}
|
| 144 |
.hashtag-spinner{width:16px;height:16px;border:2px solid #333;border-top-color:#5cb87a;border-radius:50%;animation:ht-spin .8s linear infinite}
|
| 145 |
@keyframes ht-spin{to{transform:rotate(360deg)}}
|
| 146 |
+
.hashtag-load-more{width:100%;margin-top:8px;background:#222;border:1px solid #333;color:#ccc;padding:9px;border-radius:10px;font-size:12px;cursor:pointer;text-align:center}
|
| 147 |
</style>
|
| 148 |
<script>
|
| 149 |
(function(){
|
|
|
|
| 167 |
window.doRewriteArticle=async function(btn){var url=(window._currentArticle&&window._currentArticle.url)||'';if(!url){alert('Không có URL');return;}var ctx=document.querySelector('.article-view')?.innerText?.slice(0,14000)||'';btn.disabled=true;btn.textContent='Đang rewrite...';try{var r=await fetch('/api/rewrite_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:url,context:ctx})});var j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');alert('Đã đăng Tường AI!');}catch(e){alert(e.message);}finally{btn.disabled=false;btn.textContent='🤖 Rewrite AI đăng tường';}};
|
| 168 |
window.askArticleAI=async function(){var q=document.getElementById('article-ai-question')?.value.trim();if(!q)return alert('Nhập câu hỏi');var a=document.getElementById('article-ai-answer');a.textContent='Đang hỏi...';var url=(window._currentArticle&&window._currentArticle.url)||'';var ctx=document.querySelector('.article-view')?.innerText?.slice(0,12000)||'';try{var r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:url,question:q,context:ctx})});var j=await r.json();a.textContent=j.answer||'Không trả lời được';}catch(e){a.textContent='Lỗi: '+e.message}};
|
| 169 |
|
|
|
|
| 170 |
function renderSources(sources,append){
|
| 171 |
+
var list=document.getElementById('hashtag-src-list');if(!list)return;var h='';
|
| 172 |
+
sources.forEach(function(s){var idx=_htImgIdx++;
|
|
|
|
|
|
|
| 173 |
h+='<div class="hashtag-src-item" onclick="readArticle(\''+esc(s.url||'')+'\')">';
|
| 174 |
h+='<div class="hashtag-src-img" id="ht-img-'+idx+'"></div>';
|
| 175 |
+
h+='<div class="hashtag-src-text"><div class="hashtag-src-title">'+esc(s.title)+'</div><div class="hashtag-src-via">'+esc(s.via||'')+'</div></div></div>';
|
| 176 |
+
fetch('/api/article?url='+encodeURIComponent(s.url)).then(function(r){return r.json()}).then(function(d){if(d&&(d.og_image||d.img)){var el=document.getElementById('ht-img-'+idx);if(el)el.innerHTML='<img src="'+esc(d.og_image||d.img)+'" onerror="this.style.display=\'none\'" loading="lazy">';}}).catch(function(){});
|
|
|
|
|
|
|
| 177 |
});
|
| 178 |
if(append)list.insertAdjacentHTML('beforeend',h);else list.innerHTML=h;
|
| 179 |
}
|
|
|
|
| 180 |
window.showHashtagSources=async function(topic){
|
| 181 |
_htTopic=topic;_htOffset=0;_htImgIdx=0;
|
| 182 |
var home=document.getElementById('view-home');if(!home)return;
|
| 183 |
document.getElementById('hashtag-sources-box')?.remove();
|
| 184 |
var box=document.createElement('div');box.id='hashtag-sources-box';box.className='hashtag-sources';
|
| 185 |
box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div class="hashtag-loading"><div class="hashtag-spinner"></div>Đang tìm bài viết mới nhất...</div>';
|
| 186 |
+
var compose=home.querySelector('.ai-compose');if(compose)compose.after(box);else home.prepend(box);
|
|
|
|
| 187 |
box.scrollIntoView({behavior:'smooth',block:'start'});
|
| 188 |
try{
|
| 189 |
var r=await fetch('/api/hashtag/sources?topic='+encodeURIComponent(topic)+'&offset=0&limit=6');
|
| 190 |
var j=await r.json();var sources=j.sources||[];
|
| 191 |
+
if(!sources.length){box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#888;font-size:12px;padding:8px">Không tìm được bài viết</div>';return;}
|
| 192 |
_htOffset=sources.length;
|
| 193 |
+
var h='<h3>🔍 '+esc(topic)+' <span style="font-size:10px;color:#888">('+j.total+' bài)</span></h3><div id="hashtag-src-list"></div>';
|
|
|
|
| 194 |
if(j.has_more)h+='<button class="hashtag-load-more" id="ht-load-more" onclick="loadMoreHashtag()">Tải thêm bài viết...</button>';
|
| 195 |
h+='<button class="hashtag-rewrite-btn" onclick="rewriteHashtagTopic(\''+esc(topic)+'\')">🤖 Rewrite AI tổng hợp & đăng tường</button>';
|
| 196 |
+
box.innerHTML=h;renderSources(sources,false);
|
| 197 |
+
}catch(e){box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#e74c3c;font-size:12px">Lỗi: '+esc(e.message)+'</div>';}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
};
|
| 199 |
+
window.loadMoreHashtag=async function(){var btn=document.getElementById('ht-load-more');if(btn){btn.textContent='Đang tải...';btn.disabled=true;}try{var r=await fetch('/api/hashtag/sources?topic='+encodeURIComponent(_htTopic)+'&offset='+_htOffset+'&limit=6');var j=await r.json();_htOffset+=j.sources.length;renderSources(j.sources,true);if(!j.has_more&&btn)btn.remove();else if(btn){btn.textContent='Tải thêm...';btn.disabled=false;}}catch(e){if(btn){btn.textContent='Lỗi';btn.disabled=false;}}};
|
| 200 |
window.rewriteHashtagTopic=async function(topic){var btn=event?.target;if(btn){btn.disabled=true;btn.textContent='Đang tổng hợp...';}try{var r=await fetch('/api/topic_post',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({topic:topic})});var j=await r.json();if(!r.ok||j.error)throw new Error(j.error||'Lỗi');if(btn)btn.textContent='✅ Đã đăng!';setTimeout(function(){document.getElementById('hashtag-sources-box')?.remove();},2000);}catch(e){if(btn){btn.disabled=false;btn.textContent='❌ '+e.message;}}};
|
| 201 |
window.createTopicPost=function(){var inp=document.getElementById('ai-topic-input');var topic=(inp&&inp.value||'').trim();if(!topic){alert('Nhập chủ đề');return;}showHashtagSources(topic);if(inp)inp.value='';};
|
| 202 |
window.createTopicPostFinal5=function(){var inp=document.getElementById('ai-topic-input-final5')||document.getElementById('ai-topic-input');var topic=(inp&&inp.value||'').trim();if(!topic){alert('Nhập chủ đề');return;}showHashtagSources(topic);if(inp)inp.value='';};
|