From 503caf8744a82f9e4f015123738cbcc7948bdf09 Mon Sep 17 00:00:00 2001 From: laurentperez Date: Sun, 21 May 2017 15:00:45 +0200 Subject: [PATCH 01/12] enable playlist export and creation --- lib/helpers/soap.js | 2 ++ lib/models/Player.js | 12 +++++++++++- lib/prototypes/Player/createPlaylist.js | 18 ++++++++++++++++++ lib/prototypes/Player/exportPlaylist.js | 20 ++++++++++++++++++++ lib/prototypes/Player/replaceWithPlaylist.js | 1 + lib/prototypes/SonosSystem/getPlaylists.js | 4 ++-- package.json | 2 +- 7 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 lib/prototypes/Player/createPlaylist.js create mode 100644 lib/prototypes/Player/exportPlaylist.js diff --git a/lib/helpers/soap.js b/lib/helpers/soap.js index d83c5ac7..b7030c91 100644 --- a/lib/helpers/soap.js +++ b/lib/helpers/soap.js @@ -31,6 +31,7 @@ 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', + CreateSavedQueue: 'urn:schemas-upnp-org:service:AVTransport:1#CreateSavedQueue', AddMultipleURIsToQueue: 'urn:schemas-upnp-org:service:AVTransport:1#AddMultipleURIsToQueue', ListAvailableServices: 'urn:schemas-upnp-org:service:MusicServices:1#ListAvailableServices', }); @@ -60,6 +61,7 @@ const TEMPLATES = Object.freeze({ [TYPE.BecomeCoordinatorOfStandaloneGroup]: '0', [TYPE.RefreshShareIndex]: '', [TYPE.AddURIToQueue]: '0{uri}{metadata}{desiredFirstTrackNumberEnqueued}{enqueueAsNext}', + [TYPE.CreateSavedQueue]: '0{title}', [TYPE.AddMultipleURIsToQueue]: '00{amount}{uris}{metadatas}{containerURI}{containerMetadata}{desiredFirstTrackNumberEnqueued}{enqueueAsNext}', [TYPE.ListAvailableServices]: '' }); diff --git a/lib/models/Player.js b/lib/models/Player.js index cf3fd216..3d43e1e2 100644 --- a/lib/models/Player.js +++ b/lib/models/Player.js @@ -578,6 +578,15 @@ 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.addURIToQueue = function addURIToQueue(uri, metadata, enqueueAsNext, desiredFirstTrackNumberEnqueued) { desiredFirstTrackNumberEnqueued = desiredFirstTrackNumberEnqueued === undefined @@ -767,7 +776,8 @@ Player.prototype.browse = function browse(objectId, startIndex, limit) { uri: item.res.$text, title: item['dc:title'], artist: item['dc:creator'], - albumArtUri: item['upnp:albumarturi'] + albumArtUri: item['upnp:albumarturi'], + sqid: item['$attrs'].id }); }); diff --git a/lib/prototypes/Player/createPlaylist.js b/lib/prototypes/Player/createPlaylist.js new file mode 100644 index 00000000..acf8449b --- /dev/null +++ b/lib/prototypes/Player/createPlaylist.js @@ -0,0 +1,18 @@ +'use strict'; +const logger = require('../../helpers/logger'); +const util = require('util'); + +function createPlaylist(title) { + console.log("createplaylist:" + title); + if(!title) { + throw new Error('No playlist name provided'); + } + return this.createSavedQueue(title) + .then((res) => { + console.log("createPlaylist res:", res); + return { sqid: res.assignedobjectid }; + }); +} + +module.exports = createPlaylist; + diff --git a/lib/prototypes/Player/exportPlaylist.js b/lib/prototypes/Player/exportPlaylist.js new file mode 100644 index 00000000..cfc0b140 --- /dev/null +++ b/lib/prototypes/Player/exportPlaylist.js @@ -0,0 +1,20 @@ +'use strict'; +const logger = require('../../helpers/logger'); +const util = require('util'); + +function exportPlaylist(sqid) { + logger.debug(`exporting with playlist id ${sqid}`); + console.log(`exporting with playlist id ${sqid}`); + return this.system.getPlaylists(sqid) + .then((playlists) => { + console.log(`found playlists by id:` + sqid + "->" + playlists.map(x => console.log(util.inspect(x, false, null)))); + //return playlists.find((list) => list.title.toLowerCase() === playlistName.toLowerCase()); + //console.log("1->" + playlists); + //console.log("2->" + playlists.map(x => console.log(util.inspect(x, false, null)))); + //playlists.map(x => console.log("exportPlaylistId map:" + util.inspect(x, false, null))); + return playlists; + }); +} + +module.exports = exportPlaylist; + diff --git a/lib/prototypes/Player/replaceWithPlaylist.js b/lib/prototypes/Player/replaceWithPlaylist.js index 061f840b..fd8f9c1d 100644 --- a/lib/prototypes/Player/replaceWithPlaylist.js +++ b/lib/prototypes/Player/replaceWithPlaylist.js @@ -29,3 +29,4 @@ function replaceWithPlaylist(playlistName) { } module.exports = replaceWithPlaylist; + diff --git a/lib/prototypes/SonosSystem/getPlaylists.js b/lib/prototypes/SonosSystem/getPlaylists.js index d4ebbe4c..365e0b75 100644 --- a/lib/prototypes/SonosSystem/getPlaylists.js +++ b/lib/prototypes/SonosSystem/getPlaylists.js @@ -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; diff --git a/package.json b/package.json index d05ea13d..ef4b9bd8 100644 --- a/package.json +++ b/package.json @@ -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 ", "repository": { From 618f7c09bc06ab54c68afd8960e1f8878dcff2ae Mon Sep 17 00:00:00 2001 From: laurentperez Date: Sun, 21 May 2017 18:37:47 +0200 Subject: [PATCH 02/12] enable playlist import --- lib/helpers/soap.js | 2 ++ lib/models/Player.js | 17 +++++++++++++++++ lib/prototypes/Player/importPlaylist.js | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 lib/prototypes/Player/importPlaylist.js diff --git a/lib/helpers/soap.js b/lib/helpers/soap.js index b7030c91..b977171e 100644 --- a/lib/helpers/soap.js +++ b/lib/helpers/soap.js @@ -31,6 +31,7 @@ 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', AddMultipleURIsToQueue: 'urn:schemas-upnp-org:service:AVTransport:1#AddMultipleURIsToQueue', ListAvailableServices: 'urn:schemas-upnp-org:service:MusicServices:1#ListAvailableServices', @@ -61,6 +62,7 @@ const TEMPLATES = Object.freeze({ [TYPE.BecomeCoordinatorOfStandaloneGroup]: '0', [TYPE.RefreshShareIndex]: '', [TYPE.AddURIToQueue]: '0{uri}{metadata}{desiredFirstTrackNumberEnqueued}{enqueueAsNext}', + [TYPE.AddURIToSavedQueue]: '0SQ:{sqid}{updateID}x-file-cifs://{uri}<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="S://{uri}" parentID="" restricted="true"><dc:title>{title}</dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">RINCON_AssociatedZPUDN</desc></item></DIDL-Lite>4294967295', [TYPE.CreateSavedQueue]: '0{title}', [TYPE.AddMultipleURIsToQueue]: '00{amount}{uris}{metadatas}{containerURI}{containerMetadata}{desiredFirstTrackNumberEnqueued}{enqueueAsNext}', [TYPE.ListAvailableServices]: '' diff --git a/lib/models/Player.js b/lib/models/Player.js index 3d43e1e2..f72ff932 100644 --- a/lib/models/Player.js +++ b/lib/models/Player.js @@ -587,6 +587,22 @@ Player.prototype.createSavedQueue = function createSavedQueue(title) { }).then(soap.parse); }; +Player.prototype.addURIToSavedQueue = function addURIToSavedQueue(sqid, uri, title) { + return this.browse('SQ:' + sqid, 0, 100).then((res) => { + let updateID = res.updateID; + return soap.invoke( + `${this.baseUrl}/MediaRenderer/AVTransport/Control`, + TYPE.AddURIToSavedQueue, + { + sqid, + uri, + title, + updateID + }) + } + ).then(soap.parse); +}; + Player.prototype.addURIToQueue = function addURIToQueue(uri, metadata, enqueueAsNext, desiredFirstTrackNumberEnqueued) { desiredFirstTrackNumberEnqueued = desiredFirstTrackNumberEnqueued === undefined @@ -752,6 +768,7 @@ Player.prototype.browse = function browse(objectId, startIndex, limit) { 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 }); diff --git a/lib/prototypes/Player/importPlaylist.js b/lib/prototypes/Player/importPlaylist.js new file mode 100644 index 00000000..4114ca29 --- /dev/null +++ b/lib/prototypes/Player/importPlaylist.js @@ -0,0 +1,19 @@ +'use strict'; +const logger = require('../../helpers/logger'); +const util = require('util'); + +function importPlaylist(sqid,uri,title) { + console.log("importPlaylist:" + sqid + ',' + uri + ',' + title); + if(!sqid || !uri) { + throw new Error('No playlist id or uri provided'); + }; + + return this.addURIToSavedQueue(sqid, uri, title) + .then((res) => { + console.log("importPlaylist res:", res); + return { out : res }; + }); +} + +module.exports = importPlaylist; + From 1ba6bdd361a5a950bb7e0b957114ac1fb0eb6984 Mon Sep 17 00:00:00 2001 From: laurentperez Date: Sun, 21 May 2017 19:55:40 +0200 Subject: [PATCH 03/12] gardening --- lib/helpers/soap.js | 2 +- lib/models/Player.js | 7 +++---- lib/prototypes/Player/createPlaylist.js | 15 +++++++++------ lib/prototypes/Player/exportPlaylist.js | 13 ++++--------- lib/prototypes/Player/importPlaylist.js | 12 ++++++------ test/unit/models/Player.js | 1 + 6 files changed, 24 insertions(+), 26 deletions(-) diff --git a/lib/helpers/soap.js b/lib/helpers/soap.js index b977171e..563320d9 100644 --- a/lib/helpers/soap.js +++ b/lib/helpers/soap.js @@ -31,7 +31,7 @@ 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', + AddURIToSavedQueue: 'urn:schemas-upnp-org:service:AVTransport:1#AddURIToSavedQueue', CreateSavedQueue: 'urn:schemas-upnp-org:service:AVTransport:1#CreateSavedQueue', AddMultipleURIsToQueue: 'urn:schemas-upnp-org:service:AVTransport:1#AddMultipleURIsToQueue', ListAvailableServices: 'urn:schemas-upnp-org:service:MusicServices:1#ListAvailableServices', diff --git a/lib/models/Player.js b/lib/models/Player.js index f72ff932..e6b569a4 100644 --- a/lib/models/Player.js +++ b/lib/models/Player.js @@ -590,7 +590,7 @@ Player.prototype.createSavedQueue = function createSavedQueue(title) { Player.prototype.addURIToSavedQueue = function addURIToSavedQueue(sqid, uri, title) { return this.browse('SQ:' + sqid, 0, 100).then((res) => { let updateID = res.updateID; - return soap.invoke( + return soap.invoke( `${this.baseUrl}/MediaRenderer/AVTransport/Control`, TYPE.AddURIToSavedQueue, { @@ -598,7 +598,7 @@ Player.prototype.addURIToSavedQueue = function addURIToSavedQueue(sqid, uri, tit uri, title, updateID - }) + }); } ).then(soap.parse); }; @@ -788,13 +788,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'], - sqid: item['$attrs'].id + sqid: item.$attrs.id.replace(/SQ:/, '') }); }); diff --git a/lib/prototypes/Player/createPlaylist.js b/lib/prototypes/Player/createPlaylist.js index acf8449b..42432545 100644 --- a/lib/prototypes/Player/createPlaylist.js +++ b/lib/prototypes/Player/createPlaylist.js @@ -1,16 +1,19 @@ 'use strict'; const logger = require('../../helpers/logger'); -const util = require('util'); function createPlaylist(title) { - console.log("createplaylist:" + title); - if(!title) { + logger.debug(`creating playlist with name ${title}`); + + if (!title) { throw new Error('No playlist name provided'); } - return this.createSavedQueue(title) + + return this.createSavedQueue(title) .then((res) => { - console.log("createPlaylist res:", res); - return { sqid: res.assignedobjectid }; + return { + result: 'success', + sqid: res.assignedobjectid.replace(/SQ:/, '') + }; }); } diff --git a/lib/prototypes/Player/exportPlaylist.js b/lib/prototypes/Player/exportPlaylist.js index cfc0b140..5d1e7a22 100644 --- a/lib/prototypes/Player/exportPlaylist.js +++ b/lib/prototypes/Player/exportPlaylist.js @@ -3,16 +3,11 @@ const logger = require('../../helpers/logger'); const util = require('util'); function exportPlaylist(sqid) { - logger.debug(`exporting with playlist id ${sqid}`); - console.log(`exporting with playlist id ${sqid}`); + logger.debug(`exporting playlist id ${sqid}`); return this.system.getPlaylists(sqid) - .then((playlists) => { - console.log(`found playlists by id:` + sqid + "->" + playlists.map(x => console.log(util.inspect(x, false, null)))); - //return playlists.find((list) => list.title.toLowerCase() === playlistName.toLowerCase()); - //console.log("1->" + playlists); - //console.log("2->" + playlists.map(x => console.log(util.inspect(x, false, null)))); - //playlists.map(x => console.log("exportPlaylistId map:" + util.inspect(x, false, null))); - return playlists; + .then((playlist) => { + console.log(`found playlists by id:` + sqid + '->' + playlist.map(x => console.log(util.inspect(x, false, null)))); + return playlist; }); } diff --git a/lib/prototypes/Player/importPlaylist.js b/lib/prototypes/Player/importPlaylist.js index 4114ca29..a23cdc2a 100644 --- a/lib/prototypes/Player/importPlaylist.js +++ b/lib/prototypes/Player/importPlaylist.js @@ -2,16 +2,16 @@ const logger = require('../../helpers/logger'); const util = require('util'); -function importPlaylist(sqid,uri,title) { - console.log("importPlaylist:" + sqid + ',' + uri + ',' + title); - if(!sqid || !uri) { +function importPlaylist(sqid, uri, title) { + console.log('importPlaylist:' + sqid + ',' + uri + ',' + title); + if (!sqid || !uri) { throw new Error('No playlist id or uri provided'); }; - return this.addURIToSavedQueue(sqid, uri, title) + return this.addURIToSavedQueue(sqid, uri, title) .then((res) => { - console.log("importPlaylist res:", res); - return { out : res }; + console.log('importPlaylist res:', res); + return { result: 'success', import: res }; }); } diff --git a/test/unit/models/Player.js b/test/unit/models/Player.js index 5096da43..cc6ee5ba 100644 --- a/test/unit/models/Player.js +++ b/test/unit/models/Player.js @@ -828,6 +828,7 @@ describe('Player', () => { uri: 'file:///jffs/settings/savedqueues.rsq#2', title: 'Morgon', artist: undefined, + sqid: '2', albumArtUri: [ '/getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a35N1AduT1LDo3deLfYniTY%3fsid%3d9%26flags%3d0', '/getaa?s=1&u=x-sonos-spotify%3aspotify%253atrack%253a1MQYow43CGLYMECVSjTpCM%3fsid%3d9%26flags%3d0', From f821ae996a7c963299c76511a9581e6d1c38ee9a Mon Sep 17 00:00:00 2001 From: laurentperez Date: Sun, 21 May 2017 20:06:28 +0200 Subject: [PATCH 04/12] test for createSavedQueue --- test/data/createSavedQueue.xml | 10 ++++++++++ test/unit/models/Player.js | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 test/data/createSavedQueue.xml diff --git a/test/data/createSavedQueue.xml b/test/data/createSavedQueue.xml new file mode 100644 index 00000000..66f7694d --- /dev/null +++ b/test/data/createSavedQueue.xml @@ -0,0 +1,10 @@ + + + + 0 + 0 + 0 + SQ:1 + + + diff --git a/test/unit/models/Player.js b/test/unit/models/Player.js index cc6ee5ba..9f78ba31 100644 --- a/test/unit/models/Player.js +++ b/test/unit/models/Player.js @@ -668,6 +668,32 @@ describe('Player', () => { }); }); + it('createSavedQueue', () => { + soap.parse.restore(); + let createSavedQueueXml = fs.createReadStream(`${__dirname}/../../data/createSavedQueue.xml`); + createSavedQueueXml.statusCode = 200; + soap.invoke.resolves(createSavedQueueXml); + + expect(TYPE.CreateSavedQueue).not.undefined; + return player.createSavedQueue('myplaylist') + .then((result) => { + expect(result).eql({ + assignedobjectid: 'SQ:1', + newqueuelength: '0', + newupdateid: '0', + numtracksadded: '0' + }); + expect(soap.invoke).calledOnce; + expect(soap.invoke.firstCall.args).eql([ + 'http://192.168.1.151:1400/MediaRenderer/AVTransport/Control', + TYPE.CreateSavedQueue, + { + title: 'myplaylist' + } + ]); + }); + }); + it('addURIToQueue', () => { soap.parse.restore(); let addURIToQueueXml = fs.createReadStream(`${__dirname}/../../data/addURIToQueue.xml`); From 71e3b19a9eabc7657b3f818c150c48fe0c9cd9fd Mon Sep 17 00:00:00 2001 From: laurentperez Date: Sat, 27 May 2017 15:38:52 +0200 Subject: [PATCH 05/12] playlist delete and test --- lib/helpers/soap.js | 2 + lib/models/Player.js | 14 ++++- lib/prototypes/Player/deletePlaylist.js | 20 +++++++ lib/prototypes/Player/exportPlaylist.js | 2 - test/data/addURIToSavedQueue.xml | 9 +++ test/data/destroyObject.xml | 5 ++ test/unit/models/Player.js | 74 +++++++++++++++++++++++++ 7 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 lib/prototypes/Player/deletePlaylist.js create mode 100644 test/data/addURIToSavedQueue.xml create mode 100644 test/data/destroyObject.xml diff --git a/lib/helpers/soap.js b/lib/helpers/soap.js index 563320d9..2dc6a804 100644 --- a/lib/helpers/soap.js +++ b/lib/helpers/soap.js @@ -33,6 +33,7 @@ const TYPE = Object.freeze({ 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', }); @@ -64,6 +65,7 @@ const TEMPLATES = Object.freeze({ [TYPE.AddURIToQueue]: '0{uri}{metadata}{desiredFirstTrackNumberEnqueued}{enqueueAsNext}', [TYPE.AddURIToSavedQueue]: '0SQ:{sqid}{updateID}x-file-cifs://{uri}<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="S://{uri}" parentID="" restricted="true"><dc:title>{title}</dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">RINCON_AssociatedZPUDN</desc></item></DIDL-Lite>4294967295', [TYPE.CreateSavedQueue]: '0{title}', + [TYPE.DestroyObject]: '{id}', [TYPE.AddMultipleURIsToQueue]: '00{amount}{uris}{metadatas}{containerURI}{containerMetadata}{desiredFirstTrackNumberEnqueued}{enqueueAsNext}', [TYPE.ListAvailableServices]: '' }); diff --git a/lib/models/Player.js b/lib/models/Player.js index e6b569a4..2937b9d6 100644 --- a/lib/models/Player.js +++ b/lib/models/Player.js @@ -587,8 +587,20 @@ Player.prototype.createSavedQueue = function 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.addURIToSavedQueue = function addURIToSavedQueue(sqid, uri, title) { + // TODO do not prefix uris with x-file-cifs. use the raw export uri, but strip the cifs part for the DIDL cdata. + console.log('xxxxxxxxxxxxxx addURIToSavedQueue:', sqid + ',' + uri + ',' + title); return this.browse('SQ:' + sqid, 0, 100).then((res) => { + console.log('xxxxxxxxxxxxxx addURIToSavedQueue res', res); let updateID = res.updateID; return soap.invoke( `${this.baseUrl}/MediaRenderer/AVTransport/Control`, @@ -765,7 +777,7 @@ Player.prototype.browse = function browse(objectId, startIndex, limit) { startIndex, items: [] }; - + console.log('xxxxxxxxxxxxx1', returnResult); returnResult.numberReturned = parseInt(res.numberreturned, 10); returnResult.totalMatches = parseInt(res.totalmatches, 10); returnResult.updateID = parseInt(res.updateid, 10); diff --git a/lib/prototypes/Player/deletePlaylist.js b/lib/prototypes/Player/deletePlaylist.js new file mode 100644 index 00000000..4077646a --- /dev/null +++ b/lib/prototypes/Player/deletePlaylist.js @@ -0,0 +1,20 @@ +'use strict'; +const logger = require('../../helpers/logger'); + +function deletePlaylist(sqid) { + logger.debug(`deleting playlist with id ${sqid}`); + + if (!sqid) { + throw new Error('No playlist id provided'); + } + + return this.destroyObject('SQ:' + sqid) + .then((res) => { + return { + result: 'success' + }; + }); +} + +module.exports = deletePlaylist; + diff --git a/lib/prototypes/Player/exportPlaylist.js b/lib/prototypes/Player/exportPlaylist.js index 5d1e7a22..2b5fddb6 100644 --- a/lib/prototypes/Player/exportPlaylist.js +++ b/lib/prototypes/Player/exportPlaylist.js @@ -1,12 +1,10 @@ 'use strict'; const logger = require('../../helpers/logger'); -const util = require('util'); function exportPlaylist(sqid) { logger.debug(`exporting playlist id ${sqid}`); return this.system.getPlaylists(sqid) .then((playlist) => { - console.log(`found playlists by id:` + sqid + '->' + playlist.map(x => console.log(util.inspect(x, false, null)))); return playlist; }); } diff --git a/test/data/addURIToSavedQueue.xml b/test/data/addURIToSavedQueue.xml new file mode 100644 index 00000000..cb67e262 --- /dev/null +++ b/test/data/addURIToSavedQueue.xml @@ -0,0 +1,9 @@ + + + + 1 + 1 + 1 + + + diff --git a/test/data/destroyObject.xml b/test/data/destroyObject.xml new file mode 100644 index 00000000..d7f69a41 --- /dev/null +++ b/test/data/destroyObject.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/test/unit/models/Player.js b/test/unit/models/Player.js index 9f78ba31..b6992502 100644 --- a/test/unit/models/Player.js +++ b/test/unit/models/Player.js @@ -668,6 +668,60 @@ describe('Player', () => { }); }); + describe('addURIToSavedQueue browse', () => { + + beforeEach('We need parse functionality here', () => { + soap.parse.restore(); + }); + + describe('addURIToSavedQueue2', () => { + let playlist; + beforeEach(() => { + let queueStream = fs.createReadStream(path.join(__dirname, '../../data/playlists.xml')); + + soap.invoke.resolves(queueStream); + + return player.browse() + .then((res) => { + playlist = res; + }); + }); + + it('addURIToSavedQueue should browse first', () => { + console.log('yyyyyyyyyyy1', playlist); + let addURIToSavedQueueXml = fs.createReadStream(`${__dirname}/../../data/addURIToSavedQueue.xml`); + addURIToSavedQueueXml.statusCode = 200; + soap.invoke.resolves(addURIToSavedQueueXml); + + expect(TYPE.AddURIToSavedQueue).not.undefined; + return player.addURIToSavedQueue('1', 'myuri', 'mytitle') + .then((result) => { + expect(soap.invoke).calledThrice; + expect(soap.invoke.secondCall.args).eql([ + 'http://192.168.1.151:1400/MediaServer/ContentDirectory/Control', + TYPE.Browse, + { + limit: 100, + objectId: 'SQ:1', + startIndex: 0 + } + ]); + expect(soap.invoke.thirdCall.args).eql([ + 'http://192.168.1.151:1400/MediaRenderer/AVTransport/Control', + TYPE.AddURIToSavedQueue, + { + sqid: '1', + uri: 'myuri', + title: 'mytitle', + updateID: playlist.updateID + } + ]); + }); + }); + + }); + }); + it('createSavedQueue', () => { soap.parse.restore(); let createSavedQueueXml = fs.createReadStream(`${__dirname}/../../data/createSavedQueue.xml`); @@ -694,6 +748,26 @@ describe('Player', () => { }); }); + it('destroyObject', () => { + soap.parse.restore(); + let destroyObjectXml = fs.createReadStream(`${__dirname}/../../data/destroyObject.xml`); + destroyObjectXml.statusCode = 200; + soap.invoke.resolves(destroyObjectXml); + + expect(TYPE.DestroyObject).not.undefined; + return player.destroyObject('1') + .then(() => { + expect(soap.invoke).calledOnce; + expect(soap.invoke.firstCall.args).eql([ + 'http://192.168.1.151:1400/MediaServer/ContentDirectory/Control', + TYPE.DestroyObject, + { + id: '1' + } + ]); + }); + }); + it('addURIToQueue', () => { soap.parse.restore(); let addURIToQueueXml = fs.createReadStream(`${__dirname}/../../data/addURIToQueue.xml`); From 3c8bb531502c07ac3edceb6204edb85a0a71d14b Mon Sep 17 00:00:00 2001 From: laurentperez Date: Sun, 28 May 2017 11:55:33 +0200 Subject: [PATCH 06/12] enable playlist multitrack import --- lib/helpers/soap.js | 2 +- lib/models/Player.js | 9 +++++++- lib/prototypes/Player/importPlaylist.js | 5 ++--- test/unit/models/Player.js | 29 +++++++++++++++++-------- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/lib/helpers/soap.js b/lib/helpers/soap.js index 2dc6a804..ed7b0ef4 100644 --- a/lib/helpers/soap.js +++ b/lib/helpers/soap.js @@ -63,7 +63,7 @@ const TEMPLATES = Object.freeze({ [TYPE.BecomeCoordinatorOfStandaloneGroup]: '0', [TYPE.RefreshShareIndex]: '', [TYPE.AddURIToQueue]: '0{uri}{metadata}{desiredFirstTrackNumberEnqueued}{enqueueAsNext}', - [TYPE.AddURIToSavedQueue]: '0SQ:{sqid}{updateID}x-file-cifs://{uri}<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="S://{uri}" parentID="" restricted="true"><dc:title>{title}</dc:title><upnp:class>object.item.audioItem.musicTrack</upnp:class><desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">RINCON_AssociatedZPUDN</desc></item></DIDL-Lite>4294967295', + [TYPE.AddURIToSavedQueue]: '0SQ:{sqid}{updateID}x-file-cifs://{uri}<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="S://{uri}" parentID="" restricted="true"><dc:title>{title}</dc:title><upnp:class>{upnpClass}</upnp:class><desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">RINCON_AssociatedZPUDN</desc></item></DIDL-Lite>4294967295', [TYPE.CreateSavedQueue]: '0{title}', [TYPE.DestroyObject]: '{id}', [TYPE.AddMultipleURIsToQueue]: '00{amount}{uris}{metadatas}{containerURI}{containerMetadata}{desiredFirstTrackNumberEnqueued}{enqueueAsNext}', diff --git a/lib/models/Player.js b/lib/models/Player.js index 2937b9d6..f9561047 100644 --- a/lib/models/Player.js +++ b/lib/models/Player.js @@ -599,6 +599,12 @@ Player.prototype.destroyObject = function destroyObject(id) { Player.prototype.addURIToSavedQueue = function addURIToSavedQueue(sqid, uri, title) { // TODO do not prefix uris with x-file-cifs. use the raw export uri, but strip the cifs part for the DIDL cdata. console.log('xxxxxxxxxxxxxx addURIToSavedQueue:', sqid + ',' + uri + ',' + title); + let upnpClass = 'object.item.audioItem.musicTrack'; + if (uri.indexOf('savedqueues.rsq') !== -1) { + upnpClass = 'object.container.playlistContainer'; + } + + let shortURI = ''; // remove x-file-cifs:// return this.browse('SQ:' + sqid, 0, 100).then((res) => { console.log('xxxxxxxxxxxxxx addURIToSavedQueue res', res); let updateID = res.updateID; @@ -609,7 +615,8 @@ Player.prototype.addURIToSavedQueue = function addURIToSavedQueue(sqid, uri, tit sqid, uri, title, - updateID + updateID, + upnpClass }); } ).then(soap.parse); diff --git a/lib/prototypes/Player/importPlaylist.js b/lib/prototypes/Player/importPlaylist.js index a23cdc2a..713265bf 100644 --- a/lib/prototypes/Player/importPlaylist.js +++ b/lib/prototypes/Player/importPlaylist.js @@ -1,11 +1,10 @@ 'use strict'; const logger = require('../../helpers/logger'); -const util = require('util'); function importPlaylist(sqid, uri, title) { console.log('importPlaylist:' + sqid + ',' + uri + ',' + title); - if (!sqid || !uri) { - throw new Error('No playlist id or uri provided'); + if (!sqid || !uri || !title) { + throw new Error('No playlist id or title provided or no URI provided'); }; return this.addURIToSavedQueue(sqid, uri, title) diff --git a/test/unit/models/Player.js b/test/unit/models/Player.js index b6992502..c64bd9a0 100644 --- a/test/unit/models/Player.js +++ b/test/unit/models/Player.js @@ -4,6 +4,7 @@ const sinon = require('sinon'); const proxyquire = require('proxyquire'); const fs = require('fs'); const path = require('path'); +const util = require('util'); require('chai').use(require('sinon-chai')); require('sinon-as-promised'); @@ -668,14 +669,19 @@ describe('Player', () => { }); }); - describe('addURIToSavedQueue browse', () => { + describe('addURIToSavedQueue', () => { beforeEach('We need parse functionality here', () => { soap.parse.restore(); }); - describe('addURIToSavedQueue2', () => { - let playlist; + describe('Setup playlist', () => { + let playlist = {}; + + // numtracksadded + playlist.updateID = 1; + + /* beforeEach(() => { let queueStream = fs.createReadStream(path.join(__dirname, '../../data/playlists.xml')); @@ -686,18 +692,20 @@ describe('Player', () => { playlist = res; }); }); + */ - it('addURIToSavedQueue should browse first', () => { - console.log('yyyyyyyyyyy1', playlist); + it('Should have invoked browse to find the playlist to add URI to', () => { let addURIToSavedQueueXml = fs.createReadStream(`${__dirname}/../../data/addURIToSavedQueue.xml`); addURIToSavedQueueXml.statusCode = 200; soap.invoke.resolves(addURIToSavedQueueXml); expect(TYPE.AddURIToSavedQueue).not.undefined; + return player.addURIToSavedQueue('1', 'myuri', 'mytitle') .then((result) => { - expect(soap.invoke).calledThrice; - expect(soap.invoke.secondCall.args).eql([ + // updatedID in res + expect(soap.invoke).calledTwice; + expect(soap.invoke.firstCall.args).eql([ 'http://192.168.1.151:1400/MediaServer/ContentDirectory/Control', TYPE.Browse, { @@ -706,14 +714,17 @@ describe('Player', () => { startIndex: 0 } ]); - expect(soap.invoke.thirdCall.args).eql([ + + // todo hostname/uri no cifs multitrackadd + expect(soap.invoke.secondCall.args).eql([ 'http://192.168.1.151:1400/MediaRenderer/AVTransport/Control', TYPE.AddURIToSavedQueue, { sqid: '1', uri: 'myuri', title: 'mytitle', - updateID: playlist.updateID + updateID: playlist.updateID, + upnpClass: 'object.item.audioItem.musicTrack' } ]); }); From b88fbc053d76bab810efa4002360fdc1f0ea69bc Mon Sep 17 00:00:00 2001 From: laurentperez Date: Sun, 28 May 2017 12:35:17 +0200 Subject: [PATCH 07/12] fix uri import --- lib/helpers/soap.js | 2 +- lib/models/Player.js | 5 +++-- test/unit/models/Player.js | 9 ++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/helpers/soap.js b/lib/helpers/soap.js index ed7b0ef4..371a6d32 100644 --- a/lib/helpers/soap.js +++ b/lib/helpers/soap.js @@ -63,7 +63,7 @@ const TEMPLATES = Object.freeze({ [TYPE.BecomeCoordinatorOfStandaloneGroup]: '0', [TYPE.RefreshShareIndex]: '', [TYPE.AddURIToQueue]: '0{uri}{metadata}{desiredFirstTrackNumberEnqueued}{enqueueAsNext}', - [TYPE.AddURIToSavedQueue]: '0SQ:{sqid}{updateID}x-file-cifs://{uri}<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="S://{uri}" parentID="" restricted="true"><dc:title>{title}</dc:title><upnp:class>{upnpClass}</upnp:class><desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">RINCON_AssociatedZPUDN</desc></item></DIDL-Lite>4294967295', + [TYPE.AddURIToSavedQueue]: '0SQ:{sqid}{updateID}{uri}<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="{itemId}" parentID="" restricted="true"><dc:title>{title}</dc:title><upnp:class>{upnpClass}</upnp:class><desc id="cdudn" nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/">RINCON_AssociatedZPUDN</desc></item></DIDL-Lite>4294967295', [TYPE.CreateSavedQueue]: '0{title}', [TYPE.DestroyObject]: '{id}', [TYPE.AddMultipleURIsToQueue]: '00{amount}{uris}{metadatas}{containerURI}{containerMetadata}{desiredFirstTrackNumberEnqueued}{enqueueAsNext}', diff --git a/lib/models/Player.js b/lib/models/Player.js index f9561047..dd692586 100644 --- a/lib/models/Player.js +++ b/lib/models/Player.js @@ -597,14 +597,14 @@ Player.prototype.destroyObject = function destroyObject(id) { }; Player.prototype.addURIToSavedQueue = function addURIToSavedQueue(sqid, uri, title) { - // TODO do not prefix uris with x-file-cifs. use the raw export uri, but strip the cifs part for the DIDL cdata. console.log('xxxxxxxxxxxxxx addURIToSavedQueue:', sqid + ',' + uri + ',' + title); let upnpClass = 'object.item.audioItem.musicTrack'; if (uri.indexOf('savedqueues.rsq') !== -1) { upnpClass = 'object.container.playlistContainer'; } - let shortURI = ''; // remove x-file-cifs:// + // todo test multi share + let itemId = 'S://' + uri.replace(/(.*:\/\/)/, ''); // remove protocols like x-file-cifs:// return this.browse('SQ:' + sqid, 0, 100).then((res) => { console.log('xxxxxxxxxxxxxx addURIToSavedQueue res', res); let updateID = res.updateID; @@ -614,6 +614,7 @@ Player.prototype.addURIToSavedQueue = function addURIToSavedQueue(sqid, uri, tit { sqid, uri, + itemId, title, updateID, upnpClass diff --git a/test/unit/models/Player.js b/test/unit/models/Player.js index c64bd9a0..33a24089 100644 --- a/test/unit/models/Player.js +++ b/test/unit/models/Player.js @@ -701,7 +701,7 @@ describe('Player', () => { expect(TYPE.AddURIToSavedQueue).not.undefined; - return player.addURIToSavedQueue('1', 'myuri', 'mytitle') + return player.addURIToSavedQueue('1', 'x-file-cifs://MacBook-Air-de-laurent-2/Musique/iTunes/iTunes%20Media/Music/The%20xx/I%20See%20You/06%20Replica.mp3', 'track title') .then((result) => { // updatedID in res expect(soap.invoke).calledTwice; @@ -714,15 +714,14 @@ describe('Player', () => { startIndex: 0 } ]); - - // todo hostname/uri no cifs multitrackadd expect(soap.invoke.secondCall.args).eql([ 'http://192.168.1.151:1400/MediaRenderer/AVTransport/Control', TYPE.AddURIToSavedQueue, { sqid: '1', - uri: 'myuri', - title: 'mytitle', + uri: 'x-file-cifs://MacBook-Air-de-laurent-2/Musique/iTunes/iTunes%20Media/Music/The%20xx/I%20See%20You/06%20Replica.mp3', + itemId: 'S://MacBook-Air-de-laurent-2/Musique/iTunes/iTunes%20Media/Music/The%20xx/I%20See%20You/06%20Replica.mp3', + title: 'track title', updateID: playlist.updateID, upnpClass: 'object.item.audioItem.musicTrack' } From 79508a22a2c9166849cdce68f5d906740a68eaac Mon Sep 17 00:00:00 2001 From: laurentperez Date: Sun, 11 Jun 2017 15:38:49 +0200 Subject: [PATCH 08/12] playlist crud. test fails, don't know to to stub browse response --- lib/models/Player.js | 101 +++++++++++++++++++----- lib/prototypes/Player/createPlaylist.js | 1 + lib/prototypes/Player/deletePlaylist.js | 12 +-- lib/prototypes/Player/exportPlaylist.js | 37 ++++++++- lib/prototypes/Player/importPlaylist.js | 4 +- lib/services/spotify.js | 1 - test/unit/models/Player.js | 97 ++++++++--------------- 7 files changed, 158 insertions(+), 95 deletions(-) diff --git a/lib/models/Player.js b/lib/models/Player.js index dd692586..d471f36f 100644 --- a/lib/models/Player.js +++ b/lib/models/Player.js @@ -596,31 +596,88 @@ Player.prototype.destroyObject = function destroyObject(id) { }).then(soap.parse); }; -Player.prototype.addURIToSavedQueue = function addURIToSavedQueue(sqid, uri, title) { - console.log('xxxxxxxxxxxxxx addURIToSavedQueue:', sqid + ',' + uri + ',' + title); +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(/(.*:\/\/)/, ''); + } } - // todo test multi share - let itemId = 'S://' + uri.replace(/(.*:\/\/)/, ''); // remove protocols like x-file-cifs:// - return this.browse('SQ:' + sqid, 0, 100).then((res) => { - console.log('xxxxxxxxxxxxxx addURIToSavedQueue res', res); - let updateID = res.updateID; - return soap.invoke( - `${this.baseUrl}/MediaRenderer/AVTransport/Control`, - TYPE.AddURIToSavedQueue, - { - sqid, - uri, - itemId, - title, - updateID, - upnpClass - }); + 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; + } } - ).then(soap.parse); + + 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) { @@ -785,7 +842,6 @@ Player.prototype.browse = function browse(objectId, startIndex, limit) { startIndex, items: [] }; - console.log('xxxxxxxxxxxxx1', returnResult); returnResult.numberReturned = parseInt(res.numberreturned, 10); returnResult.totalMatches = parseInt(res.totalmatches, 10); returnResult.updateID = parseInt(res.updateid, 10); @@ -839,6 +895,8 @@ Player.prototype.browseAll = function browseAll(objectId) { let getChunk = (chunk) => { if (!chunk || !Array.isArray(chunk.items) || isNaN(chunk.totalMatches)) { // something went wrong. prevent infinite loop + console.log('xxxx objectId', objectId); + console.log('xxxx chunk', chunk); return Promise.reject(new Error('browse() returned an invalid payload')); } @@ -851,6 +909,9 @@ Player.prototype.browseAll = function browseAll(objectId) { } // Recursive promise chain + console.log('rxxxx objectId', objectId); + console.log('rxxxx chunk', chunk); + return this.browse(objectId, chunk.startIndex + chunk.numberReturned, 0) .then(getChunk); }; diff --git a/lib/prototypes/Player/createPlaylist.js b/lib/prototypes/Player/createPlaylist.js index 42432545..93078434 100644 --- a/lib/prototypes/Player/createPlaylist.js +++ b/lib/prototypes/Player/createPlaylist.js @@ -12,6 +12,7 @@ function createPlaylist(title) { .then((res) => { return { result: 'success', + title: title, sqid: res.assignedobjectid.replace(/SQ:/, '') }; }); diff --git a/lib/prototypes/Player/deletePlaylist.js b/lib/prototypes/Player/deletePlaylist.js index 4077646a..8f828611 100644 --- a/lib/prototypes/Player/deletePlaylist.js +++ b/lib/prototypes/Player/deletePlaylist.js @@ -1,20 +1,20 @@ 'use strict'; const logger = require('../../helpers/logger'); -function deletePlaylist(sqid) { - logger.debug(`deleting playlist with id ${sqid}`); +function deletePlaylist(name) { + logger.debug(`deleting playlist with name ${name}`); - if (!sqid) { - throw new Error('No playlist id provided'); + if (!name) { + throw new Error('No playlist name provided'); } - return this.destroyObject('SQ:' + sqid) + return this.destroyByTitle(name) .then((res) => { return { result: 'success' }; }); + } module.exports = deletePlaylist; - diff --git a/lib/prototypes/Player/exportPlaylist.js b/lib/prototypes/Player/exportPlaylist.js index 2b5fddb6..10660a0a 100644 --- a/lib/prototypes/Player/exportPlaylist.js +++ b/lib/prototypes/Player/exportPlaylist.js @@ -1,9 +1,40 @@ 'use strict'; const logger = require('../../helpers/logger'); -function exportPlaylist(sqid) { - logger.debug(`exporting playlist id ${sqid}`); - return this.system.getPlaylists(sqid) +function exportPlaylist(id) { + logger.debug(`exporting playlist using id ${id}`); + if (id) { + return this.system.getPlaylists() + .then((playlists) => { + let match; + playlists.map(playlist => { + if (playlist.title.toLowerCase() === id.toLowerCase()) { + match = playlist; + } + }); + 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; }); diff --git a/lib/prototypes/Player/importPlaylist.js b/lib/prototypes/Player/importPlaylist.js index 713265bf..dc2626f7 100644 --- a/lib/prototypes/Player/importPlaylist.js +++ b/lib/prototypes/Player/importPlaylist.js @@ -2,15 +2,15 @@ const logger = require('../../helpers/logger'); function importPlaylist(sqid, uri, title) { - console.log('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) => { - console.log('importPlaylist res:', res); return { result: 'success', import: res }; + }).catch((err) => { + throw new Error(err); }); } diff --git a/lib/services/spotify.js b/lib/services/spotify.js index 48488189..1181a25f 100644 --- a/lib/services/spotify.js +++ b/lib/services/spotify.js @@ -23,7 +23,6 @@ function parseUri(uri) { function tryGetHighResArt(uri) { let trackInfo = parseUri(uri); - let apiUrl = [apiBaseEndpoint, endpoints.track, trackInfo.id].join(''); return request({ diff --git a/test/unit/models/Player.js b/test/unit/models/Player.js index 33a24089..f75635b3 100644 --- a/test/unit/models/Player.js +++ b/test/unit/models/Player.js @@ -669,69 +669,6 @@ describe('Player', () => { }); }); - describe('addURIToSavedQueue', () => { - - beforeEach('We need parse functionality here', () => { - soap.parse.restore(); - }); - - describe('Setup playlist', () => { - let playlist = {}; - - // numtracksadded - playlist.updateID = 1; - - /* - beforeEach(() => { - let queueStream = fs.createReadStream(path.join(__dirname, '../../data/playlists.xml')); - - soap.invoke.resolves(queueStream); - - return player.browse() - .then((res) => { - playlist = res; - }); - }); - */ - - it('Should have invoked browse to find the playlist to add URI to', () => { - let addURIToSavedQueueXml = fs.createReadStream(`${__dirname}/../../data/addURIToSavedQueue.xml`); - addURIToSavedQueueXml.statusCode = 200; - soap.invoke.resolves(addURIToSavedQueueXml); - - expect(TYPE.AddURIToSavedQueue).not.undefined; - - return player.addURIToSavedQueue('1', 'x-file-cifs://MacBook-Air-de-laurent-2/Musique/iTunes/iTunes%20Media/Music/The%20xx/I%20See%20You/06%20Replica.mp3', 'track title') - .then((result) => { - // updatedID in res - expect(soap.invoke).calledTwice; - expect(soap.invoke.firstCall.args).eql([ - 'http://192.168.1.151:1400/MediaServer/ContentDirectory/Control', - TYPE.Browse, - { - limit: 100, - objectId: 'SQ:1', - startIndex: 0 - } - ]); - expect(soap.invoke.secondCall.args).eql([ - 'http://192.168.1.151:1400/MediaRenderer/AVTransport/Control', - TYPE.AddURIToSavedQueue, - { - sqid: '1', - uri: 'x-file-cifs://MacBook-Air-de-laurent-2/Musique/iTunes/iTunes%20Media/Music/The%20xx/I%20See%20You/06%20Replica.mp3', - itemId: 'S://MacBook-Air-de-laurent-2/Musique/iTunes/iTunes%20Media/Music/The%20xx/I%20See%20You/06%20Replica.mp3', - title: 'track title', - updateID: playlist.updateID, - upnpClass: 'object.item.audioItem.musicTrack' - } - ]); - }); - }); - - }); - }); - it('createSavedQueue', () => { soap.parse.restore(); let createSavedQueueXml = fs.createReadStream(`${__dirname}/../../data/createSavedQueue.xml`); @@ -778,6 +715,40 @@ describe('Player', () => { }); }); + it('Should have invoked browse to find the playlist to add URI to', () => { + let addURIToSavedQueueXml = fs.createReadStream(`${__dirname}/../../data/addURIToSavedQueue.xml`); + addURIToSavedQueueXml.statusCode = 200; + soap.invoke.resolves(addURIToSavedQueueXml); + + expect(TYPE.AddURIToSavedQueue).not.undefined; + + return player.addURIToSavedQueue('1', 'x-file-cifs://MacBook-Air-de-laurent-2/Musique/iTunes/iTunes%20Media/Music/The%20xx/I%20See%20You/06%20Replica.mp3', 'track title') + .then((result) => { + expect(soap.invoke).calledThrice; + expect(soap.invoke.secondCall.args).eql([ + 'http://192.168.1.151:1400/MediaServer/ContentDirectory/Control', + TYPE.Browse, + { + limit: 100, + objectId: 'SQ:1', + startIndex: 0 + } + ]); + expect(soap.invoke.thirdCall.args).eql([ + 'http://192.168.1.151:1400/MediaRenderer/AVTransport/Control', + TYPE.AddURIToSavedQueue, + { + sqid: '1', + uri: 'x-file-cifs://MacBook-Air-de-laurent-2/Musique/iTunes/iTunes%20Media/Music/The%20xx/I%20See%20You/06%20Replica.mp3', + itemId: 'S://MacBook-Air-de-laurent-2/Musique/iTunes/iTunes%20Media/Music/The%20xx/I%20See%20You/06%20Replica.mp3', + title: 'track title', + updateID: NaN, /* I did not find a way to stub the updateID from browser */ + upnpClass: 'object.item.audioItem.musicTrack' + } + ]); + }); + }); + it('addURIToQueue', () => { soap.parse.restore(); let addURIToQueueXml = fs.createReadStream(`${__dirname}/../../data/addURIToQueue.xml`); From 473909506ffac394307b28b3ed43353fd3682b48 Mon Sep 17 00:00:00 2001 From: laurentperez Date: Sun, 11 Jun 2017 18:25:28 +0200 Subject: [PATCH 09/12] remove console log --- lib/models/Player.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/models/Player.js b/lib/models/Player.js index d471f36f..39e9465e 100644 --- a/lib/models/Player.js +++ b/lib/models/Player.js @@ -895,8 +895,6 @@ Player.prototype.browseAll = function browseAll(objectId) { let getChunk = (chunk) => { if (!chunk || !Array.isArray(chunk.items) || isNaN(chunk.totalMatches)) { // something went wrong. prevent infinite loop - console.log('xxxx objectId', objectId); - console.log('xxxx chunk', chunk); return Promise.reject(new Error('browse() returned an invalid payload')); } @@ -909,8 +907,6 @@ Player.prototype.browseAll = function browseAll(objectId) { } // Recursive promise chain - console.log('rxxxx objectId', objectId); - console.log('rxxxx chunk', chunk); return this.browse(objectId, chunk.startIndex + chunk.numberReturned, 0) .then(getChunk); From 956c838abf86cb1aeafac3c7e5a06c63fbee1957 Mon Sep 17 00:00:00 2001 From: laurentperez Date: Sun, 11 Jun 2017 18:32:04 +0200 Subject: [PATCH 10/12] polish --- lib/prototypes/Player/exportPlaylist.js | 7 +++++-- test/unit/models/Player.js | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/prototypes/Player/exportPlaylist.js b/lib/prototypes/Player/exportPlaylist.js index 10660a0a..3ac25bcb 100644 --- a/lib/prototypes/Player/exportPlaylist.js +++ b/lib/prototypes/Player/exportPlaylist.js @@ -7,11 +7,14 @@ function exportPlaylist(id) { return this.system.getPlaylists() .then((playlists) => { let match; - playlists.map(playlist => { + 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) => { diff --git a/test/unit/models/Player.js b/test/unit/models/Player.js index f75635b3..2915b6c7 100644 --- a/test/unit/models/Player.js +++ b/test/unit/models/Player.js @@ -4,7 +4,6 @@ const sinon = require('sinon'); const proxyquire = require('proxyquire'); const fs = require('fs'); const path = require('path'); -const util = require('util'); require('chai').use(require('sinon-chai')); require('sinon-as-promised'); From a6cff03fafc5b8527b68288298a3aa81ebbe4c93 Mon Sep 17 00:00:00 2001 From: laurentperez Date: Sun, 11 Jun 2017 18:34:19 +0200 Subject: [PATCH 11/12] polish --- lib/services/spotify.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/services/spotify.js b/lib/services/spotify.js index 1181a25f..48488189 100644 --- a/lib/services/spotify.js +++ b/lib/services/spotify.js @@ -23,6 +23,7 @@ function parseUri(uri) { function tryGetHighResArt(uri) { let trackInfo = parseUri(uri); + let apiUrl = [apiBaseEndpoint, endpoints.track, trackInfo.id].join(''); return request({ From b1930c576ece816843c8dd98aa728e993522cedb Mon Sep 17 00:00:00 2001 From: laurentperez Date: Sun, 11 Jun 2017 18:36:24 +0200 Subject: [PATCH 12/12] polish --- lib/prototypes/Player/replaceWithPlaylist.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/prototypes/Player/replaceWithPlaylist.js b/lib/prototypes/Player/replaceWithPlaylist.js index fd8f9c1d..061f840b 100644 --- a/lib/prototypes/Player/replaceWithPlaylist.js +++ b/lib/prototypes/Player/replaceWithPlaylist.js @@ -29,4 +29,3 @@ function replaceWithPlaylist(playlistName) { } module.exports = replaceWithPlaylist; -