bep40 commited on
Commit
a82a808
·
verified ·
1 Parent(s): 0051c4f

Fix VTV loading, match detail, homepage speed

Browse files

1. VTV: reduce API timeout to 2.5s, HLS fail-fast (no retry), 8s per-source timeout
2. Match detail: show basic info even when scraping fails, display events if available
3. Homepage: add per-API timeouts (8-15s) so slow APIs don't block everything

Files changed (1) hide show
  1. static/app_v2.js +92 -24
static/app_v2.js CHANGED
@@ -80,15 +80,14 @@ function buildVTVBlockHTML(){
80
  }
81
 
82
  async function loadVTVStreams(){
83
- // Safety: always hide loader after 5s max
84
- const loadEl = document.getElementById('vtv-load');
85
  const hideLoader = () => { const el = document.getElementById('vtv-load'); if(el) el.style.display = 'none'; };
86
- setTimeout(hideLoader, 5000);
87
 
88
- let apiOk = false;
89
  try {
90
  const ctrl = new AbortController();
91
- const tid = setTimeout(() => ctrl.abort(), 4000);
92
  const r = await fetch('/api/vtv/streams', {signal: ctrl.signal});
93
  clearTimeout(tid);
94
  if(r.ok){
@@ -98,7 +97,6 @@ async function loadVTVStreams(){
98
  const sources = (info && info.all_sources && info.all_sources.length > 0) ? info.all_sources : (info && info.stream_url ? [info.stream_url] : []);
99
  if(sources.length > 0){
100
  _vtvStreams[ch.id] = sources.map(u => '/api/proxy/m3u8/vtv?url=' + encodeURIComponent(u));
101
- apiOk = true;
102
  }
103
  });
104
  }
@@ -112,7 +110,6 @@ async function loadVTVStreams(){
112
  const fallback = VTV_STATIC_SOURCES[ch.id] || [];
113
  _vtvStreams[ch.id] = fallback.map(u => '/api/proxy/m3u8/vtv?url=' + encodeURIComponent(u));
114
  }
115
- // Update tab: always show as available (opacity 1) since we have static fallback
116
  const tab = document.getElementById('vtvt-'+ch.id);
117
  if(tab){
118
  tab.classList.remove('off');
@@ -166,21 +163,34 @@ function _vtvTryPlay(video, urls, idx, name, loadEl, errEl, errMsg){
166
  const src = urls[idx];
167
  loadEl.innerHTML = '<div class="vtv-spinner"></div>Đang kết nối ' + name + ' (' + (idx+1) + '/' + urls.length + ')...';
168
  if(typeof Hls !== 'undefined' && Hls.isSupported()){
169
- const hls = new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:15,maxMaxBufferLength:30});
170
  _vtvHls = hls;
 
 
 
 
 
 
 
 
 
 
 
171
  hls.loadSource(src);
172
  hls.attachMedia(video);
173
- hls.on(Hls.Events.MANIFEST_PARSED, () => { video.play().catch(()=>{}); loadEl.style.display='none'; video.style.display='block'; });
174
- let recAttempts = 0;
 
 
 
 
175
  hls.on(Hls.Events.ERROR, (ev, data) => {
176
  if(data.fatal){
177
- if(data.type === Hls.ErrorTypes.NETWORK_ERROR){
178
- recAttempts++;
179
- if(recAttempts <= 2){ setTimeout(() => hls.startLoad(), 1500); }
180
- else { hls.destroy(); _vtvHls = null; _vtvTryPlay(video, urls, idx+1, name, loadEl, errEl, errMsg); }
181
- } else if(data.type === Hls.ErrorTypes.MEDIA_ERROR){
182
- try { hls.recoverMediaError(); } catch(e) {}
183
- } else { hls.destroy(); _vtvHls = null; _vtvTryPlay(video, urls, idx+1, name, loadEl, errEl, errMsg); }
184
  }
185
  });
186
  } else if(video.canPlayType('application/vnd.apple.mpegurl')){
@@ -197,13 +207,20 @@ async function loadHome(){
197
  // Load VTV streams in parallel (non-blocking)
198
  loadVTVStreams();
199
 
 
 
 
 
 
 
 
200
  const[featured,sh,wall,hlLeagues,ai,wcData]=await Promise.all([
201
- fetch('/api/livescore/featured').then(r=>r.json()).catch(()=>null),
202
- fetch('/api/shorts').then(r=>r.json()).catch(()=>[]),
203
- fetch('/api/wall').then(r=>r.json()).catch(()=>({posts:[]})),
204
- fetch('/api/highlights/leagues').then(r=>r.json()).catch(()=>({})),
205
- fetch('/api/genk_ai').then(r=>r.json()).catch(()=>[]),
206
- fetch('/api/wc2026').then(r=>r.json()).catch(()=>null)
207
  ]);
208
  _hlLeagueData=hlLeagues;
209
  _wc2026Data=wcData;
@@ -414,7 +431,58 @@ function bindMatchClicks(el){
414
  }
415
  function openMatch(id,url){if(!id)return;_currentEventId=id;if(url)_currentMatchUrl=url;document.getElementById('match-overlay').classList.add('active');document.body.style.overflow='hidden';loadMatchTab('detail')}
416
  function closeMatch(){document.getElementById('match-overlay').classList.remove('active');document.body.style.overflow=''}
417
- async function loadMatchTab(tab){document.querySelectorAll('.mo-tab').forEach(t=>t.classList.remove('active'));document.querySelectorAll('.mo-tab').forEach(t=>{if((tab==='comm'&&t.textContent==='Diễn biến')||(tab==='stats'&&t.textContent==='Thống kê')||(tab==='detail'&&t.textContent.includes('Chi tiết')))t.classList.add('active')});const el=document.getElementById('mo-body');if(!el)return;el.innerHTML='<div class="loading">Đang tải...</div>';try{let apiUrl;if(tab==='stats')apiUrl=`/api/match/${_currentEventId}/stats`;else if(tab==='comm')apiUrl=`/api/match/${_currentEventId}/commentaries`;else{apiUrl=`/api/match/${_currentEventId}/detail`;if(_currentMatchUrl)apiUrl+='?url='+encodeURIComponent(_currentMatchUrl)}const r=await fetch(apiUrl);if(!r.ok){el.innerHTML='<div class="loading">Lỗi máy chủ ('+r.status+')</div>';return}const d=await r.json();if(d.error){el.innerHTML='<div class="loading">'+esc(d.error)+'</div>';return}if(tab==='detail'&&typeof renderMatchDetail==='function'){renderMatchDetail(el,d);return}el.innerHTML=d.html||'<div class="loading">Không có dữ liệu</div>'}catch(e){el.innerHTML='<div class="loading">Lỗi</div>'}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  async function doInteract(videoId,type){try{const r=await fetch('/api/v2/interact',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:videoId,type})});return await r.json();}catch(e){return null;}}
419
  async function getInteractions(videoId){try{return await fetch('/api/v2/interactions?id='+encodeURIComponent(videoId)).then(r=>r.json());}catch(e){return{views:0,likes:0,comments:0};}}
420
  async function getComments(videoId){try{const j=await fetch('/api/v2/comments?id='+encodeURIComponent(videoId)).then(r=>r.json());return j.comments||[];}catch(e){return[];}}
 
80
  }
81
 
82
  async function loadVTVStreams(){
83
+ // Safety: always hide loader after 3s max
 
84
  const hideLoader = () => { const el = document.getElementById('vtv-load'); if(el) el.style.display = 'none'; };
85
+ setTimeout(hideLoader, 3000);
86
 
87
+ // Try API with short timeout
88
  try {
89
  const ctrl = new AbortController();
90
+ const tid = setTimeout(() => ctrl.abort(), 2500);
91
  const r = await fetch('/api/vtv/streams', {signal: ctrl.signal});
92
  clearTimeout(tid);
93
  if(r.ok){
 
97
  const sources = (info && info.all_sources && info.all_sources.length > 0) ? info.all_sources : (info && info.stream_url ? [info.stream_url] : []);
98
  if(sources.length > 0){
99
  _vtvStreams[ch.id] = sources.map(u => '/api/proxy/m3u8/vtv?url=' + encodeURIComponent(u));
 
100
  }
101
  });
102
  }
 
110
  const fallback = VTV_STATIC_SOURCES[ch.id] || [];
111
  _vtvStreams[ch.id] = fallback.map(u => '/api/proxy/m3u8/vtv?url=' + encodeURIComponent(u));
112
  }
 
113
  const tab = document.getElementById('vtvt-'+ch.id);
114
  if(tab){
115
  tab.classList.remove('off');
 
163
  const src = urls[idx];
164
  loadEl.innerHTML = '<div class="vtv-spinner"></div>Đang kết nối ' + name + ' (' + (idx+1) + '/' + urls.length + ')...';
165
  if(typeof Hls !== 'undefined' && Hls.isSupported()){
166
+ const hls = new Hls({enableWorker:true,lowLatencyMode:true,startLevel:-1,capLevelToPlayerSize:true,maxBufferLength:8,maxMaxBufferLength:15,maxFragLookUpTolerance:0.5});
167
  _vtvHls = hls;
168
+
169
+ // Timeout: if not loaded in 8s, try next source
170
+ const loadTimeout = setTimeout(() => {
171
+ if(_vtvHls === hls) {
172
+ console.warn(`VTV ${name} source ${idx+1} timeout, trying next...`);
173
+ hls.destroy();
174
+ _vtvHls = null;
175
+ _vtvTryPlay(video, urls, idx+1, name, loadEl, errEl, errMsg);
176
+ }
177
+ }, 8000);
178
+
179
  hls.loadSource(src);
180
  hls.attachMedia(video);
181
+ hls.on(Hls.Events.MANIFEST_PARSED, () => {
182
+ clearTimeout(loadTimeout);
183
+ video.play().catch(()=>{});
184
+ loadEl.style.display='none';
185
+ video.style.display='block';
186
+ });
187
  hls.on(Hls.Events.ERROR, (ev, data) => {
188
  if(data.fatal){
189
+ clearTimeout(loadTimeout);
190
+ // For any fatal error, try next source immediately
191
+ hls.destroy();
192
+ _vtvHls = null;
193
+ _vtvTryPlay(video, urls, idx+1, name, loadEl, errEl, errMsg);
 
 
194
  }
195
  });
196
  } else if(video.canPlayType('application/vnd.apple.mpegurl')){
 
207
  // Load VTV streams in parallel (non-blocking)
208
  loadVTVStreams();
209
 
210
+ // Helper: fetch with timeout
211
+ const fetchWithTimeout = (url, ms=12000) => {
212
+ const ctrl = new AbortController();
213
+ const tid = setTimeout(() => ctrl.abort(), ms);
214
+ return fetch(url, {signal: ctrl.signal}).finally(() => clearTimeout(tid));
215
+ };
216
+
217
  const[featured,sh,wall,hlLeagues,ai,wcData]=await Promise.all([
218
+ fetchWithTimeout('/api/livescore/featured', 8000).then(r=>r.json()).catch(()=>null),
219
+ fetchWithTimeout('/api/shorts', 15000).then(r=>r.json()).catch(()=>[]),
220
+ fetchWithTimeout('/api/wall', 8000).then(r=>r.json()).catch(()=>({posts:[]})),
221
+ fetchWithTimeout('/api/highlights/leagues', 15000).then(r=>r.json()).catch(()=>({})),
222
+ fetchWithTimeout('/api/genk_ai', 12000).then(r=>r.json()).catch(()=>[]),
223
+ fetchWithTimeout('/api/wc2026', 15000).then(r=>r.json()).catch(()=>null)
224
  ]);
225
  _hlLeagueData=hlLeagues;
226
  _wc2026Data=wcData;
 
431
  }
432
  function openMatch(id,url){if(!id)return;_currentEventId=id;if(url)_currentMatchUrl=url;document.getElementById('match-overlay').classList.add('active');document.body.style.overflow='hidden';loadMatchTab('detail')}
433
  function closeMatch(){document.getElementById('match-overlay').classList.remove('active');document.body.style.overflow=''}
434
+ async function loadMatchTab(tab){
435
+ document.querySelectorAll('.mo-tab').forEach(t=>t.classList.remove('active'));
436
+ document.querySelectorAll('.mo-tab').forEach(t=>{
437
+ if((tab==='comm'&&t.textContent==='Diễn biến')||(tab==='stats'&&t.textContent==='Thống kê')||(tab==='detail'&&t.textContent.includes('Chi tiết')))t.classList.add('active')
438
+ });
439
+ const el=document.getElementById('mo-body');
440
+ if(!el)return;
441
+ el.innerHTML='<div class="loading">Đang tải...</div>';
442
+ try{
443
+ let apiUrl;
444
+ if(tab==='stats')apiUrl=`/api/match/${_currentEventId}/stats`;
445
+ else if(tab==='comm')apiUrl=`/api/match/${_currentEventId}/commentaries`;
446
+ else{
447
+ apiUrl=`/api/match/${_currentEventId}/detail`;
448
+ if(_currentMatchUrl)apiUrl+='?url='+encodeURIComponent(_currentMatchUrl);
449
+ }
450
+ const r=await fetch(apiUrl);
451
+ if(!r.ok){el.innerHTML='<div class="loading">Lỗi máy chủ ('+r.status+')</div>';return}
452
+ const d=await r.json();
453
+ if(d.error){el.innerHTML='<div class="loading">'+esc(d.error)+'</div>';return}
454
+
455
+ // For detail tab: show match info even if scraping "found:false"
456
+ if(tab==='detail'){
457
+ if(typeof renderMatchDetail==='function'){
458
+ renderMatchDetail(el,d);
459
+ } else if(d.info && (d.info.home_team || d.info.home)){
460
+ // Basic info display when renderMatchDetail not available
461
+ const info = d.info;
462
+ const home = info.home_team || info.home || '';
463
+ const away = info.away_team || info.away || '';
464
+ const score = info.score || 'VS';
465
+ const league = info.league || info.status_label || '';
466
+ el.innerHTML = `<div class="match-basic-info">
467
+ <div class="mbl-league">${esc(league)}</div>
468
+ <div class="mbl-teams">
469
+ <div class="mbl-team">${esc(home)}</div>
470
+ <div class="mbl-score">${esc(score)}</div>
471
+ <div class="mbl-team">${esc(away)}</div>
472
+ </div>
473
+ ${d.found===false ? '<div class="mbl-notice">⚠️ Chi tiết trận đấu chưa có sẵn. Vui lòng thử lại sau.</div>' : ''}
474
+ ${d.events ? '<div class="mbl-events"><h4>Diễn biến</h4>' + d.events.map(ev => `<div class="mbl-ev ${ev.team}"><span class="mbl-ev-time">${ev.time||''}</span> ${ev.type==='goal'?'⚽':ev.type==='redcard'?'🟥':ev.type==='yellowcard'?'🟨':'↔️'} ${ev.players||''}</div>`).join('') + '</div>' : ''}
475
+ </div>`;
476
+ } else {
477
+ el.innerHTML='<div class="loading">Không tìm thấy dữ liệu trận đấu</div>';
478
+ }
479
+ return;
480
+ }
481
+ el.innerHTML=d.html||'<div class="loading">Không có dữ liệu</div>';
482
+ }catch(e){
483
+ el.innerHTML='<div class="loading">Lỗi kết nối</div>';
484
+ }
485
+ }
486
  async function doInteract(videoId,type){try{const r=await fetch('/api/v2/interact',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:videoId,type})});return await r.json();}catch(e){return null;}}
487
  async function getInteractions(videoId){try{return await fetch('/api/v2/interactions?id='+encodeURIComponent(videoId)).then(r=>r.json());}catch(e){return{views:0,likes:0,comments:0};}}
488
  async function getComments(videoId){try{const j=await fetch('/api/v2/comments?id='+encodeURIComponent(videoId)).then(r=>r.json());return j.comments||[];}catch(e){return[];}}