bep40 commited on
Commit
89e641c
·
verified ·
1 Parent(s): ecfd22b

Upload static/yt_live.js

Browse files
Files changed (1) hide show
  1. static/yt_live.js +85 -141
static/yt_live.js CHANGED
@@ -1,20 +1,17 @@
1
- // === VNEWS — VTV1-VTV10 LIVE CHANNELS pinned to TOP of home page ===
2
- // Fetches VTV streams from iptv-org/vn.m3u playlist dynamically
3
  // Uses HLS.js (already loaded in index_v2.html) for playback
4
 
5
  (function(){
6
- // iptv-org Vietnam playlist URL
7
- const VN_M3U_URL = 'https://raw.githubusercontent.com/iptv-org/iptv/master/streams/vn.m3u';
8
- // Fallback: Love4vn VTV playlist
9
- const LOVE4VN_VTV_URL = 'https://raw.githubusercontent.com/Love4vn/Test/main/VTV.m3u';
10
- // Fallback: Ykcitus Vietnam playlist
11
- const YKCITUS_VN_URL = 'https://raw.githubusercontent.com/Ykcitus/IPTV/master/vietnam.m3u';
12
 
13
- const CHANNEL_ORDER = ['VTV1', 'VTV2', 'VTV3', 'VTV4', 'VTV5', 'VTV6', 'VTV7', 'VTV8', 'VTV9', 'VTV10'];
14
  let _vtvChannels = [];
15
- let _currentChannelIndex = 0;
16
  let _hlsInstance = null;
17
- let _playlistLoaded = false;
18
 
19
  // Inject styles once.
20
  if(!document.getElementById('vtv-channels-css')){
@@ -23,162 +20,101 @@
23
  .vtv-channels-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden;box-shadow:0 0 0 1px rgba(0,102,204,.3)}
24
  .vtv-channels-head{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,#003366,#1a1a1a);flex-wrap:wrap}
25
  .vtv-channels-title{font-size:13px;font-weight:800;color:#00ccff;letter-spacing:.5px}
26
- .vtv-channel-tabs{display:flex;gap:4px;flex:1;overflow-x:auto;scrollbar-width:none;margin-top:4px}
27
  .vtv-channel-tabs::-webkit-scrollbar{display:none}
28
  .vtv-channel-tab{padding:4px 10px;background:#1a2a3a;border:1px solid #2a3a4a;border-radius:12px;color:#8ab4d8;font-size:10px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:all .2s}
29
  .vtv-channel-tab:hover{background:#0b4a7a;color:#fff}
30
  .vtv-channel-tab.active{background:#0066cc;border-color:#0066cc;color:#fff;font-weight:700}
31
  .vtv-channel-tab:disabled{opacity:.35;cursor:not-allowed}
32
- .vtv-live-badge{font-size:10px;font-weight:800;color:#00ccff;animation:vtv-live-pulse 1.3s infinite}
33
  @keyframes vtv-live-pulse{0%,100%{opacity:1}50%{opacity:.3}}
34
  .vtv-channels-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;min-height:200px}
35
  .vtv-channels-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
36
- .vtv-channels-error{display:flex;align-items:center;justify-content:center;height:100%;color:#888;font-size:12px;text-align:center;padding:20px;min-height:200px}
37
- .vtv-loading{display:flex;align-items:center;justify-content:center;height:100%;color:#00ccff;font-size:12px;min-height:200px}
 
 
 
38
  .vtv-channel-info{font-size:11px;color:#aaa;min-width:80px;text-align:right}
39
  `;
40
  document.head.appendChild(s);
41
  }
42
 
43
- // Parse M3U playlist for VTV channels
44
- function parseM3U(m3uText){
45
- const channels = {};
46
- const lines = m3uText.split('\n');
47
- let currentChannel = null;
48
-
49
- for(let i = 0; i < lines.length; i++){
50
- const line = lines[i].trim();
51
- if(line.startsWith('#EXTINF:')){
52
- const nameMatch = line.match(/,[^,]*$/);
53
- const tvgIdMatch = line.match(/tvg-id="([^"]+)"/);
54
- const tvgNameMatch = line.match(/tvg-name="([^"]+)"/);
55
- const logoMatch = line.match(/tvg-logo="([^"]+)"/);
56
-
57
- let name = '';
58
- if(nameMatch) name = nameMatch[0].substring(1).trim();
59
- if(tvgNameMatch) name = tvgNameMatch[1];
60
- if(tvgIdMatch) name = tvgIdMatch[1].replace('.vn', '');
61
-
62
- currentChannel = {
63
- name: name,
64
- logo: logoMatch ? logoMatch[1] : '',
65
- url: ''
66
- };
67
- } else if(line && !line.startsWith('#') && currentChannel){
68
- currentChannel.url = line.trim();
69
- const normName = currentChannel.name.toUpperCase().replace(/\s+/g, '');
70
- if(CHANNEL_ORDER.some(c => normName.includes(c))){
71
- for(const ch of CHANNEL_ORDER){
72
- if(normName.includes(ch)){
73
- if(!channels[ch] || !channels[ch].url){
74
- channels[ch] = currentChannel;
75
- }
76
- break;
77
- }
78
- }
79
- }
80
- currentChannel = null;
81
- }
82
- }
83
- return channels;
84
- }
85
-
86
- // Fetch and merge playlists
87
- async function loadPlaylists(){
88
- const allChannels = {};
89
- const urls = [VN_M3U_URL, LOVE4VN_VTV_URL, YKCITUS_VN_URL];
90
-
91
- for(const url of urls){
92
- try{
93
- const response = await fetch(url);
94
- if(response.ok){
95
- const text = await response.text();
96
- const parsed = parseM3U(text);
97
- for(const [key, ch] of Object.entries(parsed)){
98
- if(ch.url && (!allChannels[key] || !allChannels[key].url)){
99
- allChannels[key] = ch;
100
- }
101
- }
102
- }
103
- }catch(e){
104
- console.warn('Failed to load playlist:', url, e);
105
- }
106
- }
107
-
108
- // Build ordered array
109
- _vtvChannels = CHANNEL_ORDER.map(name => {
110
- const ch = allChannels[name];
111
- return ch ? {...ch, displayName: name} : null;
112
- }).filter(ch => ch && ch.url);
113
-
114
- _playlistLoaded = true;
115
- return _vtvChannels.length > 0;
116
- }
117
 
118
  function buildChannelsBlock(){
119
  const wrap=document.createElement('div');
120
  wrap.className='vtv-channels-wrap';
121
  wrap.id='vtv-channels-block';
122
-
123
- const tabsHtml = CHANNEL_ORDER.map((name, i) =>
124
- `<button class="vtv-channel-tab${i===0?' active':''}" data-idx="${i}" onclick="window._switchVTVChannel(${i})">${name}</button>`
125
- ).join('');
126
-
 
127
  wrap.innerHTML=
128
  '<div class="vtv-channels-head">'
129
  +'<span class="vtv-channels-title">📺 Kênh VTV Trực Tuyến</span>'
130
  +'<span class="vtv-live-badge">● LIVE</span>'
131
- +'<span class="vtv-channel-info" id="vtv-channel-info"></span>'
132
- +'</div>'
133
- +'<div class="vtv-channel-tabs" id="vtv-channel-tabs">'
134
- + tabsHtml
135
  +'</div>'
 
136
  +'<div class="vtv-channels-frame">'
137
- +'<div class="vtv-loading" id="vtv-loading">Đang tải danh sách kênh...</div>'
138
  +'<video id="vtv-player" playsinline muted controls preload="auto" style="display:none"></video>'
139
- +'<div class="vtv-channels-error" id="vtv-error" style="display:none">Không thể phát kênh này</div>'
140
  +'</div>';
141
  return wrap;
142
  }
143
 
144
  function pinChannelsBlock(){
145
  const home=document.getElementById('view-home');
146
- if(!home)return;
147
- if(document.getElementById('vtv-channels-block'))return;
148
  home.insertBefore(buildChannelsBlock(), home.firstChild);
149
  initChannels();
150
  }
151
 
152
  async function initChannels(){
153
  const loadingEl = document.getElementById('vtv-loading');
 
 
154
  const tabsEl = document.getElementById('vtv-channel-tabs');
155
-
156
- if(!_playlistLoaded){
157
- const ok = await loadPlaylists();
158
- if(!ok){
159
- loadingEl.textContent = 'Không tải được danh sách kênh. Thử lại sau...';
160
- setTimeout(initChannels, 30000);
161
- return;
162
- }
163
- // Update tabs - disable channels without streams
164
- if(tabsEl){
165
- const foundNames = _vtvChannels.map(c => c.displayName);
166
- tabsEl.querySelectorAll('.vtv-channel-tab').forEach((tab) => {
167
- const idx = parseInt(tab.dataset.idx);
168
- const chName = CHANNEL_ORDER[idx];
169
- if(!foundNames.includes(chName)){
170
- tab.disabled = true;
171
- tab.title = chName + ': Không có luồng';
172
- }
173
- });
174
- }
175
  }
176
-
177
  loadingEl.style.display = 'none';
178
- playChannel(0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  }
180
 
181
- function switchVTVChannel(index){
182
  if(index >= _vtvChannels.length) return;
183
  const tabs = document.querySelectorAll('.vtv-channel-tab');
184
  tabs.forEach((tab, i) => {
@@ -188,33 +124,38 @@
188
  }
189
 
190
  // Expose globally
191
- window._switchVTVChannel = switchVTVChannel;
192
 
193
  function playChannel(index){
194
  if(index >= _vtvChannels.length) return;
195
-
196
  const ch = _vtvChannels[index];
197
  const video = document.getElementById('vtv-player');
198
  const errorEl = document.getElementById('vtv-error');
199
  const loadingEl = document.getElementById('vtv-loading');
200
- const infoEl = document.getElementById('vtv-channel-info');
201
-
202
- if(!video || !ch.url) return;
203
-
204
- _currentChannelIndex = index;
205
- if(infoEl) infoEl.textContent = ch.displayName;
206
-
 
 
 
 
 
207
  // Clean up previous HLS instance
208
  if(_hlsInstance){
209
  _hlsInstance.destroy();
210
  _hlsInstance = null;
211
  }
212
-
213
  video.style.display = 'none';
214
  errorEl.style.display = 'none';
215
  loadingEl.style.display = 'flex';
216
- loadingEl.textContent = 'Đang kết nối ' + ch.displayName + '...';
217
-
218
  if(typeof Hls !== 'undefined' && Hls.isSupported()){
219
  const hls = new Hls({
220
  enableWorker: true,
@@ -246,7 +187,8 @@
246
  _hlsInstance = null;
247
  loadingEl.style.display = 'none';
248
  errorEl.style.display = 'flex';
249
- errorEl.textContent = ch.displayName + ': Lỗi phát video';
 
250
  break;
251
  }
252
  }
@@ -261,12 +203,14 @@
261
  video.addEventListener('error', () => {
262
  loadingEl.style.display = 'none';
263
  errorEl.style.display = 'flex';
264
- errorEl.textContent = ch.displayName + ': Không phát được';
 
265
  }, {once: true});
266
  } else {
267
  loadingEl.style.display = 'none';
268
  errorEl.style.display = 'flex';
269
- errorEl.textContent = 'Trình duyệt không hỗ trợ HLS';
 
270
  }
271
  }
272
 
@@ -291,4 +235,4 @@
291
  }
292
  if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',()=>setTimeout(pinChannelsBlock,300));
293
  else setTimeout(pinChannelsBlock,300);
294
- })();
 
1
+ // === VNEWS — VTV1-VTV10+ LIVE CHANNELS pinned to TOP of home page ===
2
+ // Fetches stream URLs from backend API (vtv_scraper.py which scrapes xemtv.net)
3
  // Uses HLS.js (already loaded in index_v2.html) for playback
4
 
5
  (function(){
6
+ const CHANNEL_ORDER = ['VTV1','VTV2','VTV3','VTV4','VTV5','VTV6','VTV7','VTV8','VTV9','VTV10'];
7
+ const DISPLAY_NAMES = {
8
+ 'VTV1':'VTV1', 'VTV2':'VTV2', 'VTV3':'VTV3', 'VTV4':'VTV4', 'VTV5':'VTV5',
9
+ 'VTV6':'VTV Cần Thơ', 'VTV7':'VTV7', 'VTV8':'VTV8', 'VTV9':'VTV9', 'VTV10':'VTV10'
10
+ };
 
11
 
 
12
  let _vtvChannels = [];
 
13
  let _hlsInstance = null;
14
+ let _currentChannelIdx = 0;
15
 
16
  // Inject styles once.
17
  if(!document.getElementById('vtv-channels-css')){
 
20
  .vtv-channels-wrap{margin:6px 4px;background:#111;border:1px solid #0066cc;border-radius:10px;overflow:hidden;box-shadow:0 0 0 1px rgba(0,102,204,.3)}
21
  .vtv-channels-head{display:flex;align-items:center;gap:8px;padding:8px 10px;background:linear-gradient(90deg,#003366,#1a1a1a);flex-wrap:wrap}
22
  .vtv-channels-title{font-size:13px;font-weight:800;color:#00ccff;letter-spacing:.5px}
23
+ .vtv-channel-tabs{display:flex;gap:4px;flex:1;overflow-x:auto;scrollbar-width:none;margin-top:4px;padding-bottom:4px}
24
  .vtv-channel-tabs::-webkit-scrollbar{display:none}
25
  .vtv-channel-tab{padding:4px 10px;background:#1a2a3a;border:1px solid #2a3a4a;border-radius:12px;color:#8ab4d8;font-size:10px;cursor:pointer;white-space:nowrap;flex-shrink:0;transition:all .2s}
26
  .vtv-channel-tab:hover{background:#0b4a7a;color:#fff}
27
  .vtv-channel-tab.active{background:#0066cc;border-color:#0066cc;color:#fff;font-weight:700}
28
  .vtv-channel-tab:disabled{opacity:.35;cursor:not-allowed}
29
+ .vtv-live-badge{font-size:10px;font-weight:800;color:#00ccff;animation:vtv-live-pulse 1.3s infinite;margin-left:4px}
30
  @keyframes vtv-live-pulse{0%,100%{opacity:1}50%{opacity:.3}}
31
  .vtv-channels-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;min-height:200px}
32
  .vtv-channels-frame video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}
33
+ .vtv-channels-error{display:flex;align-items:center;justify-content:center;height:100%;color:#888;font-size:12px;text-align:center;padding:20px;min-height:200px;flex-direction:column;gap:8px}
34
+ .vtv-channels-error a{color:#00ccff;text-decoration:none;font-size:11px}
35
+ .vtv-loading{display:flex;align-items:center;justify-content:center;height:100%;color:#00ccff;font-size:12px;min-height:200px;flex-direction:column;gap:8px}
36
+ .vtv-spinner{width:24px;height:24px;border:3px solid #1a3a5a;border-top-color:#00ccff;border-radius:50%;animation:vtv-spin .8s linear infinite}
37
+ @keyframes vtv-spin{to{transform:rotate(360deg)}}
38
  .vtv-channel-info{font-size:11px;color:#aaa;min-width:80px;text-align:right}
39
  `;
40
  document.head.appendChild(s);
41
  }
42
 
43
+ function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
  function buildChannelsBlock(){
46
  const wrap=document.createElement('div');
47
  wrap.className='vtv-channels-wrap';
48
  wrap.id='vtv-channels-block';
49
+
50
+ const tabsHtml = CHANNEL_ORDER.map((name, i) => {
51
+ const dn = DISPLAY_NAMES[name] || name;
52
+ return `<button class="vtv-channel-tab${i===0?' active':''}" data-name="${name}" data-idx="${i}" onclick="window._switchVTV(${i})">${dn}</button>`;
53
+ }).join('');
54
+
55
  wrap.innerHTML=
56
  '<div class="vtv-channels-head">'
57
  +'<span class="vtv-channels-title">📺 Kênh VTV Trực Tuyến</span>'
58
  +'<span class="vtv-live-badge">● LIVE</span>'
 
 
 
 
59
  +'</div>'
60
+ +'<div class="vtv-channel-tabs" id="vtv-channel-tabs">' + tabsHtml + '</div>'
61
  +'<div class="vtv-channels-frame">'
62
+ +'<div class="vtv-loading" id="vtv-loading"><div class="vtv-spinner"></div>Đang tải danh sách kênh...</div>'
63
  +'<video id="vtv-player" playsinline muted controls preload="auto" style="display:none"></video>'
64
+ +'<div class="vtv-channels-error" id="vtv-error" style="display:none">Không thể phát kênh</div>'
65
  +'</div>';
66
  return wrap;
67
  }
68
 
69
  function pinChannelsBlock(){
70
  const home=document.getElementById('view-home');
71
+ if(!home) return;
72
+ if(document.getElementById('vtv-channels-block')) return;
73
  home.insertBefore(buildChannelsBlock(), home.firstChild);
74
  initChannels();
75
  }
76
 
77
  async function initChannels(){
78
  const loadingEl = document.getElementById('vtv-loading');
79
+ const video = document.getElementById('vtv-player');
80
+ const errorEl = document.getElementById('vtv-error');
81
  const tabsEl = document.getElementById('vtv-channel-tabs');
82
+
83
+ // Fetch channel streams from backend
84
+ try{
85
+ const r = await fetch('/api/vtv/channels');
86
+ const data = await r.json();
87
+ _vtvChannels = data.channels || [];
88
+ }catch(e){
89
+ console.warn('Failed to fetch VTV channels:', e);
90
+ _vtvChannels = [];
 
 
 
 
 
 
 
 
 
 
 
91
  }
92
+
93
  loadingEl.style.display = 'none';
94
+
95
+ // Disable tabs for channels without streams
96
+ if(tabsEl){
97
+ const foundNames = _vtvChannels.map(c => c.name);
98
+ tabsEl.querySelectorAll('.vtv-channel-tab').forEach(tab => {
99
+ const name = tab.dataset.name;
100
+ if(!foundNames.includes(name)){
101
+ tab.disabled = true;
102
+ tab.title = DISPLAY_NAMES[name] + ': Chưa có luồng';
103
+ }
104
+ });
105
+ }
106
+
107
+ // Play first available channel
108
+ if(_vtvChannels.length > 0){
109
+ playChannel(0);
110
+ } else {
111
+ errorEl.style.display = 'flex';
112
+ errorEl.innerHTML = 'Chưa có kênh nào khả dụng.<br><a href="https://hd.xemtv.net/kenh-vtv3-truc-tuyen-hot.html" target="_blank">Xem tại xemtv.net →</a>';
113
+ video.style.display = 'none';
114
+ }
115
  }
116
 
117
+ function switchVTV(index){
118
  if(index >= _vtvChannels.length) return;
119
  const tabs = document.querySelectorAll('.vtv-channel-tab');
120
  tabs.forEach((tab, i) => {
 
124
  }
125
 
126
  // Expose globally
127
+ window._switchVTV = switchVTV;
128
 
129
  function playChannel(index){
130
  if(index >= _vtvChannels.length) return;
131
+
132
  const ch = _vtvChannels[index];
133
  const video = document.getElementById('vtv-player');
134
  const errorEl = document.getElementById('vtv-error');
135
  const loadingEl = document.getElementById('vtv-loading');
136
+
137
+ if(!video || !ch.url) {
138
+ if(errorEl){
139
+ errorEl.style.display = 'flex';
140
+ errorEl.innerHTML = `${esc(ch.displayName || ch.name)}: Chưa có luồng.<br><a href="${esc(ch.fallback_url || 'https://hd.xemtv.net/')}" target="_blank">Xem tại xemtv.net →</a>`;
141
+ }
142
+ video.style.display = 'none';
143
+ return;
144
+ }
145
+
146
+ _currentChannelIdx = index;
147
+
148
  // Clean up previous HLS instance
149
  if(_hlsInstance){
150
  _hlsInstance.destroy();
151
  _hlsInstance = null;
152
  }
153
+
154
  video.style.display = 'none';
155
  errorEl.style.display = 'none';
156
  loadingEl.style.display = 'flex';
157
+ loadingEl.innerHTML = `<div class="vtv-spinner"></div>Đang kết nối ${esc(ch.displayName || ch.name)}...`;
158
+
159
  if(typeof Hls !== 'undefined' && Hls.isSupported()){
160
  const hls = new Hls({
161
  enableWorker: true,
 
187
  _hlsInstance = null;
188
  loadingEl.style.display = 'none';
189
  errorEl.style.display = 'flex';
190
+ errorEl.innerHTML = `${esc(ch.displayName || ch.name)}: Lỗi phát video.<br><a href="${esc(ch.fallback_url || 'https://hd.xemtv.net/')}" target="_blank">Xem tại xemtv.net →</a>`;
191
+ video.style.display = 'none';
192
  break;
193
  }
194
  }
 
203
  video.addEventListener('error', () => {
204
  loadingEl.style.display = 'none';
205
  errorEl.style.display = 'flex';
206
+ errorEl.innerHTML = `${esc(ch.displayName || ch.name)}: Không phát được.<br><a href="${esc(ch.fallback_url || 'https://hd.xemtv.net/')}" target="_blank">Xem tại xemtv.net →</a>`;
207
+ video.style.display = 'none';
208
  }, {once: true});
209
  } else {
210
  loadingEl.style.display = 'none';
211
  errorEl.style.display = 'flex';
212
+ errorEl.innerHTML = 'Trình duyệt không hỗ trợ HLS.<br><a href="https://hd.xemtv.net/" target="_blank">Xem tại xemtv.net →</a>';
213
+ video.style.display = 'none';
214
  }
215
  }
216
 
 
235
  }
236
  if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',()=>setTimeout(pinChannelsBlock,300));
237
  else setTimeout(pinChannelsBlock,300);
238
+ })();