Report phone playback metadata once per browser session

This commit is contained in:
Jonathan Sykes
2026-10-03 17:56:01 +08:00
parent 856ddda6a6
commit 8ea92c1aac
6 changed files with 73 additions and 0 deletions

View File

@@ -2952,6 +2952,7 @@ function wirePlayerEvents() {
});
el.addEventListener('playing', () => {
if (!masterIs(el)) return;
if (WEB && current?.meta) window.ServerBackfill?.report(current.meta);
if (performance.getEntriesByName('ytp:tap').length) {
perfMeasure('ytp:tap-to-play', 'ytp:tap');
try { performance.clearMarks('ytp:tap'); } catch { /* ignore */ }

View File

@@ -662,6 +662,7 @@
<script src="downloads.js"></script>
<script src="downloads-page.js"></script>
<script src="saved-page.js"></script>
<script src="server-backfill.js"></script>
<script src="party-dj.js"></script>
<script src="lyrics-window.js"></script>
<script src="midi.js"></script>

View File

@@ -0,0 +1,34 @@
/* Best-effort discovery for playback that never requests server streams. */
(function(root) {
'use strict';
function createReporter({ fetch: send = root.fetch?.bind(root), online = () => root.navigator?.onLine !== false, enabled = () => true } = {}) {
const seen = new Set(), deferred = new Map();
function report(meta) {
if (!enabled() || !send || !meta || !/^[A-Za-z0-9_-]{11}$/.test(meta.id) || meta.custom || meta.upload || !meta.title || meta.title === meta.id || seen.has(meta.id)) return;
// Snapshot only the known metadata, never stream URLs or local blob art.
let thumbnail;
try {
const url = new URL(meta.thumbnail);
if (url.protocol === 'https:' && /^(?:i|i\d)\.ytimg\.com$/.test(url.hostname) && !url.port && !url.username && !url.password) thumbnail = url.href;
} catch { /* server supplies canonical art */ }
const card = { id: meta.id, title: String(meta.title).slice(0,300), channel: String(meta.channel || meta.artist || meta.uploader || '').slice(0,300),
duration: Math.max(0, Math.min(86400, Number(meta.duration) || 0)), ...(thumbnail ? { thumbnail } : {}) };
if (!online()) {
deferred.set(meta.id, card);
if (deferred.size > 256) deferred.delete(deferred.keys().next().value);
return;
}
seen.add(meta.id); deferred.delete(meta.id);
// No synchronous/network failure can interrupt the player event handler.
try {
Promise.resolve(send(`/api/media/${encodeURIComponent(card.id)}/meta`, {
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(card),
signal: root.AbortSignal?.timeout?.(10000),
})).catch(() => {});
} catch { /* best effort; a later session can report again */ }
}
return { report, flush: () => { if (online()) for (const card of [...deferred.values()]) report(card); } };
}
if (typeof module !== 'undefined') module.exports = { createReporter };
else { const reporter = createReporter(); root.ServerBackfill = reporter; root.addEventListener('online', reporter.flush); }
})(typeof window !== 'undefined' ? window : globalThis);

View File

@@ -0,0 +1,26 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { createReporter } = require('./server-backfill.js');
const meta={id:'0gfX0dFLaBc',title:'Because You are God',artist:'Cathedral of Praise Worship',duration:240,thumbnail:'blob:device-art'};
test('first playback reports known metadata once per video per session',()=>{
const requests=[];const reporter=createReporter({fetch:(url,options)=>{requests.push({url,options});return Promise.resolve({ok:true});}});
reporter.report(meta);reporter.report(meta);reporter.report({...meta,id:'bbbbbbbbbbb'});
assert.equal(requests.length,2);assert.equal(requests[0].url,'/api/media/0gfX0dFLaBc/meta');
const card=JSON.parse(requests[0].options.body);assert.equal(card.channel,meta.artist);assert.equal(card.duration,240);assert.equal(card.thumbnail,undefined);
assert.equal(requests[0].options.method,'POST');
});
test('offline plays defer until online and repeated events do not multiply reports',()=>{
let online=false;const requests=[];const reporter=createReporter({online:()=>online,fetch:(...args)=>{requests.push(args);return Promise.resolve();}});
reporter.report(meta);reporter.report(meta);reporter.flush();assert.equal(requests.length,0);
online=true;reporter.flush();reporter.flush();reporter.report(meta);assert.equal(requests.length,1);
});
test('uploads, edits, unknown titles and disabled native shells never report',()=>{
const requests=[];const fetch=(...args)=>{requests.push(args);};const reporter=createReporter({fetch});
for(const item of [{...meta,id:'upl_123'},{...meta,custom:true},{...meta,upload:true},{...meta,title:meta.id},{...meta,title:''}])reporter.report(item);
createReporter({fetch,enabled:()=>false}).report(meta);assert.equal(requests.length,0);
});
test('synchronous and rejected network errors are isolated from playback',async()=>{
assert.doesNotThrow(()=>createReporter({fetch:()=>{throw new Error('offline');}}).report(meta));
assert.doesNotThrow(()=>createReporter({fetch:()=>Promise.reject(new Error('offline'))}).report(meta));
await new Promise(resolve=>setImmediate(resolve));
});

View File

@@ -88,6 +88,7 @@ const SHELL = [
'/downloads-page.js',
'/offline-pages.css',
'/saved-page.js',
'/server-backfill.js',
'/saved-page.css',
'/party-dj.js',
'/lyrics-window.js',