diff options
-rw-r--r-- | main.js | 212 | ||||
-rw-r--r-- | package.json | 19 |
2 files changed, 231 insertions, 0 deletions
@@ -0,0 +1,212 @@ +"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 [+-](<num>[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; diff --git a/package.json b/package.json new file mode 100644 index 0000000..880ea50 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "cmus-remote-node", + "version": "0.1.0", + "description": "API to cmus-remote for controlling cmus", + "main": "main.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://git.tomsmeding.com/cmus-remote-node" + }, + "keywords": [ + "cmus", + "cmus-remote" + ], + "author": "Tom Smeding <tom.smeding@gmail.com> (https://tomsmeding.com)", + "license": "MIT" +} |