bep40 commited on
Commit
c0d9cad
·
verified ·
1 Parent(s): 6db3bcc

Upload static/shorts_fresh.js with huggingface_hub

Browse files
Files changed (1) hide show
  1. static/shorts_fresh.js +105 -64
static/shorts_fresh.js CHANGED
@@ -1,7 +1,6 @@
1
  /**
2
- * VNEWS Fresh Shorts Fetcher v2
3
- * Fetches latest shorts from YouTube page via allorigins proxy
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
- if (tr.title === 'Shorts' || tr.selected) {
48
- // Check if it has richGrid with shorts
49
- const content = tr.content || {};
50
- if (content.richGridRenderer) {
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 onTap = slvm.onTap || {};
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
- // Get title
92
- let title = '';
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: title.substring(0, 120),
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 fallback
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
- // Fetch all channels
172
- window.fetchFreshShorts = async function() {
173
- const all = [];
174
- for (const ch of CHANNELS) {
175
- const url = 'https://www.youtube.com/@' + ch.handle + '/shorts';
176
- const html = await fetchWithRetry(url, 3);
177
- if (html) {
178
- const shorts = extractShorts(html, ch.handle);
179
- all.push(...shorts);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
- await new Promise(r => setTimeout(r, 1500));
182
  }
183
- // Deduplicate
184
- const seen = new Set();
185
- return all.filter(s => seen.has(s.id) ? false : (seen.add(s.id), true));
186
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
  // Main refresh function
189
  window.refreshShorts = async function() {
190
  try {
191
  toast('🔄 Đang tải shorts mới từ YouTube...');
192
- const fresh = await window.fetchFreshShorts();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: use cache first, then refresh in background
226
  const cached = loadCache();
227
- if (cached) window._shortsData = cached;
 
 
 
 
228
 
229
  // Auto refresh 4s after page load
230
  setTimeout(() => { window.refreshShorts().catch(() => {}); }, 4000);
231
 
232
- console.log('[Shorts] Fresh fetcher v2 loaded');
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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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
  })();