diff options
| -rw-r--r-- | src/features/messages.js | 17 | ||||
| -rw-r--r-- | tools/media-grid-dups-test.js | 8 |
2 files changed, 18 insertions, 7 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 4900c40..6122ade 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -6768,6 +6768,7 @@ export function messagesFeature(ctx) { const videoProbes = new Map(); let videoProbing = 0; const videoProbeQueue = []; + const clipGone = (url) => { const d = videoInfo.get(url); return !!d && typeof d.goneAt === 'number' && Date.now() - d.goneAt < 86_400_000; }; const probeFailedLately = (d) => !!d && typeof d.failedAt === 'number' && Date.now() - d.failedAt < 3_600_000; function probeVideo(url, layout = false) { const have = videoInfo.get(url); @@ -6801,7 +6802,7 @@ export function messagesFeature(ctx) { // player's plain load answers this request without them, and it fails. // Any clip that had played in the feed was "unmeasurable" that way. const r = await fetch(url, { headers: { Range: 'bytes=0-' + (VIDEO_PROBE_BYTES - 1) }, cache: 'no-store', signal: ctrl.signal }); - if (!r.ok) throw new Error('HTTP ' + r.status); + if (!r.ok) throw Object.assign(new Error('HTTP ' + r.status), { status: r.status }); const ranges = r.status === 206; const total = ranges ? +(/\/(\d+)\s*$/.exec(r.headers.get('content-range') || '') || [])[1] : +r.headers.get('content-length'); // never more than the first bytes, even from a server that ignored the range @@ -6818,7 +6819,7 @@ export function messagesFeature(ctx) { let o = 0; for (const p of parts) { if (o >= buf.length) break; buf.set(p.subarray(0, buf.length - o), o); o += p.length; } const head = readMp4Head(buf); - const d = { probed: true, ranges, faststart: head.faststart === true, failedAt: undefined, ...(head.moovEnd ? { moovEnd: head.moovEnd } : {}), ...(head.hevc ? { hevc: true } : {}), ...(head.dur && !(videoInfo.get(url) || {}).dur ? { dur: head.dur } : {}) }; + const d = { probed: true, ranges, faststart: head.faststart === true, failedAt: undefined, goneAt: undefined, ...(head.moovEnd ? { moovEnd: head.moovEnd } : {}), ...(head.hevc ? { hevc: true } : {}), ...(head.dur && !(videoInfo.get(url) || {}).dur ? { dur: head.dur } : {}) }; if (total > 0) d.size = total; else { // a server that hides Content-Range from scripts (Primal's) still @@ -6832,8 +6833,13 @@ export function messagesFeature(ctx) { if (head.w && !(videoInfo.get(url) || {}).w) { d.w = head.w; d.h = head.h; } if (head.w) rememberVideo(url, head.w, head.h); noteVideoInfo(url, d); - } catch { - noteVideoInfo(url, { failedAt: Date.now() }); // a host that won't say (no CORS, down, slow): asked again in an hour + } catch (e) { + // a host that won't say (no CORS, down, slow): asked again in an hour. + // One that says the file is gone (deleted: a 404 or 410) is believed + // for a day, and the grid leaves the clip out (clipGone). + const gone = e && (e.status === 404 || e.status === 410); + noteVideoInfo(url, { failedAt: Date.now(), ...(gone ? { goneAt: Date.now() } : {}) }); + if (gone && mediaMode === 'videos') scheduleRepaint(); } finally { clearTimeout(timer); } return videoInfo.get(url); } @@ -13965,7 +13971,8 @@ export function messagesFeature(ctx) { const again = saidAgain(ev); if (again && said.has(again) && said.get(again) !== ev.id) continue; if (kind === 'videos') learnVideoMeta(ev); - const urls = kind === 'videos' ? clipUrlsOf(ev).filter(fitClip) + // (a clip whose host says it's deleted has nothing to show: a blank tile) + const urls = kind === 'videos' ? clipUrlsOf(ev).filter((u) => !clipGone(u)).filter(fitClip) : noteMediaUrls(ev.content).filter((u) => !u.startsWith('https://i.ytimg.com/')); // (the words are taken by a post that shows: one whose clip doesn't fit // the screen leaves them to the next) diff --git a/tools/media-grid-dups-test.js b/tools/media-grid-dups-test.js index 360ae0c..0077f48 100644 --- a/tools/media-grid-dups-test.js +++ b/tools/media-grid-dups-test.js @@ -30,6 +30,7 @@ const server = Bun.serve({ port: 0, fetch: (req) => { const a = +range[1], b = range[2] ? Math.min(+range[2], f.size - 1) : f.size - 1; return new Response(f.slice(a, b + 1), { status: 206, headers: { 'content-type': 'video/webm', 'accept-ranges': 'bytes', 'content-range': 'bytes ' + a + '-' + b + '/' + f.size, 'content-length': String(b - a + 1) } }); } + if (path === '/dead.webm') return new Response('gone', { status: 404 }); if (path === '/still.png') return new Response(png, { headers: { 'content-type': 'image/png' } }); if (path.startsWith('/punks') || path === '/verify-worker.js') return new Response(Bun.file('dist' + path)); return new Response(html, { headers: { 'content-type': 'text/html' } }); @@ -45,6 +46,8 @@ const seeded = [ post('Transylvania lightning conference 2026 ' + U('again'), U('again'), 200), post('Transylvania lightning conference 2026 ' + U('other'), U('other'), 300), post('the last clip ' + U('last'), U('last'), 400), + // a clip its host has since deleted (it says so: 404) — no blank tile for it + finalizeEvent({ kind: 1, created_at: now - 500, tags: [['imeta', 'url ' + U('dead'), 'm video/webm', 'dim 320x180']], content: 'deleted since ' + U('dead') }, SK), ]; const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox', '--autoplay-policy=no-user-gesture-required'] }); @@ -80,12 +83,13 @@ try { await page.reload({ waitUntil: 'domcontentloaded' }); await waitText('receive', 20000); await page.evaluate(() => document.querySelector('.app-nav-button[aria-label="Feed"]').click()); - check('the clips are in the feed', await waitFor(() => document.querySelectorAll('.notes-feed video.note-video').length >= 6, null, 20000)); + check('the clips are in the feed', await waitFor(() => document.querySelectorAll('.notes-feed video.note-video').length >= 7, null, 20000)); await page.evaluate(() => document.querySelector('.media-mode[data-mode="videos"]').click()); await waitFor(() => document.querySelectorAll('.media-grid .media-tile').length >= 3, null, 15000); + await waitFor(() => document.querySelectorAll('.media-grid .media-tile').length === 3, null, 8000); await sleep(1500); const tiles = await page.evaluate(() => document.querySelectorAll('.media-grid .media-tile').length); - check('one tile per clip: the reposted file once, the re-uploaded post once, the other one', tiles === 3, tiles + ' tiles'); + check('one tile per clip: the reposted file once, the re-uploaded post once, the other one, none for the deleted one', tiles === 3, tiles + ' tiles'); await page.evaluate(() => document.querySelector('.media-grid .media-tile').click()); check('the pager opens', await waitFor(() => !!document.querySelector('.mp-slide.mp-cur video.mp-media'), null, 6000)); check('the clip is drawn no bigger than 1.5× its pixels', await waitFor(() => { const v = document.querySelector('.mp-slide.mp-cur video.mp-media'); return v && v.videoWidth && v.getBoundingClientRect().width <= v.videoWidth * 1.5 + 1 && v.style.maxWidth; }, null, 8000), |
