summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--main.js212
-rw-r--r--package.json19
2 files changed, 231 insertions, 0 deletions
diff --git a/main.js b/main.js
new file mode 100644
index 0000000..6ef391f
--- /dev/null
+++ b/main.js
@@ -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"
+}