"use strict"; const spawn=require("child_process").spawn; const READ_TIMEOUT=50; const KILL_TIMEOUT=1000; function mProcessClosed(){ this._.open=false; if(this._.killtimeout){ clearTimeout(this._.killtimeout); } } function mCheckQueue(){ if(!this._.open){ throw new Error("cmus-remote process was closed"); } if(!this._.datahandler&&this._.queue.length>0){ const [l,h,eR]=this._.queue.shift(); // console.log("Committing write <<"+l+">>"); this._.proc.stdin.write(l+"\n","utf8"); if(eR){ this._.datahandler=h; } else { h.call(this); mCheckQueue.call(this); } } } function mSubmitLine(line,expectResponse,handler){ if(!handler)throw 0; this._.queue.push([line,handler,expectResponse]); mCheckQueue.call(this); } class CmusRemote{ constructor(socketloc /* optional */){ this._={}; let args=[]; if(socketloc)args=args.concat(["--server",socketloc]); this._.proc=spawn("cmus-remote",args,{ stdio: ["pipe","pipe",process.stderr] }); this._.open=true; this._.proc.on("close",mProcessClosed.bind(this)); this._.proc.on("exit",mProcessClosed.bind(this)); this._.proc.on("error",(err)=>{ throw err; }); this._.queue=[]; let buffer=""; this._.proc.stdout.on("data",(data)=>{ if(data instanceof Buffer){ data=new String(data); } buffer+=data; if(this._.readtimeout){ clearTimeout(this._.readtimeout); } this._.readtimeout=setTimeout((()=>{ // console.log("Readtimeout: buffer=<<"+buffer+">>, datahandler=<<"+this._.datahandler+">>, queue=<<"+this._.queue+">>"); if(buffer.length>0&&this._.datahandler){ let e=null; try { this._.datahandler.call(this,buffer); } catch(err){ e=err; } buffer=""; this._.datahandler=null; mCheckQueue.call(this); if(e)throw e; } }).bind(this),READ_TIMEOUT); }); } close(){ this._.proc.stdin.end(); this._.killtimeout=setTimeout((()=>{ this._.proc.kill("SIGTERM"); this._.killtimeout=setTimeout((()=>{ this._.proc.kill("SIGKILL"); this._.killtimeout=null; }).bind(this),KILL_TIMEOUT); }).bind(this),KILL_TIMEOUT); } ready(){ return this._.open; } status(cb){ mSubmitLine.call(this,"status",true,(str)=>{ // console.log("Status internal callback called"); const lines=str.split("\n"); const obj={ track: {}, settings: {} }; for(const line of lines){ if(line.length==0)continue; const idx=line.indexOf(" "); if(idx==-1){ throw new Error("Unexpected line on 'status' output from cmus-remote: '"+line+"'"); } const head=line.slice(0,idx),rest=line.slice(idx+1); if(head=="status"){ obj.status=rest; } else if(head=="file"||head=="duration"||head=="position"){ obj.track[head]=rest; } else if(head=="tag"||head=="set"){ const idx=rest.indexOf(" "); if(idx==-1){ throw new Error("Unexpected line on 'status' output from cmus-remote: '"+line+"'"); } const tag=rest.slice(0,idx),rest2=rest.slice(idx+1); obj[head=="tag"?"track":"settings"][tag]=rest2; } } cb(obj); }); } // This requests data of THE ENTIRE LIBRARY. Use with care on large libraries. getLibrary(cb){ mSubmitLine.call(this,"save -e -l -",true,(str)=>{ const lines=str.split("\n"); const tracks=[]; let track=null; for(const line of lines){ if(line.length==0)continue; const idx=line.indexOf(" "); if(idx==-1){ throw new Error("Unexpected line on 'save -l -' output from cmus-remote: '"+line+"'"); } const head=line.slice(0,idx),rest=line.slice(idx+1); if(head=="file"){ if(track)tracks.push(track); track={}; track.file=rest; continue; } if(!track){ throw new Error("Expected 'file' key on output 'save -l -' from cmus-remote: '"+line+"'"); } if(head=="duration"||head=="codec"||head=="bitrate"){ track[head]=rest; } else if(head=="tag"){ const idx=rest.indexOf(" "); if(idx==-1){ throw new Error("Unexpected line on 'save -l -' output from cmus-remote: '"+line+"'"); } const tag=rest.slice(0,idx),rest2=rest.slice(idx+1); track[tag]=rest2; } } if(track)tracks.push(track); cb(tracks); }); } play(filename /* optional */){ mSubmitLine.call(this,"player-play"+(filename?" "+filename:""),false,()=>{}); } pause(){ mSubmitLine.call(this,"player-pause",false,()=>{}); } next(){ mSubmitLine.call(this,"player-next",false,()=>{}); } prev(){ mSubmitLine.call(this,"player-prev",false,()=>{}); } stop(){ mSubmitLine.call(this,"player-stop",false,()=>{}); } /* From the cmus man page: * * seek [+-]([mh] | [HH:]MM:SS) * Seek to absolute or relative position. Position can be given in seconds, * minutes (m), hours (h) or HH:MM:SS format where HH: is optional. * * Seek 1 minute backward * :seek -1m * * Seek 5 seconds forward * :seek +5 * * Seek to absolute position 1h * :seek 1h * * Seek 90 seconds forward * :seek +1:30 */ seek(repr){ mSubmitLine.call(this,"seek "+repr,false,()=>{}); } } module.exports=CmusRemote;