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 #105

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/helpers/soap.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ const TYPE = Object.freeze({
BecomeCoordinatorOfStandaloneGroup: 'urn:schemas-upnp-org:service:AVTransport:1#BecomeCoordinatorOfStandaloneGroup',
RefreshShareIndex: 'urn:schemas-upnp-org:service:ContentDirectory:1#RefreshShareIndex',
AddURIToQueue: 'urn:schemas-upnp-org:service:AVTransport:1#AddURIToQueue',
AddURIToSavedQueue: 'urn:schemas-upnp-org:service:AVTransport:1#AddURIToSavedQueue',
CreateSavedQueue: 'urn:schemas-upnp-org:service:AVTransport:1#CreateSavedQueue',
DestroyObject: 'urn:schemas-upnp-org:service:ContentDirectory:1#DestroyObject',
AddMultipleURIsToQueue: 'urn:schemas-upnp-org:service:AVTransport:1#AddMultipleURIsToQueue',
ListAvailableServices: 'urn:schemas-upnp-org:service:MusicServices:1#ListAvailableServices',
});
Expand Down Expand Up @@ -60,6 +63,9 @@ const TEMPLATES = Object.freeze({
[TYPE.BecomeCoordinatorOfStandaloneGroup]: '<u:BecomeCoordinatorOfStandaloneGroup xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID></u:BecomeCoordinatorOfStandaloneGroup>',
[TYPE.RefreshShareIndex]: '<u:RefreshShareIndex xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1"><AlbumArtistDisplayOption></AlbumArtistDisplayOption></u:RefreshShareIndex>',
[TYPE.AddURIToQueue]: '<u:AddURIToQueue xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><EnqueuedURI>{uri}</EnqueuedURI><EnqueuedURIMetaData>{metadata}</EnqueuedURIMetaData><DesiredFirstTrackNumberEnqueued>{desiredFirstTrackNumberEnqueued}</DesiredFirstTrackNumberEnqueued><EnqueueAsNext>{enqueueAsNext}</EnqueueAsNext></u:AddURIToQueue>',
[TYPE.AddURIToSavedQueue]: '<u:AddURIToSavedQueue xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><ObjectID>SQ:{sqid}</ObjectID><UpdateID>{updateID}</UpdateID><EnqueuedURI>{uri}</EnqueuedURI><EnqueuedURIMetaData>&lt;DIDL-Lite xmlns:dc=&quot;http://purl.org/dc/elements/1.1/&quot; xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot; xmlns:r=&quot;urn:schemas-rinconnetworks-com:metadata-1-0/&quot; xmlns=&quot;urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/&quot;&gt;&lt;item id=&quot;{itemId}&quot; parentID=&quot;&quot; restricted=&quot;true&quot;&gt;&lt;dc:title&gt;{title}&lt;/dc:title&gt;&lt;upnp:class&gt;{upnpClass}&lt;/upnp:class&gt;&lt;desc id=&quot;cdudn&quot; nameSpace=&quot;urn:schemas-rinconnetworks-com:metadata-1-0/&quot;&gt;RINCON_AssociatedZPUDN&lt;/desc&gt;&lt;/item&gt;&lt;/DIDL-Lite&gt;</EnqueuedURIMetaData><AddAtIndex>4294967295</AddAtIndex></u:AddURIToSavedQueue>',
[TYPE.CreateSavedQueue]: '<u:CreateSavedQueue xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><Title>{title}</Title><EnqueuedURI></EnqueuedURI><EnqueuedURIMetaData></EnqueuedURIMetaData></u:CreateSavedQueue>',
[TYPE.DestroyObject]: '<u:DestroyObject xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1"><ObjectID>{id}</ObjectID></u:DestroyObject>',
[TYPE.AddMultipleURIsToQueue]: '<u:AddMultipleURIsToQueue xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><UpdateID>0</UpdateID><NumberOfURIs>{amount}</NumberOfURIs><EnqueuedURIs>{uris}</EnqueuedURIs><EnqueuedURIsMetaData>{metadatas}</EnqueuedURIsMetaData><ContainerURI>{containerURI}</ContainerURI><ContainerMetaData>{containerMetadata}</ContainerMetaData><DesiredFirstTrackNumberEnqueued>{desiredFirstTrackNumberEnqueued}</DesiredFirstTrackNumberEnqueued><EnqueueAsNext>{enqueueAsNext}</EnqueueAsNext></u:AddMultipleURIsToQueue>',
[TYPE.ListAvailableServices]: '<u:ListAvailableServices xmlns:u="urn:schemas-upnp-org:service:MusicServices:1"></u:ListAvailableServices>'
});
Expand Down
109 changes: 106 additions & 3 deletions lib/models/Player.js
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,108 @@ Player.prototype.saveQueue = function saveQueue(title) {
});
};

Player.prototype.createSavedQueue = function createSavedQueue(title) {
return soap.invoke(
`${this.baseUrl}/MediaRenderer/AVTransport/Control`,
TYPE.CreateSavedQueue,
{
title
}).then(soap.parse);
};

Player.prototype.destroyObject = function destroyObject(id) {
return soap.invoke(
`${this.baseUrl}/MediaServer/ContentDirectory/Control`,
TYPE.DestroyObject,
{
id
}).then(soap.parse);
};

Player.prototype.destroyByTitle = function destroyByTitle(name) {
return this.browseAll('SQ:')
.then((playlists) => {
let match;
for (var i = playlists.length - 1; i >= 0; i--) {
const playlist = playlists[i];
if (playlist.title.toLowerCase() === name.toLowerCase()) {
match = playlist;
break;
}
}

return 'SQ:' + match.sqid;
}).
then((id) => {
return this.destroyObject(id);
});
};

Player.prototype.addURIToSavedQueue = function addURIToSavedQueue(name, uri, title) {
let upnpClass = 'object.item.audioItem.musicTrack';
let itemId;
if (uri.indexOf('savedqueues.rsq') !== -1) {
// append a playlist into another
upnpClass = 'object.container.playlistContainer';
itemId = 'SQ:' + uri.match('#(\\d+)$')[1];
} else {
// TODO test multi controller setup
if (uri.indexOf('x-file-cifs') !== -1) {
// for DIDL CDATA, per wireshark, remove protocols like x-file-cifs://
// I don't have a windows or android setup so I don't know if S:// is assumed by device as a shared volume (on macOS, yes)
itemId = 'S://' + uri.replace(/(.*:\/\/)/, '');
}
}

return this.browseAll('SQ:')
.then((playlists) => {
let match;
for (var i = playlists.length - 1; i >= 0; i--) {
const playlist = playlists[i];
if (playlist.title.toLowerCase() === name.toLowerCase()) {
match = playlist;
break;
}
}

return match;
}).then((match) => {
const sqid = match.sqid;
if (uri.indexOf('savedqueues.rsq') !== -1) {

// the title is not the rsq destination name but the origin name
title = match.title;
}

return this.browse('SQ:' + sqid, 0, 100).then((res) => {
const updateID = res.updateID;
logger.info(`will import to sqid ${sqid} at index ${updateID} with : ${uri}, ${title}, DIDL metadata : itemId ${itemId}, title ${title}`);
return soap.invoke(
`${this.baseUrl}/MediaRenderer/AVTransport/Control`,
TYPE.AddURIToSavedQueue,
{
sqid,
uri,
itemId,
title,
updateID,
upnpClass
}).catch((err) => {
const path = err.path;
const statusCode = err.statusCode;
const statusMessage = err.statusMessage;
const body = err.body;
const msg = `import error for ${uri}@SQ:${sqid}:${path}: ${statusCode}, ${statusMessage}, ${body}`;
logger.error(msg, err);
return Promise.reject(new Error(msg));
});
}
);
}
)
.then(soap.parse);
};

Player.prototype.addURIToQueue = function addURIToQueue(uri, metadata, enqueueAsNext, desiredFirstTrackNumberEnqueued) {
desiredFirstTrackNumberEnqueued =
desiredFirstTrackNumberEnqueued === undefined
Expand Down Expand Up @@ -740,9 +842,9 @@ Player.prototype.browse = function browse(objectId, startIndex, limit) {
startIndex,
items: []
};

returnResult.numberReturned = parseInt(res.numberreturned, 10);
returnResult.totalMatches = parseInt(res.totalmatches, 10);
returnResult.updateID = parseInt(res.updateid, 10);

let stream = streamer(res.result);
let sax = flow(stream, { preserveMarkup: flow.NEVER });
Expand All @@ -762,12 +864,12 @@ Player.prototype.browse = function browse(objectId, startIndex, limit) {
});

sax.on('tag:container', (item) => {

returnResult.items.push({
uri: item.res.$text,
title: item['dc:title'],
artist: item['dc:creator'],
albumArtUri: item['upnp:albumarturi']
albumArtUri: item['upnp:albumarturi'],
sqid: item.$attrs.id.replace(/SQ:/, '')
});
});

Expand Down Expand Up @@ -805,6 +907,7 @@ Player.prototype.browseAll = function browseAll(objectId) {
}

// Recursive promise chain

return this.browse(objectId, chunk.startIndex + chunk.numberReturned, 0)
.then(getChunk);
};
Expand Down
22 changes: 22 additions & 0 deletions lib/prototypes/Player/createPlaylist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';
const logger = require('../../helpers/logger');

function createPlaylist(title) {
logger.debug(`creating playlist with name ${title}`);

if (!title) {
throw new Error('No playlist name provided');
}

return this.createSavedQueue(title)
.then((res) => {
return {
result: 'success',
title: title,
sqid: res.assignedobjectid.replace(/SQ:/, '')
};
});
}

module.exports = createPlaylist;

20 changes: 20 additions & 0 deletions lib/prototypes/Player/deletePlaylist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use strict';
const logger = require('../../helpers/logger');

function deletePlaylist(name) {
logger.debug(`deleting playlist with name ${name}`);

if (!name) {
throw new Error('No playlist name provided');
}

return this.destroyByTitle(name)
.then((res) => {
return {
result: 'success'
};
});

}

module.exports = deletePlaylist;
47 changes: 47 additions & 0 deletions lib/prototypes/Player/exportPlaylist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';
const logger = require('../../helpers/logger');

function exportPlaylist(id) {
logger.debug(`exporting playlist using id ${id}`);
if (id) {
return this.system.getPlaylists()
.then((playlists) => {
let match;
for (var i = playlists.length - 1; i >= 0; i--) {
const playlist = playlists[i];
if (playlist.title.toLowerCase() === id.toLowerCase()) {
match = playlist;
break;
}
};

return match || {};
})
.then((playlist) => {
const ptitle = playlist.title;
if (ptitle === undefined) {
return {};
}

const psqid = playlist.sqid;
return this.system.getPlaylists(psqid)
.then((playlist) => {
playlist.title = ptitle;
playlist.sqid = psqid;
return {
title: ptitle,
sqid: psqid,
items: playlist
};
});
});
}

return this.system.getPlaylists(id)
.then((playlist) => {
return playlist;
});
}

module.exports = exportPlaylist;

18 changes: 18 additions & 0 deletions lib/prototypes/Player/importPlaylist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict';
const logger = require('../../helpers/logger');

function importPlaylist(sqid, uri, title) {
if (!sqid || !uri || !title) {
throw new Error('No playlist id or title provided or no URI provided');
};

return this.addURIToSavedQueue(sqid, uri, title)
.then((res) => {
return { result: 'success', import: res };
}).catch((err) => {
throw new Error(err);
});
}

module.exports = importPlaylist;

4 changes: 2 additions & 2 deletions lib/prototypes/SonosSystem/getPlaylists.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

function getPlaylists() {
return this.getAnyPlayer().browseAll('SQ:');
function getPlaylists(id) {
return this.getAnyPlayer().browseAll(id ? 'SQ:' + id : 'SQ:');
}

module.exports = getPlaylists;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sonos-discovery",
"version": "1.4.1",
"version": "1.4.2",
"description": "A simple node library for managing your Sonos system",
"author": "Jimmy Shimizu <[email protected]>",
"repository": {
Expand Down
9 changes: 9 additions & 0 deletions test/data/addURIToSavedQueue.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:AddURIToSavedQueueResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<NumTracksAdded>1</NumTracksAdded>
<NewQueueLength>1</NewQueueLength>
<NewUpdateID>1</NewUpdateID>
</u:AddURIToSavedQueueResponse>
</s:Body>
</s:Envelope>
10 changes: 10 additions & 0 deletions test/data/createSavedQueue.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:CreateSavedQueueResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<NumTracksAdded>0</NumTracksAdded>
<NewQueueLength>0</NewQueueLength>
<NewUpdateID>0</NewUpdateID>
<AssignedObjectID>SQ:1</AssignedObjectID>
</u:CreateSavedQueueResponse>
</s:Body>
</s:Envelope>
5 changes: 5 additions & 0 deletions test/data/destroyObject.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:DestroyObjectResponse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1"></u:DestroyObjectResponse>
</s:Body>
</s:Envelope>
Loading