Repository navigation
Expand file tree
/
Copy pathserver.mjs
More file actions
16 lines (15 loc) · 4.33 KB
/
Copy pathserver.mjs
File metadata and controls
16 lines (15 loc) · 4.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {createAudioLeases} from './audio-leases.mjs';
import {startSpotifyProxy} from './proxy.mjs';import {execFile} from 'node:child_process';import {promisify} from 'node:util';
import {DeskThing} from '@deskthing/server';import {readFile,writeFile} from 'node:fs/promises';import {fileURLToPath} from 'node:url';import {spawn} from 'node:child_process';import {createSpotify,lyricsFor} from './music.mjs';
const spotify=createSpotify({auth:async()=>{try{return JSON.parse(await readFile(new URL('../../../data/spotify.json',import.meta.url),'utf8')).data?.access_token}catch{return null}}});
const exec=promisify(execFile),adb='/Applications/DeskThing.app/Contents/Resources/mac/adb';let proxy=null;
async function prepareVideo(){if(!proxy)proxy=startSpotifyProxy();const list=(await exec(adb,['devices'],{timeout:5000})).stdout;const devices=list.split(/\r?\n/).map(l=>l.match(/^(\S+)\s+device$/)).filter(Boolean).map(m=>m[1]);if(devices.length!==1)throw Error('Connect one Car Thing over USB for video.');const prefix=['-s',devices[0]];await exec(adb,[...prefix,'reverse','tcp:8892','tcp:8892'],{timeout:5000});const date=new Date().toISOString().slice(0,19).replace('T',' ');await exec(adb,[...prefix,'shell',`date -u -s '${date}'`],{timeout:5000});return{ready:true};}
DeskThing.on('start',async()=>{const old=await DeskThing.getSettings()||{};await DeskThing.initSettings({spotify_relay:{type:'boolean',label:'Enable legacy Spotify HTTPS relay',description:'Optional for devices previously configured to use the Spotify-only USB relay. Does not enable protected video playback.',value:old.spotify_relay?.value??false}});if(old.spotify_relay?.value===true)prepareVideo().catch(()=>{})});
let capture=null,leaseTimer=null,lyricCache=new Map();const leases=createAudioLeases(),send=(type,payload)=>DeskThing.send({type,payload});
function stop(){const child=capture;capture=null;if(child)child.kill('SIGTERM')}
function listen(){if(capture)return;const child=spawn(fileURLToPath(new URL('./visualizer-capture',import.meta.url)),[],{stdio:['ignore','pipe','ignore']});capture=child;let buffer='';child.stdout.on('data',chunk=>{if(capture!==child)return;buffer+=chunk;const lines=buffer.split('\n');buffer=lines.pop();for(const line of lines)try{send('visualizer-frame',JSON.parse(line))}catch{}});child.on('error',()=>{if(capture===child){send('visualizer-frame',{status:'error',message:'Audio helper could not start.'});stop()}});child.on('exit',()=>{if(capture===child)capture=null});}
function syncAudio(){if(leases.active())listen();else stop()}
DeskThing.on('visualizer',message=>{const owner=message.payload?.owner==='spotify'?'spotify':'musicthing';leases.update(owner,!!message.payload?.active);syncAudio();if(!leaseTimer)leaseTimer=setInterval(syncAudio,1000)});
DeskThing.on('client_status',message=>{if(message.request==='closed'){leases.update('musicthing',false);syncAudio()}});DeskThing.on('stop',()=>{clearInterval(leaseTimer);leaseTimer=null;leases.clear();stop();proxy?.close();proxy=null});
DeskThing.on('music-request',async message=>{const {section,value,id}=message.payload||{};try{let data;if(section==='transport')data=await spotify.transport(value);else if(section==='video-ready')data=await prepareVideo();else if(section==='playing')data=await spotify.playback();else if(section==='queue')data=await spotify.queue();else if(section==='recent')data=await spotify.recent();else if(section==='devices')data=await spotify.devices();else if(section==='transfer')data=await spotify.transfer(value);else if(section==='play'){await spotify.play(value);data={ok:true}}else if(section==='enqueue'){await spotify.enqueue(value);data={ok:true}}else if(section==='lyrics'){const {track}=await spotify.playback();if(lyricCache.has(track?.id))data=lyricCache.get(track.id);else{data=await lyricsFor(track);if(track?.id){lyricCache.set(track.id,data);if(lyricCache.size>20)lyricCache.delete(lyricCache.keys().next().value)}}}else throw Error('Unknown music action.');send('music-result',{section,id,data})}catch(e){send('music-result',{section,id,error:e.message||'Music request failed.'})}});
DeskThing.on('capabilities',async message=>{const p=message.payload||{};await writeFile(new URL('../../../data/musicthing-diagnostics.json',import.meta.url),JSON.stringify({mp4:String(p.mp4||''),webm:String(p.webm||''),encryptedMedia:!!p.encryptedMedia,widevine:!!p.widevine,checkedAt:Date.now()}),{mode:0o600}).catch(()=>{})});