Report phone playback metadata once per browser session
This commit is contained in:
@@ -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 */ }
|
||||
|
||||
@@ -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>
|
||||
|
||||
34
frontend/server-backfill.js
Normal file
34
frontend/server-backfill.js
Normal 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);
|
||||
26
frontend/server-backfill.test.js
Normal file
26
frontend/server-backfill.test.js
Normal 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));
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user