Files
hub/tools/player-test.js
T
nmemmertandClaude Sonnet 5.5 522fdba4cd
build-container / build (push) Successful in 7m4s
Now Playing card on the Music tab; Signal alerts (chime, desktop notification, unread count in tab title and app icon)
- Music: a card with art, seek bar and controls above the albums, artists and playlists
- Signal: chime, optional desktop notification when the app is in the background, (n) in the tab title, app badge; Settings -> Signal alerts (per device)
- README, player test stand-in updated

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

35 lines
3.6 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},
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:''}};
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);
process.exit(process.exitCode||0);
})();