VNEWS / app_final.py
bep40's picture
Squash main
4b6e868
Raw
History Blame Contribute Delete
19.6 kB
"""Final wrapper with complete highlight override including interaction buttons.
PLUS: Hashtag inline sources on homepage with rewrite button."""
import json, os, time
from app_patch_unified import *
from app_patch_unified import app, UNIFIED_INJECT, f5, f6, rt, PATCH_INJECT
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi import Request, Query
DATA_DIR="/data" if os.path.isdir('/data') else "/app/data"
os.makedirs(DATA_DIR,exist_ok=True)
HL_STATS_FILE=os.path.join(DATA_DIR,'highlight_stats.json')
def _load_hl():
try:
if os.path.exists(HL_STATS_FILE):return json.load(open(HL_STATS_FILE,'r',encoding='utf-8'))
except:pass
return {}
def _save_hl(db):
try:open(HL_STATS_FILE+'.tmp','w',encoding='utf-8').write(json.dumps(db,ensure_ascii=False));os.replace(HL_STATS_FILE+'.tmp',HL_STATS_FILE)
except:pass
app.router.routes=[r for r in app.router.routes if not (
(getattr(r,'path',None)=='/api/highlight/interact' and 'POST' in getattr(r,'methods',set())) or
(getattr(r,'path',None)=='/api/highlight/stats' and 'GET' in getattr(r,'methods',set())) or
(getattr(r,'path',None)=='/api/hashtag/sources' and 'GET' in getattr(r,'methods',set())) or
(getattr(r,'path',None)=='/' and 'GET' in getattr(r,'methods',set()))
)]
@app.post('/api/highlight/interact')
async def _hl_act(request:Request):
b=await request.json();vid=str(b.get('id','')).strip();action=str(b.get('action','')).strip()
if not vid or action not in ('view','like','share'):return JSONResponse({'error':'invalid'},status_code=400)
db=_load_hl();st=db.get(vid,{'views':0,'likes':0,'shares':0})
st[action+'s']=st.get(action+'s',0)+1
db[vid]=st;_save_hl(db);return JSONResponse({'stats':st})
@app.get('/api/highlight/stats')
def _hl_stats(ids:str=Query(default='')):
db=_load_hl();out={}
for vid in ids.split(','):
vid=vid.strip()
if vid:out[vid]=db.get(vid,{'views':0,'likes':0,'shares':0})
return JSONResponse({'stats':out})
@app.get('/api/hashtag/sources')
def _hashtag_sources(topic:str=Query(...)):
"""Return sources for a hashtag topic to display inline on homepage."""
research=f6._fast_context(topic) if hasattr(f6,'_fast_context') else f6._web_research_context(topic)
sources=research.get('sources',[])
# Add og:image for each source
from ai_runtime_patch_fast import _scrape
for s in sources[:6]:
if s.get('url') and not s.get('img'):
try:_,_,img=_scrape(s['url'],500)
except:img=''
s['img']=img if img and len(img)>20 else ''
return JSONResponse({'sources':sources[:6],'topic':topic})
# PRE_KILL fix
UNIFIED_INJECT_FIXED = UNIFIED_INJECT.replace(
"""Object.defineProperty(window,'renderAIShorts7',{get:function(){return function(){}},set:function(){},configurable:true});""",
"""Object.defineProperty(window,'renderAIShorts7',{get:function(){return function(){}},set:function(){},configurable:true});
Object.defineProperty(window,'renderPatchedWall',{get:function(){return function(){}},set:function(){},configurable:true});
Object.defineProperty(window,'renderAiShorts',{get:function(){return function(){}},set:function(){},configurable:true});
Object.defineProperty(window,'renderWall',{get:function(){return function(){}},set:function(){},configurable:true});
Object.defineProperty(window,'renderAIShorts',{get:function(){return function(){}},set:function(){},configurable:true});
Object.defineProperty(window,'loadPatchedWall',{get:function(){return function(){}},set:function(){},configurable:true});
Object.defineProperty(window,'refreshFinalWall3',{get:function(){return function(){}},set:function(){},configurable:true});"""
)
# Fix highlight fetch
UNIFIED_INJECT_FIXED = UNIFIED_INJECT_FIXED.replace(
"var articles=(window._hlLeagueData||{})[league]||[];\n if(!articles.length){el.innerHTML=",
"var articles=(window._hlLeagueData||{})[league]||[];\n if(!articles.length){try{var _r=await fetch('/api/highlights/'+league);articles=await _r.json();if(!Array.isArray(articles))articles=[];}catch(e){articles=[];}}\n if(!articles.length){el.innerHTML="
)
# Highlight full override (same as 5a5b626)
HIGHLIGHT_FULL_OVERRIDE = r'''
<style>
.tiktok-slide.ratio-wide video,.tiktok-slide.ratio-wide iframe{object-fit:contain!important}
.hl-ask-panel{position:fixed;bottom:0;left:0;right:0;max-height:50vh;background:#181818;border-radius:16px 16px 0 0;z-index:99999;padding:14px;display:none;overflow-y:auto}.hl-ask-panel.active{display:block}.hl-ask-panel textarea,.hl-ask-panel input{width:100%;background:#222;border:1px solid #444;color:#eee;border-radius:10px;padding:9px;margin:6px 0}.hl-ask-panel button{background:#2d8659;border:0;color:#fff;border-radius:10px;padding:8px 12px;margin:4px}.hl-ask-answer{white-space:pre-wrap;color:#ccc;font-size:12px;margin-top:8px}
.hashtag-sources{margin:8px 4px;background:#1a1a1a;border:1px solid #2a2a2a;border-radius:10px;padding:10px}.hashtag-sources h3{font-size:13px;color:#5cb87a;margin-bottom:8px}.hashtag-src-item{display:flex;gap:8px;padding:8px;background:#202020;border-radius:8px;margin:6px 0;cursor:pointer}.hashtag-src-item:active{opacity:.8}.hashtag-src-img{flex:0 0 80px;aspect-ratio:16/9;background:#333;border-radius:6px;overflow:hidden}.hashtag-src-img img{width:100%;height:100%;object-fit:cover}.hashtag-src-text{flex:1;min-width:0}.hashtag-src-title{font-size:12px;font-weight:700;color:#eee;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.hashtag-src-via{font-size:10px;color:#888;margin-top:2px}.hashtag-rewrite-btn{width:100%;margin-top:8px;background:#2d8659;border:0;color:#fff;padding:9px;border-radius:10px;font-size:12px;font-weight:700;cursor:pointer}
</style>
<div id="hl-ask-panel" class="hl-ask-panel"></div>
<script>
(function(){
function esc(s){return String(s||'').replace(/[&<>"']/g,function(m){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]});}
// === HASHTAG INLINE: click hashtag → show sources on homepage + rewrite button ===
window.showHashtagSources=async function(topic){
var home=document.getElementById('view-home');if(!home)return;
document.getElementById('hashtag-sources-box')?.remove();
var box=document.createElement('div');box.id='hashtag-sources-box';box.className='hashtag-sources';
box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#888;font-size:11px">Đang tìm nguồn...</div>';
var compose=home.querySelector('.ai-compose');
if(compose)compose.after(box);else home.prepend(box);
try{
var r=await fetch('/api/hashtag/sources?topic='+encodeURIComponent(topic));
var j=await r.json();var sources=j.sources||[];
if(!sources.length){box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#888;font-size:12px">Không tìm được nguồn</div>';return;}
var h='<h3>🔍 '+esc(topic)+' <span style="font-size:10px;color:#888">('+sources.length+' nguồn)</span></h3>';
sources.forEach(function(s){
h+='<div class="hashtag-src-item" onclick="if(typeof readArticle===\'function\')readArticle(\''+esc(s.url||'')+'\')">';
h+='<div class="hashtag-src-img">'+(s.img?'<img src="'+esc(s.img)+'" onerror="this.style.display=\'none\'">':'')+'</div>';
h+='<div class="hashtag-src-text"><div class="hashtag-src-title">'+esc(s.title)+'</div><div class="hashtag-src-via">'+esc(s.via||s.source||'')+'</div></div>';
h+='</div>';
});
h+='<button class="hashtag-rewrite-btn" onclick="rewriteHashtagTopic(\''+esc(topic)+'\')">🤖 Rewrite AI tổng hợp nguồn & đăng tường</button>';
box.innerHTML=h;
}catch(e){box.innerHTML='<h3>🔍 '+esc(topic)+'</h3><div style="color:#e74c3c;font-size:12px">Lỗi: '+esc(e.message)+'</div>';}
};
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 lên Tường AI!';
setTimeout(function(){document.getElementById('hashtag-sources-box')?.remove();},2000);
}catch(e){
if(btn){btn.disabled=false;btn.textContent='❌ '+e.message;}
}
};
// Override hashtag chip click to use showHashtagSources instead of topic input
setTimeout(function(){
document.querySelectorAll('.hot-chip').forEach(function(chip){
chip.onclick=function(e){
e.preventDefault();e.stopPropagation();
var topic=chip.textContent.replace(/^#/,'').trim();
if(topic)showHashtagSources(topic);
};
});
},3000);
// Re-patch after hot topics load
setInterval(function(){
document.querySelectorAll('.hot-chip:not([data-patched])').forEach(function(chip){
chip.dataset.patched='1';
chip.onclick=function(e){
e.preventDefault();e.stopPropagation();
var topic=chip.textContent.replace(/^#/,'').trim();
if(topic)showHashtagSources(topic);
};
});
},2000);
// === FULL openLeaguePlayer override (same as before) ===
window.openLeaguePlayer=async function(league,idx){
showView('view-tiktok');document.querySelectorAll('.cat').forEach(function(x){x.classList.remove('active')});
var el=document.getElementById('view-tiktok');el.innerHTML='<div class="loading">Đang tải highlight...</div>';
var cfg=(window.HL_CONFIG||{})[league]||{name:league,emoji:'🎬'};
var articles=(window._hlLeagueData||{})[league]||[];
if(!articles.length){try{var resp=await fetch('/api/highlights/'+league);articles=await resp.json();if(!Array.isArray(articles))articles=[];}catch(e){articles=[];}}
if(!articles.length){el.innerHTML='<div class="loading">Không có video</div>';return;}
var vids=[];var results=await Promise.all(articles.map(async function(a,i){try{var r=await fetch('/api/video_url?url='+encodeURIComponent(a.link));var v=await r.json();if(v&&v.src)return Object.assign({},a,v,{_idx:i});}catch(e){}return null;}));results.forEach(function(r){if(r)vids.push(r);});vids.sort(function(a,b){return a._idx-b._idx;});
if(!vids.length){el.innerHTML='<div class="loading">Không tìm thấy video</div>';return;}
var ti=vids.findIndex(function(v){return v._idx===idx;});if(ti<0)ti=0;var ordered=ti>0?vids.slice(ti).concat(vids.slice(0,ti)):vids;
var h='<button class="back-btn" onclick="switchCat(\'home\')">← '+esc(cfg.emoji)+' '+esc(cfg.name)+'</button><div class="tiktok-container"><div class="tiktok-feed" id="tiktok-feed">';
ordered.forEach(function(v,i){var hlid=encodeURIComponent(v.link||v.title);var isYT=v.type==='youtube';var isHLS=!isYT&&v.src&&v.src.indexOf('.m3u8')>-1;var poster=v.poster?' poster="'+v.poster+'"':'';var vtag=isYT?'<iframe data-yt-src="'+v.src+'" allowfullscreen allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture" style="width:100%;height:100%;border:none"></iframe>':isHLS?'<video playsinline preload="none"'+poster+' data-hls="'+v.src+'" loop controls style="width:100%;height:100%;object-fit:cover"></video>':'<video playsinline preload="none"'+poster+' loop controls style="width:100%;height:100%;object-fit:cover"><source src="'+v.src+'" type="video/mp4"></video>';h+='<div class="tiktok-slide" id="tslide-'+i+'" data-hlid="'+hlid+'">'+vtag+'<div class="tiktok-bottom"><span class="badge badge-fpt">'+esc(cfg.name)+'</span><p class="tiktok-title">'+esc(v.title)+'</p></div><div class="tiktok-right"><button class="tiktok-right-btn" onclick="event.stopPropagation();hlAct(this,\'view\')"><div class="icon">👁</div><div class="count" data-a="views">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();hlAct(this,\'like\')"><div class="icon">❤️</div><div class="count" data-a="likes">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();openHlComments(\''+hlid+'\')"><div class="icon">💬</div><div class="count">BL</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();openHlAsk(\''+hlid+'\',\''+esc(v.title)+'\')"><div class="icon">🤖</div><div class="count">Hỏi</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();hlAct(this,\'share\');if(typeof doShareVideo===\'function\')doShareVideo(\''+esc(v.title)+'\',\''+esc(v.link||'')+'\',\''+esc(v.poster||v.img||'')+'\',\'highlights\')"><div class="icon">📤</div><div class="count" data-a="shares">0</div></button><button class="tiktok-right-btn" onclick="event.stopPropagation();toggleHlRatio(this)"><div class="icon">⬜</div><div class="count">16:9</div></button></div><span class="tiktok-counter">'+(i+1)+'/'+ordered.length+'</span></div>';});
h+='</div></div>';el.innerHTML=h;
var feed=document.getElementById('tiktok-feed');if(!feed)return;var slides=feed.querySelectorAll('.tiktok-slide');var cur=-1;
function act(i){if(i===cur)return;slides.forEach(function(sl,idx){var v=sl.querySelector('video');var fr=sl.querySelector('iframe');if(idx===i){if(v&&v.dataset.hls){if(!v._hls&&typeof Hls!=='undefined'&&Hls.isSupported()){var hls=new Hls();hls.loadSource(v.dataset.hls);hls.attachMedia(v);hls.on(Hls.Events.MANIFEST_PARSED,function(){v.play().catch(function(){});});v._hls=hls;}else if(v._hls)v.play().catch(function(){});}else if(v)v.play().catch(function(){});if(fr&&!fr.src&&fr.dataset.ytSrc)fr.src=fr.dataset.ytSrc;hlAct(sl.querySelector('.tiktok-right .tiktok-right-btn'),'view');}else{if(v){v.pause();if(v._hls){v._hls.destroy();v._hls=null;}}if(fr&&fr.src)fr.src='';}});cur=i;}
var sT;feed.addEventListener('scroll',function(){clearTimeout(sT);sT=setTimeout(function(){var rect=feed.getBoundingClientRect(),ctr=rect.top+rect.height/2,best=-1,bestD=1e9;slides.forEach(function(sl,i){var d=Math.abs(sl.getBoundingClientRect().top+sl.getBoundingClientRect().height/2-ctr);if(d<bestD){bestD=d;best=i;}});if(best>=0)act(best);},150);});
setTimeout(function(){act(0);},400);slides.forEach(function(sl){var v=sl.querySelector('video');if(v)v.addEventListener('click',function(e){e.preventDefault();v.paused?v.play().catch(function(){}):v.pause();});});
var ids=[];slides.forEach(function(sl){if(sl.dataset.hlid)ids.push(sl.dataset.hlid);});
if(ids.length)fetch('/api/highlight/stats?ids='+ids.join(',')).then(function(r){return r.json()}).then(function(j){var stats=j.stats||{};slides.forEach(function(sl){var st=stats[sl.dataset.hlid];if(!st)return;var r=sl.querySelector('.tiktok-right');if(!r)return;var vc=r.querySelector('[data-a="views"]');if(vc)vc.textContent=st.views||0;var lc=r.querySelector('[data-a="likes"]');if(lc)lc.textContent=st.likes||0;var sc=r.querySelector('[data-a="shares"]');if(sc)sc.textContent=st.shares||0;});}).catch(function(){});
};
window.hlAct=async function(btn,action){var slide=btn?btn.closest('.tiktok-slide'):null;var id=slide?slide.dataset.hlid:'';if(!id)return;try{var r=await fetch('/api/highlight/interact',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id,action:action})});var j=await r.json();if(j.stats&&slide){var right=slide.querySelector('.tiktok-right');if(right){var vc=right.querySelector('[data-a="views"]');if(vc)vc.textContent=j.stats.views||0;var lc=right.querySelector('[data-a="likes"]');if(lc)lc.textContent=j.stats.likes||0;var sc=right.querySelector('[data-a="shares"]');if(sc)sc.textContent=j.stats.shares||0;}}}catch(e){}};
window.toggleHlRatio=function(btn){var slide=btn.closest('.tiktok-slide');if(!slide)return;slide.classList.toggle('ratio-wide');var label=btn.querySelector('.count');if(label)label.textContent=slide.classList.contains('ratio-wide')?'1:1':'16:9';};
window.openHlComments=async function(id){var panel=document.getElementById('hl-ask-panel');var j=await fetch('/api/short/comments?id='+id).then(function(r){return r.json()}).catch(function(){return{comments:[]}});var cmts=j.comments||[];panel.innerHTML='<h3 style="color:#5cb87a;font-size:14px">💬 Bình luận</h3><div id="hl-cmt-list">'+(cmts.map(function(c){return'<div style="background:#222;border-radius:8px;padding:7px;margin:5px 0;color:#ccc;font-size:12px">'+esc(c.text)+'</div>'}).join('')||'<div style="color:#777;font-size:12px">Chưa có</div>')+'</div><textarea id="hl-cmt-text" placeholder="Bình luận..."></textarea><button onclick="submitHlCmt(\''+id+'\')">Gửi</button><button onclick="document.getElementById(\'hl-ask-panel\').classList.remove(\'active\')">Đóng</button>';panel.classList.add('active');};
window.submitHlCmt=async function(id){var t=document.getElementById('hl-cmt-text');if(!t||!t.value.trim())return;var j=await fetch('/api/short/comment',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id,text:t.value.trim()})}).then(function(r){return r.json()}).catch(function(){return{comments:[]}});document.getElementById('hl-cmt-list').innerHTML=(j.comments||[]).map(function(c){return'<div style="background:#222;border-radius:8px;padding:7px;margin:5px 0;color:#ccc;font-size:12px">'+esc(c.text)+'</div>'}).join('');t.value='';};
window.openHlAsk=function(id,title){var panel=document.getElementById('hl-ask-panel');panel.innerHTML='<h3 style="color:#5cb87a;font-size:14px">🤖 Hỏi AI</h3><input id="hl-ask-q" placeholder="Hỏi về: '+esc(title)+'..."><div id="hl-ask-ans" class="hl-ask-answer"></div><button onclick="submitHlAsk(\''+id+'\',\''+esc(title)+'\')">Hỏi</button><button onclick="document.getElementById(\'hl-ask-panel\').classList.remove(\'active\')">Đóng</button>';panel.classList.add('active');};
window.submitHlAsk=async function(id,title){var q=document.getElementById('hl-ask-q');if(!q||!q.value.trim())return;var ans=document.getElementById('hl-ask-ans');ans.textContent='Đang hỏi...';try{var r=await fetch('/api/article/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question:q.value.trim(),context:'Video highlight: '+decodeURIComponent(title||id)})});var j=await r.json();ans.textContent=j.answer||'Không trả lời được';}catch(e){ans.textContent='Lỗi: '+e.message}};
})();
</script>
'''
EXTRA_WALL_FIX = r'''
<style>[data-wall-live="1"]{display:none!important}</style>
<script>
(function(){
var _wc=setInterval(function(){
var home=document.getElementById('view-home');if(!home||!home.classList.contains('active'))return;
var has=document.getElementById('short-ai-final-slide');
if(!has&&typeof renderShortAISlide==='function')renderShortAISlide();
if(!document.querySelector('.slider-wrap[data-wall-live]')){
fetch('/api/ai_wall').then(function(r){return r.json()}).then(function(j){
var posts=(j&&j.posts)||[];if(!posts.length)return;
if(typeof window._serverWall!=='undefined')window._serverWall=posts;
if(typeof prependWallPost==='function')prependWallPost(posts[0]);
}).catch(function(){});
}
},4000);
setTimeout(function(){clearInterval(_wc);},30000);
})();
</script>
'''
@app.get('/')
async def _index_fixed():
html=f5.f4.f3.f2.f1._load_index_html()
body=''
body+=getattr(rt.old,'PATCH_INJECT','')
body+=f5.f4.f3.f2.f1.FINAL_INJECT+f5.f4.f3.FINAL3_INJECT+f5.f4.FINAL4_INJECT+f5.FINAL5_INJECT
body+=getattr(f6,'FINAL6_INJECT','')
body+=getattr(f6,'FINAL6_FAST_HOME_INJECT','')
body+=getattr(f6,'FINAL6E_INJECT','')
body+=PATCH_INJECT
body+=UNIFIED_INJECT_FIXED
body+=HIGHLIGHT_FULL_OVERRIDE
body+=EXTRA_WALL_FIX
return HTMLResponse(html.replace('</body>',body+'\n</body>') if '</body>' in html else html+body)