UNPKG

8.5 kBJavaScriptView Raw
1#!/usr/local/bin/node
2
3var util = require('util'),
4 cli = require('cli'),
5 sonos = require('sonos'),
6 Q = require('q'),
7 http = require('http'),
8 _ = require('underscore'),
9 xml2js = require('xml2js'),
10 readline = require('readline');
11
12
13var rl = readline.createInterface({
14 input: process.stdin,
15 output: process.stdout
16});
17
18var device;
19
20cli.parse({
21 play: ['p', 'Play whichever track is currently queued'],
22 pause: ['ps', 'Pause playback'],
23 search: ['s', 'Search an artist in Spotify\'s collection', 'string' ],
24 addandplay: ['ap', 'Add a track or an album by spotify URI and play it', 'string'],
25 mute: ['m', 'Mute'],
26 unmute: ['um', 'Unmute'],
27 browse: ['b', 'Browse the current list of enqueued tracks'],
28 next: ['n', 'Plays the next track in the queue'],
29 previous: ['pr', 'Plays the previous track in the queue'],
30 current: ['c', 'Shows the track currently playing']
31});
32
33sonos.Sonos.prototype.browse = function(){
34
35 var RENDERING_ENDPOINT = '/MediaServer/ContentDirectory/Control';
36 var action = '"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"';
37 var body = '<u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1"><ObjectID>Q:0</ObjectID><BrowseFlag>BrowseDirectChildren</BrowseFlag><Filter>dc:title,res,dc:creator,upnp:artist,upnp:album,upnp:albumArtURI</Filter><StartingIndex>0</StartingIndex><RequestedCount>100</RequestedCount><SortCriteria></SortCriteria></u:Browse>';
38
39 var defer = Q.defer();
40
41 this.request(RENDERING_ENDPOINT, action, body, 'u:BrowseResponse', function(err, data){
42
43 new xml2js.Parser().parseString(data[0].Result, function(err, didl) {
44
45 var items = [];
46
47 _.each(didl['DIDL-Lite'].item, function(item, index){
48 items.push({"title": item['dc:title'][0], "artist": item['dc:creator'][0], "index": index+1});
49 });
50
51 defer.resolve(items);
52 });
53 });
54
55 return defer.promise;
56};
57
58sonos.Sonos.prototype.enqueueSpotify = function(uri){
59
60 var encodedUri = encodeURIComponent(uri);
61 var isAlbum = uri.indexOf('spotify:album') > -1;
62
63 var audioClass = isAlbum ? 'object.container.album.musicAlbum' : 'object.item.audioItem.musicTrack';
64 var enqueuedURI = isAlbum ? 'x-rincon-cpcontainer:0004006c' + encodedUri : 'x-sonos-spotify:' + encodedUri;
65 var code = isAlbum ? '0004006c' : '00030000';
66
67 var RENDERING_ENDPOINT = '/MediaRenderer/AVTransport/Control';
68 var action = '"urn:schemas-upnp-org:service:AVTransport:1#AddURIToQueue"';
69 var body = '<u:AddURIToQueue xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"> \
70 <InstanceID>0</InstanceID> \
71 <EnqueuedURI>' + enqueuedURI + '</EnqueuedURI> \
72 <EnqueuedURIMetaData> \
73 &lt;DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" \
74 xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" \
75 xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"&gt;&lt;item id="' + code + encodedUri + '" \
76 restricted="true"&gt;&lt;dc:title&gt;America&lt;/dc:title&gt; \
77 &lt;upnp:class&gt;' + audioClass + '&lt;/upnp:class&gt;&lt;desc id="cdudn" \
78 nameSpace="urn:schemas-rinconnetworks-com:metadata-1-0/"&gt;SA_RINCON2311_X_#Svc2311-0-Token&lt;/desc&gt;&lt;/item&gt;&lt;/DIDL-Lite&gt; \
79 </EnqueuedURIMetaData> \
80 <DesiredFirstTrackNumberEnqueued>0</DesiredFirstTrackNumberEnqueued> \
81 <EnqueueAsNext>0</EnqueueAsNext> \
82 </u:AddURIToQueue>';
83
84 var defer = Q.defer();
85
86 this.request(RENDERING_ENDPOINT, action, body, 'u:AddURIToQueueResponse', function(err, data){
87 var newIndex = _.reduce(data[0].FirstTrackNumberEnqueued, function(it, num){
88 return parseInt(num);
89 }, 0);
90 defer.resolve(newIndex);
91 });
92
93 return defer.promise;
94}
95
96sonos.Sonos.prototype.seekTrackNr = function(nr){
97 var RENDERING_ENDPOINT = '/MediaRenderer/AVTransport/Control';
98 var action = '"urn:schemas-upnp-org:service:AVTransport:1#Seek"';
99 var body = '<s:Body><u:Seek xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><Unit>TRACK_NR</Unit><Target>' + nr + '</Target></u:Seek>';
100
101 var defer = Q.defer();
102
103 this.request(RENDERING_ENDPOINT, action, body, 'u:AddURIToQueueResponse', function(err, data){
104 defer.resolve();
105 });
106 return defer.promise;
107}
108
109
110cli.main(function(args, options){
111
112 var deferred = Q.defer();
113
114 sonos.search(function(device){
115 deferred.resolve(device);
116 });
117
118
119 deferred.promise.then(function(_device){
120
121 device = _device;
122
123 if(options.addandplay){
124 device.enqueueSpotify(options.addandplay).then(function(nr){
125 device.seekTrackNr(nr);
126 }).then(function(){
127 device.play(function(){
128 process.exit(0);
129 });
130 });
131 }
132
133 if(options.search){
134 //I am not to pleased with the nested promises to handle the flow, but it will have to do for now
135 getArtists(options.search).then(selectArtist);
136 }
137
138 if(options.play){
139 device.play(function(){
140 process.exit(0);
141 });
142 }
143
144 if(options.pause){
145 device.pause(function(){
146 process.exit(0);
147 })
148 }
149
150 if(options.mute){
151 device.setMuted(true, function(){
152 process.exit(0);
153 });
154 }
155
156 if(options.unmute){
157 device.setMuted(false, function(){
158 process.exit(0);
159 })
160 }
161
162 if(options.browse){
163 device.browse().then(showQueue);
164 }
165
166 if(options.next){
167 device.next(function(){
168 process.exit(0);
169 });
170 }
171
172 if(options.previous){
173 device.previous(function(){
174 process.exit(0);
175 });
176 }
177
178 if(options.current){
179 device.currentTrack(function(err, result){
180 console.log(result.artist + ' - ' + result.title);
181 process.exit(0);
182 });
183 }
184 });
185});
186
187function showQueue(browseResults){
188 console.log('');
189 _.each(browseResults, function(item, index){
190 console.log(index + '. ', item.artist + ' - ' + item.title);
191 });
192
193 rl.question('\nSelect a track for playback: ', function(answer){
194 var index = parseInt(answer);
195 device.seekTrackNr(browseResults[index].index).then(function(){
196 device.play(function(err, data){
197 process.exit(0);
198 });
199 })
200 })
201}
202
203function selectArtist(searchResults){
204 console.log('');
205 _.each(searchResults.artists, function(artist, i){
206 console.log(i + '. ' + artist.name);
207 });
208
209 rl.question("\nSelect an artist: ", function(answer){
210 var index = parseInt(answer);
211 var artist = searchResults.artists[index];
212
213 var result = getAlbumsForArtist(artist);
214 result.then(function(albums){
215 selectAlbum(albums.artist.albums);
216 });
217 });
218}
219
220function selectAlbum(albums){
221 console.log('');
222 _.each(albums, function(it, i){
223 console.log(i + '. ' + it.album.artist + ' - ' + it.album.name);
224 });
225
226 rl.question('\nSelect an album: ', function(answer){
227 var index = parseInt(answer);
228 var it = albums[index];
229 var promise = getTracksForAlbum(it.album);
230 promise.then(function(tracks){
231 selectTrack(tracks.album);
232 });
233 })
234}
235
236function playTrack(track){
237 device.enqueueSpotify(track).then(function(nr){
238 device.seekTrackNr(nr).then(function(){
239 device.play(function(err, data){
240 process.exit(0);
241 });
242 })
243 });
244}
245
246function selectTrack(album){
247 console.log('');
248 console.log('0. Play entire album');
249 _.each(album.tracks, function(track, i){
250 console.log(parseInt(i) + 1 + '. ' + album.artist + ' - ' + track.name);
251 });
252
253 rl.question("\nSelect a track: ", function(answer){
254 var index = parseInt(answer);
255 var track = index > 0 ? album.tracks[index].href : album.href;
256
257 playTrack(track);
258 });
259}
260
261function getTracksForAlbum(album){
262 var def = Q.defer();
263 var url = "http://ws.spotify.com/lookup/1/.json?extras=track&uri=" + album.href;
264
265 http.get(url, function(res){
266 handleResponse(res, def);
267 }).on('error', function(e) {
268 console.log("Got error: ", e);
269 });
270 return def.promise;
271}
272
273function getAlbumsForArtist(artist){
274 var def = Q.defer();
275 var url = "http://ws.spotify.com/lookup/1/.json?extras=album&uri=" + artist.href;
276
277 http.get(url, function(res){
278 handleResponse(res, def);
279 }).on('error', function(e) {
280 console.log("Got error: ", e);
281 });
282 return def.promise;
283}
284
285
286function handleResponse(res, promise){
287 var body = '';
288
289 res.on('data', function(chunk) {
290 body += chunk;
291 });
292
293 res.on('end', function() {
294 var resp = JSON.parse(body)
295 promise.resolve(resp);
296 });
297}
298
299function getArtists(query){
300 var def = Q.defer();
301
302 var url = "http://ws.spotify.com/search/1/artist.json?q=" + query;
303 http.get(url, function(res) {
304 handleResponse(res, def);
305 }).on('error', function(e) {
306 console.log("Got error: ", e);
307 });
308
309 return def.promise;
310}