Skip to content
Open
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ The actions supported as of today:
* favorite
* favorites (with optional "detailed" parameter)
* playlist
* playlistcreate
* playlistdelete
* playlistexport
* playlistimport
* lockvolumes / unlockvolumes (experimental, will enforce the volume that was selected when locking!)
* repeat (on/off)
* shuffle (on/off)
Expand Down
70 changes: 70 additions & 0 deletions lib/actions/playlist.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,59 @@
'use strict';
const sleep = require('system-sleep');

function createPlaylist(player, values) {
const playlistName = decodeURIComponent(values[0]);
return player.coordinator
.createPlaylist(playlistName)
.then((res) => {
return res;
}
);
}

function deletePlaylist(player, values) {
const sqid = decodeURIComponent(values[0]);
return player.coordinator
.deletePlaylist(sqid)
.then((res) => {
return res;
}
);
}

function importPlaylist(player, values) {
const sqid = decodeURIComponent(values[0]);
if (values.body !== undefined) {
// multi uri
const items = values.body.export.items;
return Promise.resolve().then(_ => {
items.map(item => {
const title = item.title;
const uri = item.uri;
// there is no way to batch import in Controller UI, we must import one by one
// since adduri requires a synchronous browse for index updateID, we pause.
// pause should be low enough for the HTTP roundtrips, tested on WiFi + play:5
sleep(600);
return player.coordinator.importPlaylist(sqid, uri, title);
});
}).then((res) => {
return res;
}).catch((err) => {
throw new Error(err);
});
}
else {
// single uri or internal rsq jffs to jffs appending
const title = decodeURIComponent(values[1]);
const uri = values.slice(2).map(x => "/" + x).join().replace(/,/g,'').replace(/\//,'');
return player.coordinator
.importPlaylist(sqid, uri, title)
.then((res) => {
return res;
}
);
}
}

function playlist(player, values) {
const playlistName = decodeURIComponent(values[0]);
Expand All @@ -7,6 +62,21 @@ function playlist(player, values) {
.then(() => player.coordinator.play());
}

function exportPlaylist(player, values) {
var id = decodeURIComponent(values[0]);
return player.coordinator
.exportPlaylist(id)
.then((res) => {
return {
export : res
};
});
}

module.exports = function (api) {
api.registerAction('playlist', playlist);
api.registerAction('playlistexport', exportPlaylist);
api.registerAction('playlistcreate', createPlaylist);
api.registerAction('playlistdelete', deletePlaylist);
api.registerAction('playlistimport', importPlaylist);
};
2 changes: 2 additions & 0 deletions lib/sonos-http-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function HttpAPI(discovery, settings) {
});

this.requestHandler = function (req, res) {

if (req.url === '/favicon.ico') {
res.end();
return;
Expand All @@ -73,6 +74,7 @@ function HttpAPI(discovery, settings) {
opt.action = (params[0] || '').toLowerCase();
opt.values = params.splice(1);
}
opt.values.body = req.body;

function sendResponse(code, body) {
var jsonResponse = JSON.stringify(body);
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sonos-http-api",
"version": "1.4.1",
"version": "1.4.2",
"description": "A simple node app for controlling a Sonos system with basic HTTP requests",
"scripts": {
"start": "node server.js"
Expand All @@ -18,7 +18,8 @@
"json5": "^0.5.1",
"node-static": "~0.7.0",
"request-promise": "~1.0.2",
"sonos-discovery": "https://github.com/jishi/node-sonos-discovery/archive/v1.4.1.tar.gz"
"sonos-discovery": "file:../node-sonos-discovery",
"system-sleep": "^1.3.5"
},
"engines": {
"node": ">=4.0.0",
Expand Down
24 changes: 22 additions & 2 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ const discovery = new SonosSystem(settings);
const api = new SonosHttpAPI(discovery, settings);

var requestHandler = function (req, res) {
let body = '';

if (req.method === 'POST') {
req.on('data', function (data) {
body += data;

// Too much POST data, kill the connection!
// 1e6 === 1 * Math.pow(10, 6) === 1 * 1000000 ~~~ 1MB
if (body.length > 1e6) {
req.connection.destroy();
}
});
}
req.addListener('end', function () {
fileServer.serve(req, res, function (err) {

Expand Down Expand Up @@ -45,8 +58,15 @@ var requestHandler = function (req, res) {
res.end();
return;
}

if (req.method === 'GET') {
if (req.method === 'POST') {
req.body = body;
try {
req.body = JSON.parse(body);
} catch(e) {
logger.error("Invalid JSON body", e);
}
}
if (req.method === 'GET' || req.method === 'POST') {
api.requestHandler(req, res);
}
});
Expand Down
8 changes: 7 additions & 1 deletion static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ <h3>Actions</h3>
<li><strong>previous</strong></li>
<li><strong>state</strong> (will return a json-representation of the current state of player)</li>
<li><strong>favorite</strong></li>
<li><strong>playlist</strong></li>
<li><strong>playlist</strong> (parameter is playlist name, will play)</li>
<li><strong>playlistcreate</strong> (parameter is playlist name, will return a suitable <code>name</code> used for export, import and delete, see below)</li>
<li><strong>playlistdelete</strong> (parameter is <code>name</code> of the playlist)</li>
<li><strong>playlistexport</strong> for backup purposes in case of factory reset or hardware failure (will return a json-representation of all playlists (optional parameter is <code>name</code>, will return the desired playlist contents)</li>
<li><strong>playlistimport</strong> (parameters : <code>name</code> of the playlist / optional: track or playlist name / optional: track or playlist uri obtained by playlistexport. Example : <code>/target playlist name/track title/x-file-cifs://MacBook-Air-de-laurent-2/Music/iTunes/iTunes Media/Music/artist/album/track file.mp3</code> or for appending a playlist to another one : <code>/target playlist name/source playlist name/file:///jffs/settings/savedqueues.rsq#2</code>. Note : invalid local uri track paths will not trigger an error, the device currently does not return an UPnP error code for unmatched Controller files.
<br/>
to import a whole playlist : save the response generated by playlistexport/<code>name</code> as a file, and POST the file contents to playlistimport/<code>name</code>. Example: <code>curl -v POST http://localhost:5005/playlistimport/playlist name/places.json -d @export.json --header "Content-Type: application/json"</code>. Be patient, large playlists can take time.</li>
<li><strong>lockvolumes</strong> / <strong>unlockvolumes</strong> (experimental, will enforce the volume that was selected when locking!)</li>
<li><strong>repeat</strong> (on/off)</li>
<li><strong>shuffle</strong> (on/off)</li>
Expand Down