Spaces:
Running
Running
Upload static/shorts_fresh.js with huggingface_hub
Browse files- static/shorts_fresh.js +105 -64
static/shorts_fresh.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
| 1 |
/**
|
| 2 |
-
* VNEWS Fresh Shorts Fetcher
|
| 3 |
-
* Fetches latest shorts from YouTube
|
| 4 |
-
* Parses new YouTube shortsLockupViewModel structure
|
| 5 |
*/
|
| 6 |
(function() {
|
| 7 |
'use strict';
|
|
@@ -19,6 +18,18 @@
|
|
| 19 |
setTimeout(() => { t.style.display = 'none'; }, 4000);
|
| 20 |
}
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
async function fetchWithRetry(url, maxRetries) {
|
| 23 |
for (let i = 0; i < maxRetries; i++) {
|
| 24 |
try {
|
|
@@ -44,27 +55,12 @@
|
|
| 44 |
let shortsTab = null;
|
| 45 |
for (const tab of tabs) {
|
| 46 |
const tr = tab.tabRenderer || {};
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
shortsTab = content.richGridRenderer;
|
| 52 |
-
break;
|
| 53 |
-
}
|
| 54 |
-
}
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
if (!shortsTab) {
|
| 58 |
-
// Fallback: use first richGrid found
|
| 59 |
-
for (const tab of tabs) {
|
| 60 |
-
const content = (tab.tabRenderer || {}).content || {};
|
| 61 |
-
if (content.richGridRenderer) {
|
| 62 |
-
shortsTab = content.richGridRenderer;
|
| 63 |
-
break;
|
| 64 |
-
}
|
| 65 |
}
|
| 66 |
}
|
| 67 |
-
|
| 68 |
if (!shortsTab) return [];
|
| 69 |
|
| 70 |
const items = shortsTab.contents || [];
|
|
@@ -78,25 +74,14 @@
|
|
| 78 |
// New YouTube format: shortsLockupViewModel
|
| 79 |
if (inner.shortsLockupViewModel) {
|
| 80 |
const slvm = inner.shortsLockupViewModel;
|
| 81 |
-
|
| 82 |
-
// Get videoId from onTap endpoint
|
| 83 |
let vid = '';
|
| 84 |
-
const
|
| 85 |
-
const cmd = onTap.innertubeCommand || {};
|
| 86 |
vid = (cmd.watchEndpoint || {}).videoId || (cmd.reelWatchEndpoint || {}).videoId || '';
|
| 87 |
-
|
| 88 |
if (!vid || seen.has(vid)) continue;
|
| 89 |
seen.add(vid);
|
| 90 |
|
| 91 |
-
|
| 92 |
-
let
|
| 93 |
-
const overlay = slvm.overlayMetadata || {};
|
| 94 |
-
title = (overlay.primaryText || {}).content || slvm.accessibilityText || 'YouTube Short';
|
| 95 |
-
|
| 96 |
-
// Get thumbnail
|
| 97 |
-
let img = '';
|
| 98 |
-
const thumbs = (slvm.thumbnail || {}).sources || [];
|
| 99 |
-
if (thumbs.length > 0) img = thumbs[0].url || '';
|
| 100 |
|
| 101 |
shorts.push({
|
| 102 |
id: vid,
|
|
@@ -107,24 +92,21 @@
|
|
| 107 |
source: 'yt'
|
| 108 |
});
|
| 109 |
}
|
| 110 |
-
|
| 111 |
// Old format: reelItemRenderer
|
| 112 |
else if (inner.reelItemRenderer) {
|
| 113 |
const rir = inner.reelItemRenderer;
|
| 114 |
const vid = rir.videoId || '';
|
| 115 |
if (!vid || seen.has(vid)) continue;
|
| 116 |
seen.add(vid);
|
| 117 |
-
const title = rir.headline || 'YouTube Short';
|
| 118 |
shorts.push({
|
| 119 |
id: vid,
|
| 120 |
-
title:
|
| 121 |
img: 'https://i.ytimg.com/vi/' + vid + '/hqdefault.jpg',
|
| 122 |
link: 'https://www.youtube.com/shorts/' + vid,
|
| 123 |
channel: handle,
|
| 124 |
source: 'yt'
|
| 125 |
});
|
| 126 |
}
|
| 127 |
-
|
| 128 |
// videoRenderer fallback
|
| 129 |
else if (inner.videoRenderer) {
|
| 130 |
const vr = inner.videoRenderer;
|
|
@@ -146,7 +128,7 @@
|
|
| 146 |
if (shorts.length >= 30) break;
|
| 147 |
}
|
| 148 |
|
| 149 |
-
// Last resort: regex
|
| 150 |
if (shorts.length === 0) {
|
| 151 |
const vids = [...new Set([...html.matchAll(/"videoId":"([A-Za-z0-9_-]{11})"/g)].map(m => m[1]))];
|
| 152 |
for (const vid of vids) {
|
|
@@ -168,37 +150,92 @@
|
|
| 168 |
return shorts;
|
| 169 |
}
|
| 170 |
|
| 171 |
-
//
|
| 172 |
-
|
| 173 |
-
const
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
}
|
| 181 |
-
await new Promise(r => setTimeout(r, 1500));
|
| 182 |
}
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
// Main refresh function
|
| 189 |
window.refreshShorts = async function() {
|
| 190 |
try {
|
| 191 |
toast('🔄 Đang tải shorts mới từ YouTube...');
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
if (fresh.length > 0) {
|
|
|
|
| 194 |
window._shortsData = fresh;
|
|
|
|
|
|
|
|
|
|
| 195 |
localStorage.setItem('vnews_shorts', JSON.stringify({ ts: Date.now(), data: fresh }));
|
| 196 |
toast('✓ Đã cập nhật ' + fresh.length + ' shorts mới!');
|
| 197 |
-
// Re-render if on home
|
| 198 |
-
if (typeof _renderShortsIn === 'function') {
|
| 199 |
-
const el = document.getElementById('home-after-wc');
|
| 200 |
-
if (el) _renderShortsIn(el);
|
| 201 |
-
}
|
| 202 |
return fresh;
|
| 203 |
} else {
|
| 204 |
toast('⚠️ Không tải được shorts mới');
|
|
@@ -222,12 +259,16 @@
|
|
| 222 |
return null;
|
| 223 |
}
|
| 224 |
|
| 225 |
-
// Init
|
| 226 |
const cached = loadCache();
|
| 227 |
-
if (cached)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
// Auto refresh 4s after page load
|
| 230 |
setTimeout(() => { window.refreshShorts().catch(() => {}); }, 4000);
|
| 231 |
|
| 232 |
-
console.log('[Shorts] Fresh fetcher
|
| 233 |
})();
|
|
|
|
| 1 |
/**
|
| 2 |
+
* VNEWS Fresh Shorts Fetcher v3
|
| 3 |
+
* Fetches latest shorts from YouTube, updates slide in-place
|
|
|
|
| 4 |
*/
|
| 5 |
(function() {
|
| 6 |
'use strict';
|
|
|
|
| 18 |
setTimeout(() => { t.style.display = 'none'; }, 4000);
|
| 19 |
}
|
| 20 |
|
| 21 |
+
function esc(s) {
|
| 22 |
+
return String(s||'').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function proxyImg(url) {
|
| 26 |
+
if (!url || typeof url !== 'string') return '';
|
| 27 |
+
if (url.startsWith('http') && !url.includes(location.host)) {
|
| 28 |
+
return '/api/proxy/img?url=' + encodeURIComponent(url);
|
| 29 |
+
}
|
| 30 |
+
return url;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
async function fetchWithRetry(url, maxRetries) {
|
| 34 |
for (let i = 0; i < maxRetries; i++) {
|
| 35 |
try {
|
|
|
|
| 55 |
let shortsTab = null;
|
| 56 |
for (const tab of tabs) {
|
| 57 |
const tr = tab.tabRenderer || {};
|
| 58 |
+
const content = tr.content || {};
|
| 59 |
+
if (content.richGridRenderer) {
|
| 60 |
+
shortsTab = content.richGridRenderer;
|
| 61 |
+
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
}
|
| 63 |
}
|
|
|
|
| 64 |
if (!shortsTab) return [];
|
| 65 |
|
| 66 |
const items = shortsTab.contents || [];
|
|
|
|
| 74 |
// New YouTube format: shortsLockupViewModel
|
| 75 |
if (inner.shortsLockupViewModel) {
|
| 76 |
const slvm = inner.shortsLockupViewModel;
|
|
|
|
|
|
|
| 77 |
let vid = '';
|
| 78 |
+
const cmd = (slvm.onTap || {}).innertubeCommand || {};
|
|
|
|
| 79 |
vid = (cmd.watchEndpoint || {}).videoId || (cmd.reelWatchEndpoint || {}).videoId || '';
|
|
|
|
| 80 |
if (!vid || seen.has(vid)) continue;
|
| 81 |
seen.add(vid);
|
| 82 |
|
| 83 |
+
let title = (slvm.overlayMetadata || {}).primaryText?.content || slvm.accessibilityText || 'YouTube Short';
|
| 84 |
+
let img = (slvm.thumbnail?.sources || [])[0]?.url || '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
shorts.push({
|
| 87 |
id: vid,
|
|
|
|
| 92 |
source: 'yt'
|
| 93 |
});
|
| 94 |
}
|
|
|
|
| 95 |
// Old format: reelItemRenderer
|
| 96 |
else if (inner.reelItemRenderer) {
|
| 97 |
const rir = inner.reelItemRenderer;
|
| 98 |
const vid = rir.videoId || '';
|
| 99 |
if (!vid || seen.has(vid)) continue;
|
| 100 |
seen.add(vid);
|
|
|
|
| 101 |
shorts.push({
|
| 102 |
id: vid,
|
| 103 |
+
title: (rir.headline || 'YouTube Short').substring(0, 120),
|
| 104 |
img: 'https://i.ytimg.com/vi/' + vid + '/hqdefault.jpg',
|
| 105 |
link: 'https://www.youtube.com/shorts/' + vid,
|
| 106 |
channel: handle,
|
| 107 |
source: 'yt'
|
| 108 |
});
|
| 109 |
}
|
|
|
|
| 110 |
// videoRenderer fallback
|
| 111 |
else if (inner.videoRenderer) {
|
| 112 |
const vr = inner.videoRenderer;
|
|
|
|
| 128 |
if (shorts.length >= 30) break;
|
| 129 |
}
|
| 130 |
|
| 131 |
+
// Last resort: regex
|
| 132 |
if (shorts.length === 0) {
|
| 133 |
const vids = [...new Set([...html.matchAll(/"videoId":"([A-Za-z0-9_-]{11})"/g)].map(m => m[1]))];
|
| 134 |
for (const vid of vids) {
|
|
|
|
| 150 |
return shorts;
|
| 151 |
}
|
| 152 |
|
| 153 |
+
// Interleave shorts from different channels
|
| 154 |
+
function interleaveShorts(shorts) {
|
| 155 |
+
const dt = shorts.filter(s => s.channel === 'baodantri7941');
|
| 156 |
+
const sk = shorts.filter(s => s.channel === 'baosuckhoedoisongboyte');
|
| 157 |
+
const result = [];
|
| 158 |
+
let i = 0, j = 0;
|
| 159 |
+
while (i < dt.length || j < sk.length) {
|
| 160 |
+
if (i < dt.length) result.push(dt[i++]);
|
| 161 |
+
if (j < sk.length) result.push(sk[j++]);
|
| 162 |
+
}
|
| 163 |
+
return result;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// Re-render the shorts slide in-place
|
| 167 |
+
function rerenderShortsSlide(shorts) {
|
| 168 |
+
// Find existing shorts slide
|
| 169 |
+
let existingSlide = document.getElementById('shorts-live-slide');
|
| 170 |
+
if (!existingSlide) {
|
| 171 |
+
// Try to find by label
|
| 172 |
+
const labels = document.querySelectorAll('.slider-wrap .slider-label');
|
| 173 |
+
for (const label of labels) {
|
| 174 |
+
if ((label.textContent || '').includes('Shorts')) {
|
| 175 |
+
existingSlide = label.closest('.slider-wrap');
|
| 176 |
+
break;
|
| 177 |
+
}
|
| 178 |
}
|
|
|
|
| 179 |
}
|
| 180 |
+
|
| 181 |
+
// Build new slide HTML
|
| 182 |
+
const mixed = interleaveShorts(shorts);
|
| 183 |
+
if (!mixed.length) return;
|
| 184 |
+
|
| 185 |
+
let h = '<div class="slider-header"><span class="slider-label">📱 Shorts Dân trí & SKĐS</span><span class="slider-note">🟢 Live từ YouTube · vừa cập nhật</span></div><div class="slider-track">';
|
| 186 |
+
mixed.slice(0, 30).forEach((a, i) => {
|
| 187 |
+
const badge = a.channel === 'baosuckhoedoisongboyte' ? 'SKĐS' : 'Dân trí';
|
| 188 |
+
h += '<div class="slider-item shorts-item" onclick="openYTShortsFeed(' + i + ')"><div class="slider-thumb shorts-thumb">' + (a.img ? '<img src="' + esc(proxyImg(a.img)) + '" loading="lazy">' : '') + '<div class="card-play">▶</div></div><div class="slider-title"><span style="color:#f0c040;font-size:8px">' + badge + '</span> ' + esc(a.title) + '</div></div>';
|
| 189 |
+
});
|
| 190 |
+
h += '</div>';
|
| 191 |
+
|
| 192 |
+
if (existingSlide) {
|
| 193 |
+
// Update in place
|
| 194 |
+
existingSlide.innerHTML = h;
|
| 195 |
+
existingSlide.id = 'shorts-live-slide';
|
| 196 |
+
existingSlide.style.boxShadow = '0 0 8px #5cb87a';
|
| 197 |
+
setTimeout(() => { existingSlide.style.boxShadow = 'none'; }, 2000);
|
| 198 |
+
} else {
|
| 199 |
+
// Create new slide
|
| 200 |
+
const wrap = document.createElement('div');
|
| 201 |
+
wrap.id = 'shorts-live-slide';
|
| 202 |
+
wrap.className = 'slider-wrap';
|
| 203 |
+
wrap.innerHTML = h;
|
| 204 |
+
const homeEl = document.getElementById('view-home');
|
| 205 |
+
if (homeEl) {
|
| 206 |
+
homeEl.insertBefore(wrap, homeEl.children[1] || null);
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
|
| 211 |
// Main refresh function
|
| 212 |
window.refreshShorts = async function() {
|
| 213 |
try {
|
| 214 |
toast('🔄 Đang tải shorts mới từ YouTube...');
|
| 215 |
+
|
| 216 |
+
const all = [];
|
| 217 |
+
for (const ch of CHANNELS) {
|
| 218 |
+
const url = 'https://www.youtube.com/@' + ch.handle + '/shorts';
|
| 219 |
+
const html = await fetchWithRetry(url, 3);
|
| 220 |
+
if (html) {
|
| 221 |
+
const shorts = extractShorts(html, ch.handle);
|
| 222 |
+
all.push(...shorts);
|
| 223 |
+
}
|
| 224 |
+
await new Promise(r => setTimeout(r, 1500));
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
// Deduplicate
|
| 228 |
+
const seen = new Set();
|
| 229 |
+
const fresh = all.filter(s => seen.has(s.id) ? false : (seen.add(s.id), true));
|
| 230 |
+
|
| 231 |
if (fresh.length > 0) {
|
| 232 |
+
// Update global data
|
| 233 |
window._shortsData = fresh;
|
| 234 |
+
// Re-render the slide
|
| 235 |
+
rerenderShortsSlide(fresh);
|
| 236 |
+
// Save to localStorage
|
| 237 |
localStorage.setItem('vnews_shorts', JSON.stringify({ ts: Date.now(), data: fresh }));
|
| 238 |
toast('✓ Đã cập nhật ' + fresh.length + ' shorts mới!');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
return fresh;
|
| 240 |
} else {
|
| 241 |
toast('⚠️ Không tải được shorts mới');
|
|
|
|
| 259 |
return null;
|
| 260 |
}
|
| 261 |
|
| 262 |
+
// Init
|
| 263 |
const cached = loadCache();
|
| 264 |
+
if (cached) {
|
| 265 |
+
window._shortsData = cached;
|
| 266 |
+
// Re-render with cached data too
|
| 267 |
+
setTimeout(() => rerenderShortsSlide(cached), 3000);
|
| 268 |
+
}
|
| 269 |
|
| 270 |
// Auto refresh 4s after page load
|
| 271 |
setTimeout(() => { window.refreshShorts().catch(() => {}); }, 4000);
|
| 272 |
|
| 273 |
+
console.log('[Shorts] Fresh fetcher v3 loaded');
|
| 274 |
})();
|