Files
hub/tools/player-test.js
T
nmemmertandClaude Sonnet 5.5 b0d3aec811
build-container / build (push) Successful in 12m40s
Home dashboard: live Now Playing card; report playback to Plex so it shows in the Plex dashboard
The Home Now Playing block is now the same live card as the Music tab.
The player sends Plex timeline updates (playing/paused/stopped, position every 10 s).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 15:14:34 -04:00

42 lines
4.3 KiB
JavaScript

const fs=require('fs'), vm=require('vm');
const src=fs.readFileSync(''+require('path').join(__dirname,'..','public','app.js')+'','utf8');
const a=src.indexOf("const shuffled = a =>"), b=src.indexOf("// ---- People (admin)");
let code=src.slice(a,b);
const listeners={};
class FakeAudio{constructor(){this.paused=true;this.currentTime=0;this.duration=100;this.readyState=4;this.error=null;this._l={};this.src='';}
addEventListener(e,f){(this._l[e]=this._l[e]||[]).push(f)} fire(e){(this._l[e]||[]).forEach(f=>f())}
play(){this.paused=false;this.fire('play');return Promise.resolve()} pause(){if(!this.paused){this.paused=true;this.fire('pause')}} getAttribute(){return this.src||null} load(){}}
const toasts=[];const store={d:{},get(k,d){return this.d[k]??d},set(k,v){this.d[k]=v},del(k){delete this.d[k]}};
const el=()=>({remove(){},classList:{toggle(){},add(){}},setAttribute(){},dataset:{},style:{},querySelector:()=>el(),querySelectorAll:()=>[],set innerHTML(v){},set textContent(v){}});
const ctx={Audio:FakeAudio,store,toast:m=>toasts.push(m),px:{art:()=>'x'},icon:()=>'',esc:s=>s,fmtDur:()=>'',$:()=>el(),document:{querySelectorAll:()=>[],querySelector:()=>null},allowed:()=>true,
navigator:{onLine:true},addEventListener:(e,f)=>{listeners[e]=f},setTimeout,clearTimeout,fetch:()=>new Promise(()=>{}),URL:{createObjectURL:()=>'blob:x',revokeObjectURL(){}},
Image:class{},MediaMetadata:class{},openSheet:()=>({}),closeSheet(){},actionSheet(){},Response,URLSearchParams,AbortController,Promise,Set,Map,Date,Math,saveStateHook:0,location:{hash:'#/tasks'}};
vm.createContext(ctx); vm.runInContext(code+";this.pl=pl;this.next=next;this.prev=prev;this.setShuffle=setShuffle;this.setRepeat=setRepeat;this.playQueue=playQueue;this.advance=advance;this.cur=cur;this.recover=recover;this.srcOf=srcOf;",ctx);
const T=n=>Array.from({length:n},(_,i)=>({key:'k'+i,title:'t'+i,artist:'a',album:'',thumb:'',src:'/plex/p'+i,dur:1000}));
const eq=(n,c)=>{console.log((c?'PASS ':'FAIL ')+n); if(!c)process.exitCode=1};
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
(async()=>{
const {pl}=ctx;
ctx.playQueue(T(5),0); eq('plays first',pl.i===0&&pl.a.src==='/plex/p0');
ctx.next(); ctx.next(); ctx.next(); eq('rapid next shows track 3 at once',pl.i===3); await sleep(300); eq('only last loaded',pl.a.src==='/plex/p3');
ctx.setShuffle(true); eq('shuffle keeps current + same tracks',pl.q[pl.i].key==='k3'&&pl.q.length===5&&new Set(pl.q.map(x=>x.key)).size===5&&pl.q.slice(0,4).map(x=>x.key).join()==='k0,k1,k2,k3');
ctx.setShuffle(false); eq('shuffle off restores order',pl.q.map(x=>x.key).join()==='k0,k1,k2,k3,k4'&&pl.i===3);
ctx.setRepeat('all'); ctx.advance(true); eq('advance to end',pl.i===4); await sleep(10); ctx.advance(true); eq('repeat all wraps',pl.i===0);
ctx.setRepeat('one'); pl.a.currentTime=50; ctx.advance(true); eq('repeat one restarts',pl.i===0&&pl.a.currentTime===0);
ctx.setRepeat('off'); pl.i=4; ctx.advance(true); eq('end of queue pauses',pl.a.paused&&pl.i===4);
// undecodable file -> transcode fallback, then skip
ctx.playQueue(T(3),0); await sleep(10); pl.a.paused=false; pl.a.readyState=0; pl.a.error={code:4};
ctx.recover(true); await sleep(400); eq('falls back to transcode',pl.alt.has('k0')&&pl.a.src.includes('transcode'));
pl.a.error={code:4}; ctx.recover(true); await sleep(400); await sleep(300); eq('then skips unplayable track',pl.i===1&&toasts.some(t=>t.includes('Skipping')));
// network stall retries same source
pl.a.paused=false;pl.a.readyState=1;pl.a.error=null; const before=pl.retries; ctx.recover(true); await sleep(400); eq('stall reloads stream',pl.retries===before+1);
// Plex is told what is playing (shows in its dashboard)
const sent=[]; ctx.fetch=u=>{sent.push(new URL(u,'http://x').searchParams);return Promise.resolve()};
ctx.playQueue(T(3),0); await sleep(300); pl.a.currentTime=12; pl.a.fire('playing');
eq('tells Plex it is playing',sent.length===1&&sent[0].get('state')==='playing'&&sent[0].get('ratingKey')==='k0'&&sent[0].get('time')==='12000');
pl.a.pause(); eq('tells Plex it paused',sent[sent.length-1].get('state')==='paused');
ctx.advance(false); await sleep(300); pl.a.fire('playing');
eq('stops the old song, plays the new',sent.some(p=>p.get('state')==='stopped'&&p.get('ratingKey')==='k0')&&sent[sent.length-1].get('ratingKey')==='k1');
process.exit(process.exitCode||0);
})();