diff --git a/README.md b/README.md index 3e8674f..5c6caeb 100644 --- a/README.md +++ b/README.md @@ -59,4 +59,4 @@ See the `example` directory for some more example code. API --- -TODO: document! +> Work in progress, see `./lib/spotify.js` for full API reference diff --git a/lib/schemas.js b/lib/schemas.js index 9191dfa..1f66afc 100644 --- a/lib/schemas.js +++ b/lib/schemas.js @@ -57,7 +57,7 @@ var loadPackage = function(id) { return new protobuf.Schema(fs.readFileSync(path.resolve(protoPath, schema + '.desc'))); }); return packageCache[id]; - + } else { // protobufjs // Generate a proto string with import statements var proto = mapping.map(function(schema) { @@ -73,13 +73,13 @@ var loadPackage = function(id) { var loadMessage = module.exports.build = function(packageId, messageId) { debug('loadMessage(%j, %j) [%s]', packageId, messageId, library); - + var packageObj = loadPackage(packageId); var messageObj = null; - + if (protobuf) { var identifier = "spotify." + packageId + ".proto." + messageId; - + // Loop though each loaded schema looking for the message for (var i = 0; i < packageObj.length; i++) { messageObj = packageObj[i][identifier]; diff --git a/lib/spotify.js b/lib/spotify.js index f5f0c7d..bfea08b 100644 --- a/lib/spotify.js +++ b/lib/spotify.js @@ -28,6 +28,7 @@ module.exports = Spotify; var MercuryMultiGetRequest = schemas.build('mercury','MercuryMultiGetRequest'); var MercuryMultiGetReply = schemas.build('mercury','MercuryMultiGetReply'); var MercuryRequest = schemas.build('mercury','MercuryRequest'); +var MercuryReply = schemas.build('mercury','MercuryReply'); var Artist = require('./artist'); var Album = require('./album'); @@ -36,6 +37,8 @@ var Image = require('./image'); require('./restriction'); var SelectedListContent = schemas.build('playlist4','SelectedListContent'); +var Op = schemas.build('playlist4', 'Op'); +var CreateListReply = schemas.build('playlist4', 'CreateListReply'); var StoryRequest = schemas.build('bartender','StoryRequest'); var StoryList = schemas.build('bartender','StoryList'); @@ -782,6 +785,135 @@ Spotify.prototype.playlist = function (uri, from, length, fn) { }, fn); }; +/** + * Add track to a playlist. + * + * @param {String} uri playlist uri + * @param {String} uri track uri. + * @param {Function} fn callback function + * @api public + */ + +Spotify.prototype.addTrack = function (playlisturi, trackuri, fn) { + var self = this; + var playlist = playlisturi.split(':'); + var user = playlist[2]; + var playlistid = (playlist[3] === 'starred') ? 'starred' : "playlist/" + playlist[4]; + + var hm = 'hm://playlist/user/' + user + '/' + playlistid + '?syncpublished=1'; + var request = MercuryRequest.serialize({ body: 'ADD', uri: hm }).toString('base64'); + var data = new Buffer(trackuri).toString('base64'); + + var args = [ 0, request, data ]; + + this.sendCommand('sp/hm_b64', args, function (err, res) { + if (err) return fn(err); + + var data = res.result; + if (data.length >= 1) { + // success! + fn(err, trackuri); + } else { + // TODO: real error handling + var header = MercuryReply.parse(new Buffer(res.result[0], 'base64')); + fn(err, null); + } + + }); +}; + +/** + * Add a playlist to rootlist + * + * @param {String} username + * @param {String} uri playlist uri. + * @param {Function} fn callback function + * @api public + */ + +Spotify.prototype.addRootList = function (user, playlisturi, fn) { + + var self = this; + var hm = 'hm://playlist/user/' + user + "/rootlist?add_first=1&syncpublished=1"; + var request = MercuryRequest.serialize({ body: 'ADD', uri: hm }).toString('base64'); + var data = new Buffer(playlisturi).toString('base64'); + + var args = [ 0, request, data ]; + + this.sendCommand('sp/hm_b64', args, function (err, res) { + if (err) return fn(err); + + var data = res.result; + + if (data.length >= 1) { + // success! + var obj = self._parse(CreateListReply, new Buffer(res.result[0], 'base64')); + fn(err, playlisturi); + } else { + // TODO: real error handling + var header = self._parse(MercuryReply, new Buffer(res.result[0], 'base64')); + fn(err, null); + } + + }); +}; + +// TODO: normalize as similar to + +/** + * Create a playlist and add it to rootlist + * + * @param {String} username + * @param {String} playlist name + * @param {Function} fn callback function + * @api public + */ +Spotify.prototype.createPlaylist = function (user, name, fn) { + + var self = this; + var hm = 'hm://playlist/user/' + user; + + var request = MercuryRequest.serialize({ + body: 'PUT', + uri: hm + }).toString('base64'); + + // kind values can be found in playlist4ops schema + + var data = Op.serialize({ + kind: 6, + updateListAttributes: { + newAttributes: { + values: { + name: name + } + } + } + }); + + // need to serialize everything, including data, with mercuryrequest + data = MercuryRequest.serialize({ uri : data }).toString('base64'); + + var args = [ 0, request, data ]; + + this.sendCommand('sp/hm_b64', args, function (err, res) { + if (err) return fn(err); + var obj; + var data = res.result; + if (data.length >= 1) { + // success! + obj = self._parse(CreateListReply, new Buffer(res.result[0], 'base64')); + self.addRootList(user, obj.uri.toString(), fn); + } else { + // TODO: real error handling + var header = self._parse(MercuryReply, new Buffer(res.result[0], 'base64')); + fn(err, null); + } + + }); + +}; + /** * Gets a user's starred playlist * @@ -823,11 +955,12 @@ Spotify.prototype.starred = function (user, from, length, fn) { * @param {String} user (optional) the username for the rootlist you want to retrieve. defaults to current user. * @param {Number} from (optional) the start index. defaults to 0. * @param {Number} length (optional) number of tracks to get. defaults to 100. + * @param {Boolean} published (optional) retrieve published playlists only * @param {Function} fn callback function * @api public */ -Spotify.prototype.rootlist = function (user, from, length, fn) { +Spotify.prototype.rootlist = function (user, from, length, published, fn) { // argument surgery if ('function' == typeof user) { fn = user; @@ -838,15 +971,21 @@ Spotify.prototype.rootlist = function (user, from, length, fn) { } else if ('function' == typeof length) { fn = length; length = null; + } else if ('function' == typeof published) { + fn = published + published = null } if (null == user) user = this.username; if (null == from) from = 0; if (null == length) length = 100; + if (null == published) published = false + + var rootListType = !published ? 'rootlist' : 'publishedrootlist' debug('rootlist(%j, %j, %j)', user, from, length); var self = this; - var hm = 'hm://playlist/user/' + user + '/publishedrootlist?from=' + from + '&length=' + length; + var hm = 'hm://playlist/user/' + user + '/' + rootListType + '?from=' + from + '&length=' + length; this.sendProtobufRequest({ header: { diff --git a/proto/mercury.desc b/proto/mercury.desc index b22f46d..d669819 100644 --- a/proto/mercury.desc +++ b/proto/mercury.desc @@ -1,6 +1,6 @@ -Í - mercury.protospotify.mercury.proto"P +œ +proto/mercury.protospotify.mercury.proto"P MercuryMultiGetRequest6 request ( 2%.spotify.mercury.proto.MercuryRequest"J MercuryMultiGetReply2 @@ -11,7 +11,7 @@ method (  status_code ( source ( 5 - user_fields ( 2 .spotify.mercury.proto.UserField"º + user_fields ( 2 .spotify.mercury.proto.UserField"ƒ MercuryReply status_code ( status_message ( E @@ -19,8 +19,7 @@ ttl ( etag (  content_type (  -body ( 5 - user_fields ( 2 .spotify.mercury.proto.UserField"@ +body ( "@ CachePolicy CACHE_NO CACHE_PRIVATE diff --git a/proto/mercury.proto b/proto/mercury.proto index 9e20ffa..ba1728d 100644 --- a/proto/mercury.proto +++ b/proto/mercury.proto @@ -9,6 +9,9 @@ message MercuryMultiGetReply { message MercuryRequest { optional string uri = 1; optional string content_type = 2; + // per latest .xml? + //optional bytes body = 3; + //optional bytes etag = 4; optional string method = 3; optional sint32 status_code = 4; optional string source = 5;