bep40 commited on
Commit
0efe1a3
·
verified ·
1 Parent(s): 94c4db9

v4: client-side scraping + fallback to bongda.com.vn link

Browse files
Files changed (1) hide show
  1. static/match_detail.js +185 -153
static/match_detail.js CHANGED
@@ -1,5 +1,5 @@
1
- // === VNEWS — Match Detail UI (v3) ===
2
- // Scrapes bongda.com.vn directly via CORS proxy as fallback
3
  (function(){
4
  if(window._mdLoaded) return;
5
  window._mdLoaded = true;
@@ -70,6 +70,8 @@
70
  .md-no-data{text-align:center;color:#666;padding:16px 8px;font-size:12px}
71
  .md-err{color:#e85d04;font-size:12px;padding:8px;background:#2a1a1a;border-radius:6px;margin-bottom:6px;line-height:1.5}
72
  .md-warn{color:#f0c040;font-size:12px;padding:8px;background:#2a2a1a;border-radius:6px;margin-bottom:6px;line-height:1.5}
 
 
73
  @keyframes mdspin{to{transform:rotate(360deg)}}
74
  @keyframes mdpulse{0%,100%{opacity:1}50%{opacity:.5}}
75
  `;
@@ -90,129 +92,187 @@
90
 
91
  // Client-side HTML parser for bongda.com.vn
92
  function parseBongdaHtml(html, eventId) {
93
- const parser = new DOMParser();
94
- const doc = parser.parseFromString(html, 'text/html');
95
- const result = {event_id: eventId, found: false, sections: []};
96
-
97
- function cl(s) { return (s || '').replace(/\s+/g, ' ').trim(); }
98
-
99
- // Parse teams
100
- const teamsEl = doc.querySelector('.teams');
101
- if (teamsEl) {
102
- const info = {};
103
- const homeEl = teamsEl.querySelector('.team.home') || teamsEl.querySelector('[class*="home"]');
104
- if (homeEl) {
105
- const nameEl = homeEl.querySelector('p:not(.logo)') || homeEl.querySelector('p');
106
- if (nameEl) info.home_team = cl(nameEl.textContent);
107
- const logoEl = homeEl.querySelector('img');
108
- if (logoEl) info.home_logo = logoEl.getAttribute('src') || '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  }
110
- const awayEl = teamsEl.querySelector('.team.away') || teamsEl.querySelector('[class*="away"]');
111
- if (awayEl) {
112
- const nameEl = awayEl.querySelector('p:not(.logo)') || awayEl.querySelector('p');
113
- if (nameEl) info.away_team = cl(nameEl.textContent);
114
- const logoEl = awayEl.querySelector('img');
115
- if (logoEl) info.away_logo = logoEl.getAttribute('src') || '';
 
 
 
116
  }
117
- const scoreEl = teamsEl.querySelector('.score');
118
- if (scoreEl) {
119
- const parts = Array.from(scoreEl.querySelectorAll('p')).map(p => cl(p.textContent));
120
- if (parts.length >= 2) info.score = parts[0] + ' - ' + parts[1];
121
- const label = scoreEl.querySelector('.label');
122
- if (label) info.status_label = cl(label.textContent);
 
 
 
 
 
 
 
 
 
 
 
 
123
  }
124
- if (info.home_team && info.away_team) {
125
- result.info = info;
126
- result.found = true;
127
- result.sections.push('info');
128
  }
129
- }
130
-
131
- // Parse match info
132
- const mi = doc.querySelector('.match-info');
133
- if (mi) {
134
- const te = mi.querySelector('.times') || mi.querySelector('li');
135
- if (te) {
136
- result.info = result.info || {};
137
- result.info.datetime = cl(te.textContent);
 
 
 
 
 
138
  }
139
- }
140
-
141
- // Parse timeline
142
- const timeline = [];
143
- const tlEl = doc.querySelector('.timeline');
144
- if (tlEl) {
145
- let half = '';
146
- for (const c of tlEl.children) {
147
- if (!c.tagName) continue;
148
- const t = cl(c.textContent);
149
- if (!t) continue;
150
- if (['H1','H2','Hiệp 1','Hiệp 2'].includes(t)) { half = t; continue; }
151
- const m = t.match(/^(\d+'\+?\d*)/);
152
- if (m) {
153
- timeline.push({time: m[1], text: t.substring(m[0].length).trim(), half: half});
154
- } else if (t.length > 5) {
155
- timeline.push({time: '', text: t, half: half});
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  }
157
  }
 
 
 
 
 
 
 
 
158
  }
159
- if (timeline.length > 0) {
160
- result.timeline = timeline;
161
- result.sections.push('commentaries');
162
- }
163
-
164
- // Parse events
165
- const events = [];
166
- doc.querySelectorAll('.event').forEach(function(el) {
167
- const e = {};
168
- const cls = el.className || '';
169
- e.team = cls.includes('home') ? 'home' : (cls.includes('away') ? 'away' : '');
170
- const players = Array.from(el.querySelectorAll('p')).map(p => cl(p.textContent)).filter(Boolean);
171
- if (players.length > 0) e.players = players;
172
- events.push(e);
173
- });
174
- if (events.length > 0) {
175
- result.events = events;
176
- result.sections.push('events');
177
- }
178
-
179
- // Parse stats
180
- let statsHtml = '';
181
- for (const sel of ['.match-stats', '[class*="stats"]']) {
182
- const el = doc.querySelector(sel);
183
- if (el && el.innerHTML.length > 50) { statsHtml = el.innerHTML; break; }
184
  }
185
- if (statsHtml) {
186
- result.stats_html = statsHtml;
187
- result.sections.push('stats');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  }
 
 
 
 
 
 
 
189
 
190
- // Parse H2H
191
- const h2h = [];
192
- for (const sel of ['.head-to-head', '[class*="h2h"]']) {
193
- const el = doc.querySelector(sel);
194
- if (el && el.innerHTML.length > 50) {
195
- el.querySelectorAll('li, tr').forEach(function(it) {
196
- const cells = it.querySelectorAll('td, span, p');
197
- if (cells.length >= 3) {
198
- const m = {
199
- date: cl(cells[0]?.textContent || ''),
200
- home: cl(cells[1]?.textContent || ''),
201
- score: cl(cells[2]?.textContent || ''),
202
- };
203
- if (cells.length > 3) m.away = cl(cells[3]?.textContent || '');
204
- if (m.home) h2h.push(m);
205
  }
206
- });
207
- if (h2h.length > 0) break;
208
  }
209
  }
210
- if (h2h.length > 0) {
211
- result.h2h = h2h;
212
- result.sections.push('h2h');
213
- }
214
-
215
- return result;
216
  }
217
 
218
  window.loadMatchTab = async function(tab){
@@ -237,7 +297,7 @@
237
 
238
  let data = null;
239
 
240
- // Strategy 1: Try server-side /live endpoint
241
  try{
242
  let apiUrl = '/api/match/' + eventId + '/live';
243
  if(matchUrl) apiUrl += '?page_url=' + encodeURIComponent(matchUrl);
@@ -248,7 +308,7 @@
248
  }
249
  } catch(e) {}
250
 
251
- // Strategy 2: Try server-side /detail endpoint
252
  if(!data){
253
  try{
254
  let apiUrl = '/api/match/' + eventId + '/detail';
@@ -261,51 +321,24 @@
261
  } catch(e) {}
262
  }
263
 
264
- // Strategy 3: Client-side scraping via CORS proxy
265
  if(!data || !data.found){
266
- const bongdaUrls = [];
267
- if(matchUrl){
268
- const cleanUrl = matchUrl.replace(/\/(preview|centre)\/.*/, '');
269
- bongdaUrls.push(cleanUrl + '/centre/', cleanUrl + '/preview/');
270
- }
271
- bongdaUrls.push(
272
- 'https://bongda.com.vn/tran-dau/' + eventId + '/centre/',
273
- 'https://bongda.com.vn/tran-dau/' + eventId + '/preview/'
274
- );
275
-
276
- const corsProxies = [
277
- 'https://api.allorigins.win/raw?url=',
278
- 'https://corsproxy.io/?',
279
- ];
280
-
281
- for(const bUrl of bongdaUrls){
282
- for(const proxy of corsProxies){
283
- try{
284
- const r = await fetch(proxy + encodeURIComponent(bUrl), {signal: AbortSignal.timeout(20000)});
285
- if(r.ok){
286
- const html = await r.text();
287
- if(html && html.length > 1000 && html.includes('.teams')){
288
- // Check Portugal/Chile or team names in the HTML
289
- data = parseBongdaHtml(html, eventId);
290
- if(data.found && data.info && data.info.home_team && data.info.away_team){
291
- el.innerHTML = '<div class="md-no-data" style="font-size:10px;color:#5cb87a;text-align:center;padding:4px">📡 Dữ liệu tải trực tiếp từ bongda.com.vn</div>';
292
- break;
293
- }
294
- data = null;
295
- }
296
- }
297
- } catch(e) {}
298
- if(data && data.found) break;
299
  }
300
- if(data && data.found) break;
301
  }
302
  }
303
 
304
- // Render
305
  if(data && data.found && data.info && data.info.home_team){
306
  renderMatchDetail(el, data);
307
  } else {
308
- el.innerHTML = '<div class="md-warn">⚠️ Không có dữ liệu chi tiết cho trận đấu này.<br><small>Event ID: ' + esc(String(eventId)) + '</small><br><button onclick="window.loadMatchTab(\'detail\')" style="margin-top:8px;background:#0066cc;border:none;color:#fff;padding:6px 14px;border-radius:8px;font-size:11px;cursor:pointer">Thử lại</button></div>';
 
 
309
  }
310
  };
311
 
@@ -341,7 +374,6 @@
341
  if(info.away_logo) h += '<img src="'+esc(info.away_logo)+'" alt="'+esc(info.away_team)+'" onerror="this.style.display=\'none\'">';
342
  h += '<span>'+esc(info.away_team)+'</span></div>';
343
  h += '</div>';
344
- // Info row
345
  const infoItems = [];
346
  if(info.datetime) infoItems.push('📅 <b>'+esc(info.datetime)+'</b>');
347
  if(info.date && !info.datetime) infoItems.push('📅 <b>'+esc(info.date)+'</b>');
@@ -389,7 +421,7 @@
389
  // 3. H2H matches
390
  if(data.h2h && data.h2h.length > 0){
391
  hasAnyContent = true;
392
- h += '<div class="md-sec"><div class="md-sec-title">🔄 Lịch sử đối đấu</div>';
393
  data.h2h.slice(0, 10).forEach(function(m){
394
  h += '<div class="md-h2h-item"><span class="md-h2h-date">'+esc(m.date||'')+'</span>';
395
  if(m.league) h += '<span class="md-h2h-league">'+esc(m.league)+'</span>';
@@ -415,7 +447,7 @@
415
  h += '</div>';
416
  }
417
 
418
- // 5. Events (substitutions)
419
  if(data.events && data.events.length > 0){
420
  hasAnyContent = true;
421
  h += '<div class="md-sec"><div class="md-sec-title">🔄 Thay đổi cầu thủ</div><div class="md-events">';
 
1
+ // === VNEWS — Match Detail UI (v4) ===
2
+ // Client-side scraping with multiple fallback strategies
3
  (function(){
4
  if(window._mdLoaded) return;
5
  window._mdLoaded = true;
 
70
  .md-no-data{text-align:center;color:#666;padding:16px 8px;font-size:12px}
71
  .md-err{color:#e85d04;font-size:12px;padding:8px;background:#2a1a1a;border-radius:6px;margin-bottom:6px;line-height:1.5}
72
  .md-warn{color:#f0c040;font-size:12px;padding:8px;background:#2a2a1a;border-radius:6px;margin-bottom:6px;line-height:1.5}
73
+ .md-open-ext{display:inline-block;margin-top:10px;padding:8px 16px;background:#2d8659;color:#fff;border-radius:8px;text-decoration:none;font-size:12px;font-weight:600;cursor:pointer}
74
+ .md-open-ext:hover{background:#3a9d6a}
75
  @keyframes mdspin{to{transform:rotate(360deg)}}
76
  @keyframes mdpulse{0%,100%{opacity:1}50%{opacity:.5}}
77
  `;
 
92
 
93
  // Client-side HTML parser for bongda.com.vn
94
  function parseBongdaHtml(html, eventId) {
95
+ try {
96
+ const parser = new DOMParser();
97
+ const doc = parser.parseFromString(html, 'text/html');
98
+ const result = {event_id: eventId, found: false, sections: []};
99
+
100
+ function cl(s) { return (s || '').replace(/\s+/g, ' ').trim(); }
101
+
102
+ // Parse teams from .teams section
103
+ const teamsEl = doc.querySelector('.teams');
104
+ if (teamsEl) {
105
+ const info = {};
106
+ const homeEl = teamsEl.querySelector('.team.home') || teamsEl.querySelector('[class*="home"]');
107
+ if (homeEl) {
108
+ const nameEl = homeEl.querySelector('p:not(.logo)') || homeEl.querySelector('p');
109
+ if (nameEl) info.home_team = cl(nameEl.textContent);
110
+ const logoEl = homeEl.querySelector('img');
111
+ if (logoEl) info.home_logo = logoEl.getAttribute('src') || '';
112
+ }
113
+ const awayEl = teamsEl.querySelector('.team.away') || teamsEl.querySelector('[class*="away"]');
114
+ if (awayEl) {
115
+ const nameEl = awayEl.querySelector('p:not(.logo)') || awayEl.querySelector('p');
116
+ if (nameEl) info.away_team = cl(nameEl.textContent);
117
+ const logoEl = awayEl.querySelector('img');
118
+ if (logoEl) info.away_logo = logoEl.getAttribute('src') || '';
119
+ }
120
+ const scoreEl = teamsEl.querySelector('.score');
121
+ if (scoreEl) {
122
+ const parts = Array.from(scoreEl.querySelectorAll('p')).map(p => cl(p.textContent));
123
+ if (parts.length >= 2) info.score = parts[0] + ' - ' + parts[1];
124
+ const label = scoreEl.querySelector('.label');
125
+ if (label) info.status_label = cl(label.textContent);
126
+ }
127
+ if (info.home_team && info.away_team) {
128
+ result.info = info;
129
+ result.found = true;
130
+ result.sections.push('info');
131
+ }
132
  }
133
+
134
+ // Parse match info
135
+ const mi = doc.querySelector('.match-info');
136
+ if (mi) {
137
+ const te = mi.querySelector('.times') || mi.querySelector('li');
138
+ if (te) {
139
+ result.info = result.info || {};
140
+ result.info.datetime = cl(te.textContent);
141
+ }
142
  }
143
+
144
+ // Parse timeline
145
+ const timeline = [];
146
+ const tlEl = doc.querySelector('.timeline');
147
+ if (tlEl) {
148
+ let half = '';
149
+ for (const c of tlEl.children) {
150
+ if (!c.tagName) continue;
151
+ const t = cl(c.textContent);
152
+ if (!t) continue;
153
+ if (['H1','H2','Hi���p 1','Hiệp 2'].includes(t)) { half = t; continue; }
154
+ const m = t.match(/^(\d+'\+?\d*)/);
155
+ if (m) {
156
+ timeline.push({time: m[1], text: t.substring(m[0].length).trim(), half: half});
157
+ } else if (t.length > 5) {
158
+ timeline.push({time: '', text: t, half: half});
159
+ }
160
+ }
161
  }
162
+ if (timeline.length > 0) {
163
+ result.timeline = timeline;
164
+ result.sections.push('commentaries');
 
165
  }
166
+
167
+ // Parse events
168
+ const events = [];
169
+ doc.querySelectorAll('.event').forEach(function(el) {
170
+ const e = {};
171
+ const cls = el.className || '';
172
+ e.team = cls.includes('home') ? 'home' : (cls.includes('away') ? 'away' : '');
173
+ const players = Array.from(el.querySelectorAll('p')).map(p => cl(p.textContent)).filter(Boolean);
174
+ if (players.length > 0) e.players = players;
175
+ events.push(e);
176
+ });
177
+ if (events.length > 0) {
178
+ result.events = events;
179
+ result.sections.push('events');
180
  }
181
+
182
+ // Parse stats
183
+ let statsHtml = '';
184
+ for (const sel of ['.match-stats', '[class*="stats"]']) {
185
+ const el = doc.querySelector(sel);
186
+ if (el && el.innerHTML.length > 50) { statsHtml = el.innerHTML; break; }
187
+ }
188
+ if (statsHtml) {
189
+ result.stats_html = statsHtml;
190
+ result.sections.push('stats');
191
+ }
192
+
193
+ // Parse H2H
194
+ const h2h = [];
195
+ for (const sel of ['.head-to-head', '[class*="h2h"]']) {
196
+ const el = doc.querySelector(sel);
197
+ if (el && el.innerHTML.length > 50) {
198
+ el.querySelectorAll('li, tr').forEach(function(it) {
199
+ const cells = it.querySelectorAll('td, span, p');
200
+ if (cells.length >= 3) {
201
+ const m = {
202
+ date: cl(cells[0]?.textContent || ''),
203
+ home: cl(cells[1]?.textContent || ''),
204
+ score: cl(cells[2]?.textContent || ''),
205
+ };
206
+ if (cells.length > 3) m.away = cl(cells[3]?.textContent || '');
207
+ if (m.home) h2h.push(m);
208
+ }
209
+ });
210
+ if (h2h.length > 0) break;
211
  }
212
  }
213
+ if (h2h.length > 0) {
214
+ result.h2h = h2h;
215
+ result.sections.push('h2h');
216
+ }
217
+
218
+ return result;
219
+ } catch(e) {
220
+ return {event_id: eventId, found: false, sections: [], error: e.message};
221
  }
222
+ }
223
+
224
+ // Fetch HTML via multiple proxy strategies
225
+ async function fetchBongdaHTML(eventId, matchUrl) {
226
+ const bongdaUrls = [];
227
+ if (matchUrl) {
228
+ const cleanUrl = matchUrl.replace(/\/(preview|centre)\/.*/, '');
229
+ bongdaUrls.push(cleanUrl + '/centre/', cleanUrl + '/preview/');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  }
231
+ bongdaUrls.push(
232
+ 'https://bongda.com.vn/tran-dau/' + eventId + '/centre/',
233
+ 'https://bongda.com.vn/tran-dau/' + eventId + '/preview/'
234
+ );
235
+
236
+ // Strategy 1: Direct fetch (works if CORS allows)
237
+ for (const url of bongdaUrls) {
238
+ try {
239
+ const r = await fetch(url, {
240
+ signal: AbortSignal.timeout(10000),
241
+ headers: {'Accept': 'text/html'}
242
+ });
243
+ if (r.ok) {
244
+ const html = await r.text();
245
+ if (html.length > 1000 && html.includes('.teams')) {
246
+ return {html, url, method: 'direct'};
247
+ }
248
+ }
249
+ } catch(e) {}
250
  }
251
+
252
+ // Strategy 2: CORS proxies
253
+ const proxies = [
254
+ 'https://api.allorigins.win/raw?url=',
255
+ 'https://corsproxy.io/?',
256
+ 'https://api.codetabs.com/v1/proxy?quest=',
257
+ ];
258
 
259
+ for (const url of bongdaUrls) {
260
+ for (const proxy of proxies) {
261
+ try {
262
+ const r = await fetch(proxy + encodeURIComponent(url), {
263
+ signal: AbortSignal.timeout(15000)
264
+ });
265
+ if (r.ok) {
266
+ const html = await r.text();
267
+ if (html.length > 1000 && html.includes('.teams')) {
268
+ return {html, url, method: 'proxy:' + proxy};
269
+ }
 
 
 
 
270
  }
271
+ } catch(e) {}
 
272
  }
273
  }
274
+
275
+ return null;
 
 
 
 
276
  }
277
 
278
  window.loadMatchTab = async function(tab){
 
297
 
298
  let data = null;
299
 
300
+ // Strategy 1: Server API /live
301
  try{
302
  let apiUrl = '/api/match/' + eventId + '/live';
303
  if(matchUrl) apiUrl += '?page_url=' + encodeURIComponent(matchUrl);
 
308
  }
309
  } catch(e) {}
310
 
311
+ // Strategy 2: Server API /detail
312
  if(!data){
313
  try{
314
  let apiUrl = '/api/match/' + eventId + '/detail';
 
321
  } catch(e) {}
322
  }
323
 
324
+ // Strategy 3: Client-side scraping
325
  if(!data || !data.found){
326
+ const fetched = await fetchBongdaHTML(eventId, matchUrl);
327
+ if(fetched){
328
+ data = parseBongdaHtml(fetched.html, eventId);
329
+ if(data.found){
330
+ el.innerHTML = '<div style="font-size:9px;color:#5cb87a;text-align:center;padding:3px;background:#1a2a1f;border-radius:4px;margin-bottom:8px">📡 Dữ liệu tải trực tiếp từ bongda.com.vn' + (fetched.method !== 'direct' ? ' (qua proxy)' : '') + '</div>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  }
 
332
  }
333
  }
334
 
335
+ // Render or show fallback
336
  if(data && data.found && data.info && data.info.home_team){
337
  renderMatchDetail(el, data);
338
  } else {
339
+ // Fallback: show link to bongda.com.vn
340
+ const bongdaLink = matchUrl || 'https://bongda.com.vn/tran-dau/' + eventId + '/centre/';
341
+ el.innerHTML = '<div class="md-warn">⚠️ Không thể tải dữ liệu chi tiết tự động.<br><small>Event ID: ' + esc(String(eventId)) + '</small><br><a class="md-open-ext" href="' + esc(bongdaLink) + '" target="_blank" rel="noopener">🔗 Xem trận đấu trên bongda.com.vn</a></div>';
342
  }
343
  };
344
 
 
374
  if(info.away_logo) h += '<img src="'+esc(info.away_logo)+'" alt="'+esc(info.away_team)+'" onerror="this.style.display=\'none\'">';
375
  h += '<span>'+esc(info.away_team)+'</span></div>';
376
  h += '</div>';
 
377
  const infoItems = [];
378
  if(info.datetime) infoItems.push('📅 <b>'+esc(info.datetime)+'</b>');
379
  if(info.date && !info.datetime) infoItems.push('📅 <b>'+esc(info.date)+'</b>');
 
421
  // 3. H2H matches
422
  if(data.h2h && data.h2h.length > 0){
423
  hasAnyContent = true;
424
+ h += '<div class="md-sec"><div class="md-sec-title">🔄 Lịch sử đối đầu</div>';
425
  data.h2h.slice(0, 10).forEach(function(m){
426
  h += '<div class="md-h2h-item"><span class="md-h2h-date">'+esc(m.date||'')+'</span>';
427
  if(m.league) h += '<span class="md-h2h-league">'+esc(m.league)+'</span>';
 
447
  h += '</div>';
448
  }
449
 
450
+ // 5. Events
451
  if(data.events && data.events.length > 0){
452
  hasAnyContent = true;
453
  h += '<div class="md-sec"><div class="md-sec-title">🔄 Thay đổi cầu thủ</div><div class="md-events">';