Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add playlist creation, deletion, export and import #515

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
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