Spaces:
Running
Running
Upload static/yt_live.js
Browse files- static/yt_live.js +37 -16
static/yt_live.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
// === VNEWS — VTV1-VTV10 + VTVPrime LIVE CHANNELS ===
|
| 2 |
// Uses backend /api/vtv/streams for stream URLs
|
| 3 |
// VTV1-VTV5,VTV7-VTV9: VTVGo failover | VTV6,VTV10: xemtv.net scraping
|
|
|
|
| 4 |
|
| 5 |
(function(){
|
| 6 |
if(window._ytLiveLoaded) return;
|
|
@@ -20,6 +21,9 @@
|
|
| 20 |
{id:'vtvprime', name:'VTVPrime', badge:'Prime'},
|
| 21 |
];
|
| 22 |
|
|
|
|
|
|
|
|
|
|
| 23 |
const NEEDS_PROXY = /fptplay\.net/;
|
| 24 |
const STREAMS = {};
|
| 25 |
let _currentCh = null;
|
|
@@ -54,12 +58,16 @@
|
|
| 54 |
.vtv-epg-toggle{background:none;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:2px 8px;border-radius:6px;cursor:pointer}
|
| 55 |
.vtv-epg-list{display:flex;gap:4px;overflow-x:auto;scrollbar-width:none;padding-bottom:4px}
|
| 56 |
.vtv-epg-list::-webkit-scrollbar{display:none}
|
| 57 |
-
.vtv-epg-item{flex:0 0 auto;padding:3px 6px;background:#1a2a3a;border-radius:4px;font-size:8px;color:#8ab4d8;white-space:nowrap}
|
|
|
|
| 58 |
.vtv-epg-item.now{background:#0066cc;color:#fff;font-weight:700}
|
| 59 |
.vtv-epg-item .epg-t{font-size:7px;color:#6a8aaa}
|
| 60 |
.vtv-epg-item.now .epg-t{color:#aaccee}
|
| 61 |
.vtv-epg-item .epg-n{color:#ccc;font-size:8px}
|
| 62 |
.vtv-epg-item.now .epg-n{color:#fff}
|
|
|
|
|
|
|
|
|
|
| 63 |
`;
|
| 64 |
document.head.appendChild(s);
|
| 65 |
|
|
@@ -122,7 +130,7 @@
|
|
| 122 |
'</div>' +
|
| 123 |
'<div class="vtv-epg" id="vtv-epg">' +
|
| 124 |
'<div class="vtv-epg-header"><span class="vtv-epg-title">📋 Lịch phát sóng</span><button class="vtv-epg-toggle" id="vtv-epg-toggle" onclick="window._vtvToggleEpg()">Ẩn</button></div>' +
|
| 125 |
-
'<div class="vtv-epg-list" id="vtv-epg-list"><
|
| 126 |
'</div>';
|
| 127 |
return w;
|
| 128 |
}
|
|
@@ -130,28 +138,46 @@
|
|
| 130 |
async function loadEpg(chId){
|
| 131 |
const epgList = document.getElementById('vtv-epg-list');
|
| 132 |
if(!epgList) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
try {
|
| 134 |
-
const r = await fetch('/api/vtv/epg/' + chId, {signal: AbortSignal.timeout(
|
| 135 |
-
if(!r.ok) throw new Error('EPG fetch failed');
|
| 136 |
const data = await r.json();
|
| 137 |
const programs = data.programs || [];
|
|
|
|
| 138 |
if(programs.length === 0){
|
| 139 |
-
epgList.innerHTML = '<
|
| 140 |
return;
|
| 141 |
}
|
|
|
|
| 142 |
let h = '';
|
| 143 |
programs.forEach(p => {
|
| 144 |
const isNow = p.now ? ' now' : '';
|
| 145 |
-
|
|
|
|
| 146 |
h += '<span class="epg-t">' + (p.time || '') + '</span> ';
|
| 147 |
h += '<span class="epg-n">' + (p.title || '') + '</span>';
|
| 148 |
h += '</div>';
|
| 149 |
});
|
| 150 |
epgList.innerHTML = h;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
} catch(e) {
|
| 152 |
-
|
|
|
|
| 153 |
}
|
| 154 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
window._vtvToggleEpg = function(){
|
| 157 |
const list = document.getElementById('vtv-epg-list');
|
|
@@ -178,12 +204,8 @@
|
|
| 178 |
_blockInserted = true;
|
| 179 |
if(!_streamsLoaded){
|
| 180 |
loadAllStreams().then(() => {
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
setTimeout(() => window._vtvPlay(ch.id), 300);
|
| 184 |
-
break;
|
| 185 |
-
}
|
| 186 |
-
}
|
| 187 |
});
|
| 188 |
}
|
| 189 |
}
|
|
@@ -293,8 +315,7 @@
|
|
| 293 |
}
|
| 294 |
}
|
| 295 |
|
| 296 |
-
//
|
| 297 |
-
// This ensures pinBlock runs exactly once after loadHome completes.
|
| 298 |
const orig = window.loadHome;
|
| 299 |
if(orig && !orig.__vtvWrapped){
|
| 300 |
window.loadHome = async function(){
|
|
@@ -308,4 +329,4 @@
|
|
| 308 |
};
|
| 309 |
window.loadHome.__vtvWrapped = true;
|
| 310 |
}
|
| 311 |
-
})();
|
|
|
|
| 1 |
// === VNEWS — VTV1-VTV10 + VTVPrime LIVE CHANNELS ===
|
| 2 |
// Uses backend /api/vtv/streams for stream URLs
|
| 3 |
// VTV1-VTV5,VTV7-VTV9: VTVGo failover | VTV6,VTV10: xemtv.net scraping
|
| 4 |
+
// Default channel: VTV6
|
| 5 |
|
| 6 |
(function(){
|
| 7 |
if(window._ytLiveLoaded) return;
|
|
|
|
| 21 |
{id:'vtvprime', name:'VTVPrime', badge:'Prime'},
|
| 22 |
];
|
| 23 |
|
| 24 |
+
// Default channel is VTV6
|
| 25 |
+
const DEFAULT_CHANNEL = 'vtv6';
|
| 26 |
+
|
| 27 |
const NEEDS_PROXY = /fptplay\.net/;
|
| 28 |
const STREAMS = {};
|
| 29 |
let _currentCh = null;
|
|
|
|
| 58 |
.vtv-epg-toggle{background:none;border:1px solid #2a3a4a;color:#8ab4d8;font-size:9px;padding:2px 8px;border-radius:6px;cursor:pointer}
|
| 59 |
.vtv-epg-list{display:flex;gap:4px;overflow-x:auto;scrollbar-width:none;padding-bottom:4px}
|
| 60 |
.vtv-epg-list::-webkit-scrollbar{display:none}
|
| 61 |
+
.vtv-epg-item{flex:0 0 auto;padding:3px 6px;background:#1a2a3a;border-radius:4px;font-size:8px;color:#8ab4d8;white-space:nowrap;cursor:pointer;transition:all .2s}
|
| 62 |
+
.vtv-epg-item:hover{background:#2a4a6a}
|
| 63 |
.vtv-epg-item.now{background:#0066cc;color:#fff;font-weight:700}
|
| 64 |
.vtv-epg-item .epg-t{font-size:7px;color:#6a8aaa}
|
| 65 |
.vtv-epg-item.now .epg-t{color:#aaccee}
|
| 66 |
.vtv-epg-item .epg-n{color:#ccc;font-size:8px}
|
| 67 |
.vtv-epg-item.now .epg-n{color:#fff}
|
| 68 |
+
.vtv-epg-empty{color:#666;font-size:9px;padding:4px}
|
| 69 |
+
.vtv-epg-loading{color:#00ccff;font-size:9px;padding:4px;display:flex;align-items:center;gap:6px}
|
| 70 |
+
.vtv-epg-spinner{width:10px;height:10px;border:1px solid #333;border-top-color:#00ccff;border-radius:50%;animation:vtvspin .8s linear infinite}
|
| 71 |
`;
|
| 72 |
document.head.appendChild(s);
|
| 73 |
|
|
|
|
| 130 |
'</div>' +
|
| 131 |
'<div class="vtv-epg" id="vtv-epg">' +
|
| 132 |
'<div class="vtv-epg-header"><span class="vtv-epg-title">📋 Lịch phát sóng</span><button class="vtv-epg-toggle" id="vtv-epg-toggle" onclick="window._vtvToggleEpg()">Ẩn</button></div>' +
|
| 133 |
+
'<div class="vtv-epg-list" id="vtv-epg-list"><div class="vtv-epg-loading"><div class="vtv-epg-spinner"></div>Đang tải EPG...</div></div>' +
|
| 134 |
'</div>';
|
| 135 |
return w;
|
| 136 |
}
|
|
|
|
| 138 |
async function loadEpg(chId){
|
| 139 |
const epgList = document.getElementById('vtv-epg-list');
|
| 140 |
if(!epgList) return;
|
| 141 |
+
|
| 142 |
+
// Show loading state
|
| 143 |
+
epgList.innerHTML = '<div class="vtv-epg-loading"><div class="vtv-epg-spinner"></div>Đang tải lịch phát sóng...</div>';
|
| 144 |
+
|
| 145 |
try {
|
| 146 |
+
const r = await fetch('/api/vtv/epg/' + chId, {signal: AbortSignal.timeout(10000)});
|
| 147 |
+
if(!r.ok) throw new Error('EPG fetch failed: ' + r.status);
|
| 148 |
const data = await r.json();
|
| 149 |
const programs = data.programs || [];
|
| 150 |
+
|
| 151 |
if(programs.length === 0){
|
| 152 |
+
epgList.innerHTML = '<div class="vtv-epg-empty">Chưa có lịch phát sóng cho kênh này</div>';
|
| 153 |
return;
|
| 154 |
}
|
| 155 |
+
|
| 156 |
let h = '';
|
| 157 |
programs.forEach(p => {
|
| 158 |
const isNow = p.now ? ' now' : '';
|
| 159 |
+
const desc = p.description ? ' title="' + escAttr(p.description) + '"' : '';
|
| 160 |
+
h += '<div class="vtv-epg-item' + isNow + '"' + desc + '>';
|
| 161 |
h += '<span class="epg-t">' + (p.time || '') + '</span> ';
|
| 162 |
h += '<span class="epg-n">' + (p.title || '') + '</span>';
|
| 163 |
h += '</div>';
|
| 164 |
});
|
| 165 |
epgList.innerHTML = h;
|
| 166 |
+
|
| 167 |
+
// Scroll to current program
|
| 168 |
+
const nowItem = epgList.querySelector('.vtv-epg-item.now');
|
| 169 |
+
if(nowItem){
|
| 170 |
+
nowItem.scrollIntoView({behavior: 'smooth', inline: 'center', block: 'nearest'});
|
| 171 |
+
}
|
| 172 |
} catch(e) {
|
| 173 |
+
console.warn('EPG load error:', e);
|
| 174 |
+
if(epgList) epgList.innerHTML = '<div class="vtv-epg-empty">Không tải được lịch phát sóng</div>';
|
| 175 |
}
|
| 176 |
}
|
| 177 |
+
|
| 178 |
+
function escAttr(s){
|
| 179 |
+
return String(s || '').replace(/&/g,'&').replace(/"/g,'"').replace(/</g,'<').replace(/>/g,'>');
|
| 180 |
+
}
|
| 181 |
|
| 182 |
window._vtvToggleEpg = function(){
|
| 183 |
const list = document.getElementById('vtv-epg-list');
|
|
|
|
| 204 |
_blockInserted = true;
|
| 205 |
if(!_streamsLoaded){
|
| 206 |
loadAllStreams().then(() => {
|
| 207 |
+
// Auto-play VTV6 as default channel
|
| 208 |
+
setTimeout(() => window._vtvPlay(DEFAULT_CHANNEL), 300);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
});
|
| 210 |
}
|
| 211 |
}
|
|
|
|
| 315 |
}
|
| 316 |
}
|
| 317 |
|
| 318 |
+
// Wrap loadHome to inject VTV block
|
|
|
|
| 319 |
const orig = window.loadHome;
|
| 320 |
if(orig && !orig.__vtvWrapped){
|
| 321 |
window.loadHome = async function(){
|
|
|
|
| 329 |
};
|
| 330 |
window.loadHome.__vtvWrapped = true;
|
| 331 |
}
|
| 332 |
+
})();
|