diff --git a/binding.gyp b/binding.gyp index f6caa2e..11e1dbf 100644 --- a/binding.gyp +++ b/binding.gyp @@ -4,15 +4,18 @@ "target_name": "libspotify", "sources": [ "src/album.cc", + "src/albumbrowse.cc", "src/artist.cc", + "src/artistbrowse.cc", "src/audio.cc", "src/binding.cc", + "src/image.cc", "src/link.cc", "src/player.cc", "src/search.cc", "src/session.cc", "src/track.cc", - "src/playlist.cc" + "src/playlist.cc" ], "cflags": ["-Wall"], "conditions" : [ diff --git a/lib/Album.js b/lib/Album.js index 29834ee..100bdce 100644 --- a/lib/Album.js +++ b/lib/Album.js @@ -51,6 +51,10 @@ Album.prototype.toString = function toString() { * libspotify finished, onImageLoaded callback gets executed. */ Album.prototype.coverImage = function coverImage(imageSize, cb) { + var sp_image, image; + var deprecated = function () { + console.log('`cb` parameter to `coverImage` is deprecated, please use the `Image` class instead'); + }; if (typeof(imageSize) == 'function') { cb = imageSize; @@ -66,7 +70,12 @@ Album.prototype.coverImage = function coverImage(imageSize, cb) { } } - var wrap = function (buffer) { + if (typeof(imageSize) == 'undefined') { + imageSize = this.IMAGE_SIZE_NORMAL; + } + + var wrap = function () { + var buffer = image.getData(); if (buffer.length == 0) { cb(new Error('Cover image is empty')); } else { @@ -74,13 +83,57 @@ Album.prototype.coverImage = function coverImage(imageSize, cb) { } }; - if (b.album_cover(this.getSession()._sp_session, this._sp_object, imageSize, wrap) === false) { - process.nextTick(function () { cb(new Error('Album has no cover image')); }); + sp_image = b.album_cover(this.getSession()._sp_session, this._sp_object, imageSize); + + if(sp_image === null) { + if(typeof(cb) == 'function') { + deprecated(); + process.nextTick(function () { cb(new Error('Album has no cover image')); }); + } + return null; + } else { + image = new sp.Image(sp_image); + if(typeof(cb) == 'function') { + deprecated(); + image.whenReady(wrap); + } + return image; } }; -Album.prototype.smallCoverImage = function (cb) { this.coverImage(this.IMAGE_SIZE_SMALL, cb); } -Album.prototype.normalCoverImage = function (cb) { this.coverImage(this.IMAGE_SIZE_NORMAL, cb); } -Album.prototype.largeCoverImage = function (cb) { this.coverImage(this.IMAGE_SIZE_LARGE, cb); } +Album.prototype.smallCoverImage = function (cb) { return this.coverImage(this.IMAGE_SIZE_SMALL, cb); } +Album.prototype.normalCoverImage = function (cb) { return this.coverImage(this.IMAGE_SIZE_NORMAL, cb); } +Album.prototype.largeCoverImage = function (cb) { return this.coverImage(this.IMAGE_SIZE_LARGE, cb); } + +Album.prototype.coverImageUrl = function coverImageUrl(imageSize) { + + if (typeof(imageSize) == 'string') { + switch(imageSize) { + case 'small': imageSize = this.IMAGE_SIZE_SMALL; break; + case 'normal': imageSize = this.IMAGE_SIZE_NORMAL; break; + case 'large': imageSize = this.IMAGE_SIZE_LARGE; break; + default: throw new Error('Unknown image size'); + } + } + + if (typeof(imageSize) == 'undefined') { + imageSize = this.IMAGE_SIZE_NORMAL; + } + + return b.link_create_from_album_cover(this._sp_object, imageSize); +} + +Album.prototype.getTracks = function (cb) { + var browser = b.albumbrowse_create(this.getSession()._sp_session, this._sp_object, function () { + tracks = new Array(b.albumbrowse_num_tracks(browser)); + + for(var i = 0; i= array.length) { - callback(array); + callback(array); + } +}; + +/** + * Get the image for the playlist + */ +Playlist.prototype.getImage = function getImage() { + this._readyOrThrow(); + + var sp_image = b.playlist_get_image(this.getSession()._sp_session, this._sp_object); + + if(sp_image === null) { + return null; + } else { + return new sp.Image(sp_image); } }; @@ -82,32 +89,29 @@ Playlist.prototype._setupNativeCallbacks = function _setupNativeCallbacks() { var self = this; var checkReady = function() { - if (self.isReady() && !self.__readyEventFired === true) { - self.__readyEventFired = true; - self._populateAttributes(); - self.emit('ready'); - // We want the number of subscribers for the - // playlist so tell spotify to get it for us (this - // is an async call so is dealt with in the - // callbacks) - b.playlist_update_subscribers(sp.Session.currentSession._sp_session, self._sp_object); - } + if (self.isReady() && !self.__readyEventFired === true) { + self.__readyEventFired = true; + self._populateAttributes(); + self.emit('ready'); + // We want the number of subscribers for the + // playlist so tell spotify to get it for us (this + // is an async call so is dealt with in the + // callbacks) + b.playlist_update_subscribers(sp.Session.currentSession._sp_session, self._sp_object); + } }; this._sp_object.state_changed = function() { - // Check the possible reasons that state changed has been triggered - - // 1. Collaboration turned on / off - - // 2. Pending Changes started / complete - - // 3. Playlist started loading / finished loading - checkReady(); + // Check the possible reasons that state changed has been triggered + // 1. Collaboration turned on / off + // 2. Pending Changes started / complete + // 3. Playlist started loading / finished loading + checkReady(); }; var t = 0; this._sp_object.subscribers_changed = function() { - self.numSubscribers = b.playlist_num_subscribers(self._sp_object); + self.numSubscribers = b.playlist_num_subscribers(self._sp_object); }; checkReady(); diff --git a/lib/PlaylistContainer.js b/lib/PlaylistContainer.js index 91cf0c0..12802de 100644 --- a/lib/PlaylistContainer.js +++ b/lib/PlaylistContainer.js @@ -21,6 +21,11 @@ PlaylistContainer.prototype.__defineGetter__('_object_type', playlistcontainer_o module.exports = PlaylistContainer; +PlaylistContainer.prototype.PLAYLIST_TYPE_PLAYLIST = b.SP_PLAYLIST_TYPE_PLAYLIST; +PlaylistContainer.prototype.PLAYLIST_TYPE_START_FOLDER = b.SP_PLAYLIST_TYPE_START_FOLDER; +PlaylistContainer.prototype.PLAYLIST_TYPE_END_FOLDER = b.SP_PLAYLIST_TYPE_END_FOLDER; +PlaylistContainer.prototype.PLAYLIST_TYPE_PLACEHOLDER = b.SP_PLAYLIST_TYPE_PLACEHOLDER; + PlaylistContainer.prototype._populateAttributes = function _populateAttributes() { return this.constructor.super_.prototype._populateAttributes(); }; @@ -33,6 +38,37 @@ PlaylistContainer.prototype.getNumPlaylists = function getNumPlaylists() { return b.playlistcontainer_num_playlists(this._sp_object); }; +/** + * gets the type of the playlist at index in the playlist container + */ +PlaylistContainer.prototype.getPlaylistType = function getPlaylistType(idx) { + this._readyOrThrow(); + return b.playlistcontainer_playlist_type(this._sp_object, idx); +}; + +/** + * gets the id of the playlist folder at index in the playlist container + */ +PlaylistContainer.prototype.getPlaylistFolderID = function getPlaylistFolderID(idx) { + this._readyOrThrow(); + return b.playlistcontainer_playlist_folder_id(this._sp_object, idx); +}; + +/** + * gets the name of the playlist folder at index in the playlist container + */ +PlaylistContainer.prototype.getPlaylistFolderName = function getPlaylistFolderName(idx) { + this._readyOrThrow(); + return b.playlistcontainer_playlist_folder_name(this._sp_object, idx); +}; + +/** + * gets a playlist from the specified index + */ +PlaylistContainer.prototype.getPlaylistAtIndex = function getPlaylistAtIndex(idx) { + return new sp.Playlist(b.playlistcontainer_playlist(this._sp_object, sp.Session.currentSession._sp_session, idx)) +} + /** * gets a list of all the playlists in the playlist container */ @@ -41,11 +77,15 @@ PlaylistContainer.prototype.getPlaylists = function getPlaylists(callback) { var i, numReady = 0; var numPlaylists = this.getNumPlaylists(); - var playlists = new Array(numPlaylists); + var playlists = new Array(); - for (i = 0; i < playlists.length; i++) { - var playlist = new sp.Playlist(b.playlistcontainer_playlist(this._sp_object, sp.Session.currentSession._sp_session, i)); - playlists[i] = playlist; + for (i = 0; i < numPlaylists; i++) { + var type = this.getPlaylistType(i); + switch (type) { + case b.SP_PLAYLIST_TYPE_PLAYLIST: + playlists.push(this.getPlaylistAtIndex(i)); + break; + } } for (i = 0; i < playlists.length; i++) { @@ -61,7 +101,7 @@ PlaylistContainer.prototype.getPlaylists = function getPlaylists(callback) { var checkPlaylistsLoaded = function(numReady, array, callback) { if (numReady >= array.length) { - callback(array); + callback(array); } }; diff --git a/lib/Search.js b/lib/Search.js index bf8b52d..81f5bcf 100644 --- a/lib/Search.js +++ b/lib/Search.js @@ -1,6 +1,8 @@ var b = require('bindings')('spotify.node'); var Session = require('./Session'); var Track = require('./Track'); +var Album = require('./Album'); +var Artist = require('./Artist'); var util = require('util'); var EventEmitter = require('events').EventEmitter; var format = require('format').format; @@ -45,7 +47,7 @@ Search.prototype.execute = function execute(cb) { this._session._sp_session, this._query, this.trackOffset || 0, - this.trackCount || 10, + this.trackCount || 0, this.albumOffset || 0, this.albumCount || 0, this.artistOffset || 0, @@ -75,14 +77,24 @@ Search.prototype.execute = function execute(cb) { }; Search.prototype._processResults = function _processResults(search) { + var i; + this.tracks = new Array(b.search_num_tracks(this._sp_search)); + this.albums = new Array(b.search_num_albums(this._sp_search)); + this.artists = new Array(b.search_num_artists(this._sp_search)); - for (var i = 0; i < this.tracks.length; ++i) { + for (i = 0; i < this.tracks.length; ++i) { this.tracks[i] = new Track(b.search_track(this._sp_search, i)); } - this.artists = []; - this.albums = []; + for (i = 0; i < this.albums.length; ++i) { + this.albums[i] = new Album(b.search_album(this._sp_search, i)); + } + + for (i = 0; i < this.artists.length; ++i) { + this.artists[i] = new Artist(b.search_artist(this._sp_search, i)); + } + this.playlists = []; }; diff --git a/lib/Session.js b/lib/Session.js index 6ac4062..16d3e89 100644 --- a/lib/Session.js +++ b/lib/Session.js @@ -241,5 +241,12 @@ Session.prototype.getPlaylistcontainer = function getPlaylistcontainer() { return new sp.PlaylistContainer(b.session_playlistcontainer(this._sp_session)); }; +/** + * get the starred playlist for the current session + */ +Session.prototype.getStarred = function getStarred() { + return new sp.Playlist(b.session_starred_create(this._sp_session)); +} + // exports this Class module.exports = Session; diff --git a/lib/SpObject.js b/lib/SpObject.js index 7b91187..73df1bf 100644 --- a/lib/SpObject.js +++ b/lib/SpObject.js @@ -15,8 +15,8 @@ function SpObject () { var self = this; var i = 0; - // Playlist Container and Playlist have their own ready callbacks so we don't need to poll - if (self._object_type != 'playlist' && self._object_type != 'playlist_container') { + // Playlist Container, Playlist and Image have their own ready callbacks so we don't need to poll + if (self._object_type != 'playlist' && self._object_type != 'playlist_container' && self._object_type != 'image') { // if our reference is already loaded // populate attributes and trigger ready event as @@ -73,16 +73,20 @@ SpObject.prototype._populateAttributes = function _populateAttributes() { * @throws TypeError when the given uri doesn't describe an object of this type */ SpObject.getFromUrl = function getFromUrl(url) { - if(sp.getLinkType(url) !== this._object_type) { + var type = sp.getLinkType(url); + var method = 'link_as_' + this._object_type; + if(type !== this._object_type) { throw new URIError("Not a "+ this._object_type +" URI"); } - // `this` is actually the constructor here - var method_name = 'link_as_' + this._object_type; - if(typeof b[method_name] === 'function') { - return new this(b[method_name](url)); + if(typeof(b[method]) !== 'function') { + throw new TypeError('Unknown Spotify object type'); } - else { - throw new TypeError('Unkown spotify object type'); + switch(sp.getLinkType(url)) { + case 'image': + case 'playlist': + return new this(b[method](url, sp.Session.currentSession._sp_session)); + default: + return new this(b[method](url)); } }; @@ -145,17 +149,19 @@ SpObject.prototype.getSession = function getSession() { * @return String * @throws URIError */ -SpObject.prototype.getUrl = function getUrl() { +SpObject.prototype.getUrl = function getUrl(ms) { if (this._sp_url === undefined) { - var method_name = 'link_create_from_' + this._object_type; - if (!b[method_name]) { - throw new URIError('This object can not be represented with a URL'); + if(this._object_type == 'track') { + this._sp_url = b.link_create_from_track(this._sp_object, (ms || 0)); + } else { + var method_name = 'link_create_from_' + this._object_type; + if (!b[method_name]) { + throw new URIError('This object can not be represented with a URL'); + } + this._sp_url = b[method_name](this._sp_object); } - this._sp_url = b[method_name](this._sp_object); - return this._sp_url; - } else { - return this._sp_url; } + return this._sp_url; }; SpObject.prototype.getLink = SpObject.prototype.getUrl; diff --git a/lib/libspotify.js b/lib/libspotify.js index 8db6e91..4dd1374 100644 --- a/lib/libspotify.js +++ b/lib/libspotify.js @@ -4,6 +4,7 @@ exports.Player = require('./Player'); exports.Search = require('./Search'); exports.Session = require('./Session'); exports.Track = require('./Track'); +exports.Image = require('./Image'); exports.Playlist = require('./Playlist'); exports.PlaylistContainer = require('./PlaylistContainer'); @@ -14,7 +15,7 @@ exports.getLinkType = function getLinkType(url) { throw new TypeError("Given parameter is not a string. Spotify links are strings."); } var res = b.link_type(url); - if(res === false) { + if(res === null) { throw new Error("Given parameter is probably not a valid URI"); } return res; diff --git a/src/album.cc b/src/album.cc index e4c2b40..e7799e8 100644 --- a/src/album.cc +++ b/src/album.cc @@ -18,6 +18,7 @@ #include "common.h" +#include "imagecallbacks.cc" using namespace v8; using namespace nsp; @@ -41,6 +42,25 @@ static Handle Album_Is_Loaded(const Arguments& args) { return scope.Close(Boolean::New(loaded)); } +/** + * JS album_is_available implementation. checks if a given album is available + */ +static Handle Album_Is_Available(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 1); + assert(args[0]->IsObject()); + + // gets sp_album pointer from given object + ObjectHandle* album = ObjectHandle::Unwrap(args[0]); + + // actually call sp_album_is_available + bool available = sp_album_is_available(album->pointer); + + return scope.Close(Boolean::New(available)); +} + /** * JS album_name implementation. checks if a given album is loaded */ @@ -170,32 +190,32 @@ static Handle Album_Cover(const Arguments& args) { HandleScope scope; // test arguments sanity - assert(args.Length() == 4); + assert(args.Length() == 3); assert(args[0]->IsObject()); // sp_session assert(args[1]->IsObject()); // sp_album assert(args[2]->IsNumber()); // sp_image_size - assert(args[3]->IsFunction()); // callback after cover image was loaded ObjectHandle *session = ObjectHandle::Unwrap(args[0]); ObjectHandle *album = ObjectHandle::Unwrap(args[1]); Handle requestedImageSize = Local::Cast(args[2]); - Handle callback = Persistent::New(Handle::Cast(args[3])); sp_image_size imageSize = static_cast(requestedImageSize->Uint32Value()); const byte *imageId = sp_album_cover(album->pointer, imageSize); if(imageId) { - sp_image *image = sp_image_create(session->pointer, imageId); - sp_image_add_load_callback(image, &cb_image_loaded_album, *callback); - return scope.Close(Boolean::New(true)); + ObjectHandle* obj = new ObjectHandle("sp_image"); + obj->pointer = sp_image_create(session->pointer, imageId); + sp_image_add_load_callback(obj->pointer, &cb_image_loaded, obj); + return scope.Close(obj->object); } else { - return scope.Close(Boolean::New(false)); + return scope.Close(Null()); } } void nsp::init_album(Handle target) { NODE_SET_METHOD(target, "album_is_loaded", Album_Is_Loaded); + NODE_SET_METHOD(target, "album_is_available", Album_Is_Available); NODE_SET_METHOD(target, "album_name", Album_Name); NODE_SET_METHOD(target, "album_year", Album_Year); NODE_SET_METHOD(target, "album_type", Album_Type); diff --git a/src/albumbrowse.cc b/src/albumbrowse.cc new file mode 100644 index 0000000..e21e84a --- /dev/null +++ b/src/albumbrowse.cc @@ -0,0 +1,102 @@ +/* + * ===================================================================================== + * + * Filename: albumbrowse.cc + * + * Description: bindings for the album subsystem + * + * Version: 1.0 + * Revision: none + * Compiler: gcc + * + * Author: Linus Unnebäck, linus@folkdatorn.se + * Company: LinusU AB + * + * ===================================================================================== + */ + + +#include "common.h" + +using namespace v8; +using namespace nsp; + +void cb_albumbrowse_complete (sp_albumbrowse *result, void *userdata) { + Persistent callback = static_cast(userdata); + + callback->Call(callback, 0, NULL); + callback.Dispose(); +} + +static Handle AlbumBrowse_Create(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 3); + assert(args[0]->IsObject()); // sp_session + assert(args[1]->IsObject()); // sp_album + assert(args[2]->IsFunction()); // callback + + ObjectHandle *session = ObjectHandle::Unwrap(args[0]); + ObjectHandle *album = ObjectHandle::Unwrap(args[1]); + Handle callback = Persistent::New(Handle::Cast(args[2])); + + ObjectHandle* albumbrowse = new ObjectHandle("sp_albumbrowse"); + albumbrowse->pointer = sp_albumbrowse_create(session->pointer, album->pointer, cb_albumbrowse_complete, *callback); + + return scope.Close(albumbrowse->object); +} + +static Handle AlbumBrowse_Num_Tracks(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 1); + assert(args[0]->IsObject()); // sp_albumbrowse + + ObjectHandle *albumbrowse = ObjectHandle::Unwrap(args[0]); + const int num = sp_albumbrowse_num_tracks(albumbrowse->pointer); + + return scope.Close(Number::New(num)); +} + +static Handle AlbumBrowse_Track(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 2); + assert(args[0]->IsObject()); // sp_albumbrowse + assert(args[1]->IsNumber()); // index + + // input + ObjectHandle *albumbrowse = ObjectHandle::Unwrap(args[0]); + int index = args[1]->ToNumber()->Int32Value(); + + // output + sp_track* sptrack = sp_albumbrowse_track(albumbrowse->pointer, index); + ObjectHandle* track = new ObjectHandle("sp_track"); + track->pointer = sptrack; + + return scope.Close(track->object); +} + +static Handle AlbumBrowse_Release(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 1); + assert(args[0]->IsObject()); // sp_albumbrowse + + ObjectHandle *albumbrowse = ObjectHandle::Unwrap(args[0]); + sp_error error = sp_albumbrowse_release(albumbrowse->pointer); + NSP_THROW_IF_ERROR(error); + + return scope.Close(Undefined()); +} + +void nsp::init_albumbrowse(Handle target) { + NODE_SET_METHOD(target, "albumbrowse_create", AlbumBrowse_Create); + NODE_SET_METHOD(target, "albumbrowse_num_tracks", AlbumBrowse_Num_Tracks); + NODE_SET_METHOD(target, "albumbrowse_track", AlbumBrowse_Track); + NODE_SET_METHOD(target, "albumbrowse_release", AlbumBrowse_Release); +} diff --git a/src/artistbrowse.cc b/src/artistbrowse.cc new file mode 100644 index 0000000..ada0487 --- /dev/null +++ b/src/artistbrowse.cc @@ -0,0 +1,102 @@ +/* + * ===================================================================================== + * + * Filename: artistbrowse.cc + * + * Description: bindings for the artist subsystem + * + * Version: 1.0 + * Revision: none + * Compiler: gcc + * + * Author: Linus Unnebäck, linus@folkdatorn.se + * Company: LinusU AB + * + * ===================================================================================== + */ + + +#include "common.h" + +using namespace v8; +using namespace nsp; + +void cb_artistbrowse_complete (sp_artistbrowse *result, void *userdata) { + Persistent callback = static_cast(userdata); + + callback->Call(callback, 0, NULL); + callback.Dispose(); +} + +static Handle ArtistBrowse_Create(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 3); + assert(args[0]->IsObject()); // sp_session + assert(args[1]->IsObject()); // sp_artist + assert(args[2]->IsFunction()); // callback + + ObjectHandle *session = ObjectHandle::Unwrap(args[0]); + ObjectHandle *artist = ObjectHandle::Unwrap(args[1]); + Handle callback = Persistent::New(Handle::Cast(args[2])); + + ObjectHandle* artistbrowse = new ObjectHandle("sp_artistbrowse"); + artistbrowse->pointer = sp_artistbrowse_create(session->pointer, artist->pointer, SP_ARTISTBROWSE_NO_TRACKS, cb_artistbrowse_complete, *callback); + + return scope.Close(artistbrowse->object); +} + +static Handle ArtistBrowse_Num_Albums(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 1); + assert(args[0]->IsObject()); // sp_artistbrowse + + ObjectHandle *artistbrowse = ObjectHandle::Unwrap(args[0]); + const int num = sp_artistbrowse_num_albums(artistbrowse->pointer); + + return scope.Close(Number::New(num)); +} + +static Handle ArtistBrowse_Album(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 2); + assert(args[0]->IsObject()); // sp_artistbrowse + assert(args[1]->IsNumber()); // index + + // input + ObjectHandle *artistbrowse = ObjectHandle::Unwrap(args[0]); + int index = args[1]->ToNumber()->Int32Value(); + + // output + sp_album* spalbum = sp_artistbrowse_album(artistbrowse->pointer, index); + ObjectHandle* album = new ObjectHandle("sp_album"); + album->pointer = spalbum; + + return scope.Close(album->object); +} + +static Handle ArtistBrowse_Release(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 1); + assert(args[0]->IsObject()); // sp_artistbrowse + + ObjectHandle *artistbrowse = ObjectHandle::Unwrap(args[0]); + sp_error error = sp_artistbrowse_release(artistbrowse->pointer); + NSP_THROW_IF_ERROR(error); + + return scope.Close(Undefined()); +} + +void nsp::init_artistbrowse(Handle target) { + NODE_SET_METHOD(target, "artistbrowse_create", ArtistBrowse_Create); + NODE_SET_METHOD(target, "artistbrowse_num_albums", ArtistBrowse_Num_Albums); + NODE_SET_METHOD(target, "artistbrowse_album", ArtistBrowse_Album); + NODE_SET_METHOD(target, "artistbrowse_release", ArtistBrowse_Release); +} diff --git a/src/audio.cc b/src/audio.cc index 2dd9305..1ee4ea4 100644 --- a/src/audio.cc +++ b/src/audio.cc @@ -40,18 +40,18 @@ audio_fifo_data_t* audio_get(audio_fifo_t *af) audio_fifo_data_t *afd; // make sure we're the only one using the queue right now pthread_mutex_lock(&af->mutex); - + // if the queue is empty, do nothing afd = TAILQ_FIRST(&af->q); if(!afd) { pthread_mutex_unlock(&af->mutex); return NULL; } - + // get the data at the head of the queue TAILQ_REMOVE(&af->q, afd, link); af->qlen -= afd->nsamples; - + // we're done using the queue pthread_mutex_unlock(&af->mutex); return afd; @@ -65,8 +65,8 @@ void audio_fifo_flush(audio_fifo_t *af) pthread_mutex_lock(&af->mutex); while((afd = TAILQ_FIRST(&af->q))) { - TAILQ_REMOVE(&af->q, afd, link); - free(afd); + TAILQ_REMOVE(&af->q, afd, link); + free(afd); } af->qlen = 0; diff --git a/src/audio.h b/src/audio.h index 5aa0bf4..ac35fd3 100644 --- a/src/audio.h +++ b/src/audio.h @@ -38,19 +38,19 @@ typedef struct sp_session sp_session; /* --- Types --- */ typedef struct audio_fifo_data { - TAILQ_ENTRY(audio_fifo_data) link; - int channels; - int rate; - int nsamples; + TAILQ_ENTRY(audio_fifo_data) link; + int channels; + int rate; + int nsamples; sp_session* session; - int16_t samples[0]; + int16_t samples[0]; } audio_fifo_data_t; typedef struct audio_fifo { - TAILQ_HEAD(, audio_fifo_data) q; - int qlen; - pthread_mutex_t mutex; - pthread_cond_t cond; + TAILQ_HEAD(, audio_fifo_data) q; + int qlen; + pthread_mutex_t mutex; + pthread_cond_t cond; } audio_fifo_t; diff --git a/src/binding.cc b/src/binding.cc index f1484bf..f4d9d98 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -27,7 +27,9 @@ extern "C" { // initializing all modules nsp::init_album(target); + nsp::init_albumbrowse(target); nsp::init_artist(target); + nsp::init_artistbrowse(target); nsp::init_link(target); nsp::init_player(target); nsp::init_search(target); @@ -35,6 +37,7 @@ extern "C" { nsp::init_track(target); nsp::init_playlistcontainer(target); nsp::init_playlist(target); + nsp::init_image(target); } } diff --git a/src/common.h b/src/common.h index 79b8e53..c27e56d 100644 --- a/src/common.h +++ b/src/common.h @@ -140,10 +140,12 @@ namespace nsp { * init the album related functions to the target module exports */ void init_album(v8::Handle target); + void init_albumbrowse(v8::Handle target); /** * init the artist related functions to the target module exports */ void init_artist(v8::Handle target); + void init_artistbrowse(v8::Handle target); /** * init the link related functions to the target module exports */ @@ -152,11 +154,15 @@ namespace nsp { * init the playlistcontainer related functions to the target module exports */ void init_playlistcontainer(v8::Handle target); - /** - * init the playlist related functions to the target module exports - */ + /** + * init the playlist related functions to the target module exports + */ void init_playlist(v8::Handle target); - + /** + * init the image related functions to the target module exports + */ + void init_image(v8::Handle target); + /** * This utility class allows to keep track of a C pointer that we attached * to a JS object. It differs from node's ObjectWrap in the fact that it @@ -191,7 +197,7 @@ namespace nsp { * We do create this one */ v8::Persistent object; - + /** * Get the name of the ObjectHandle that we gave it during instanciation */ @@ -229,7 +235,7 @@ namespace nsp { object->SetPointerInInternalField(0, this); } - + template ObjectHandle* ObjectHandle::Unwrap(v8::Handle obj) { assert(obj->IsObject()); diff --git a/src/image.cc b/src/image.cc new file mode 100644 index 0000000..ece64e5 --- /dev/null +++ b/src/image.cc @@ -0,0 +1,77 @@ +/* + * ===================================================================================== + * + * Filename: image.cc + * + * Description: bindings for the image subsystem + * + * Version: 1.0 + * Revision: none + * Compiler: gcc + * + * Author: Linus Unnebäck, linus@folkdatorn.se + * Company: LinusU AB + * + * ===================================================================================== + */ + + +#include "common.h" + +using namespace v8; +using namespace nsp; + +static Handle Image_Is_Loaded(const Arguments& args) { + HandleScope scope; + + assert(args.Length() == 1); + assert(args[0]->IsObject()); + + ObjectHandle* image = ObjectHandle::Unwrap(args[0]); + bool loaded = sp_image_is_loaded(image->pointer); + + return scope.Close(Boolean::New(loaded)); +} + +static Handle Image_Data(const Arguments& args) { + HandleScope scope; + + assert(args.Length() == 1); + assert(args[0]->IsObject()); + + size_t image_size; + ObjectHandle* image = ObjectHandle::Unwrap(args[0]); + const void *image_data = sp_image_data(image->pointer, &image_size); + + // Create a C++ world slow buffer: + node::Buffer *slowBuffer= node::Buffer::New(image_size); + memcpy(node::Buffer::Data(slowBuffer), image_data, image_size); + + // Get the Buffer constructor from the JavaScript world: + Local globalObj = Context::GetCurrent()->Global(); + Local bufferConstructor = Local::Cast(globalObj->Get(String::New("Buffer"))); + Handle constructorArgs[3] = { slowBuffer->handle_, Integer::New(image_size), Integer::New(0) }; + + // Create a JavaScript buffer using the slow buffer: + Local actualBuffer = bufferConstructor->NewInstance(3, constructorArgs); + + return scope.Close(actualBuffer); +} + +static Handle Image_Relase(const Arguments& args) { + HandleScope scope; + + assert(args.Length() == 1); + assert(args[0]->IsObject()); + + ObjectHandle* image = ObjectHandle::Unwrap(args[0]); + sp_image_release(image->pointer); + + return scope.Close(Undefined()); +} + +void nsp::init_image(Handle target) { + NODE_SET_METHOD(target, "image_data", Image_Data); + NODE_SET_METHOD(target, "image_relase", Image_Relase); + NODE_SET_METHOD(target, "image_is_loaded", Image_Is_Loaded); +} diff --git a/src/imagecallbacks.cc b/src/imagecallbacks.cc new file mode 100644 index 0000000..2989b8f --- /dev/null +++ b/src/imagecallbacks.cc @@ -0,0 +1,32 @@ +/* + * ===================================================================================== + * + * Filename: imagecallbacks.cc + * + * Description: callbacks for the image subsystem + * + * Version: 1.0 + * Revision: none + * Compiler: gcc + * + * Author: Linus Unnebäck, linus@folkdatorn.se + * Company: LinusU AB + * + * ===================================================================================== + */ + + +#include "common.h" + +using namespace v8; +using namespace nsp; + +static void cb_image_loaded(sp_image *sp_obj, void *userdata) { + ObjectHandle* image = (ObjectHandle*) userdata; + Handle cbv = image->object->Get(String::New("image_loaded")); + + if(cbv->IsFunction()) { + Handle callback = Local(Function::Cast(*cbv)); + callback->Call(Context::GetCurrent()->Global(), 0, NULL); + } +} diff --git a/src/link.cc b/src/link.cc index 947c0ea..40a25cc 100644 --- a/src/link.cc +++ b/src/link.cc @@ -3,7 +3,7 @@ * * Filename: link.cc * - * Description: bindings for links subsystem + * Description: bindings for links subsystem * * Version: 1.0 * Created: 07/01/2013 12:37:03 @@ -18,67 +18,64 @@ #include "common.h" +#include "imagecallbacks.cc" #include "playlistcallbacks.cc" using namespace v8; using namespace nsp; -static Handle Link_Create_From_Track(const Arguments& args) { +template +static Handle Link_Create(const Arguments& args, sp_link* (*link_fn)(sp_type*)) { HandleScope scope; // test arguments sanity assert(args.Length() == 1); assert(args[0]->IsObject()); - // gets sp_track pointer from given object - ObjectHandle* track = ObjectHandle::Unwrap(args[0]); - - // TODO handle length in ms - sp_link* link = sp_link_create_from_track(track->pointer, 0); - char url[256]; - // TODO handle truncated urls - sp_link_as_string(link, url, 256); + // gets sp_type pointer from given object + ObjectHandle* obj = ObjectHandle::Unwrap(args[0]); - return scope.Close(String::New(url)); -} + sp_link* link = (*link_fn)(obj->pointer); -static Handle Link_Create_From_Artist(const Arguments& args) { - HandleScope scope; - - // test arguments sanity - assert(args.Length() == 1); - assert(args[0]->IsObject()); + if(link == NULL) { + return scope.Close(Null()); + } else { + // TODO handle truncated urls + char url[256]; + sp_link_as_string(link, url, 256); - // gets sp_artist pointer from given object - ObjectHandle* artist = ObjectHandle::Unwrap(args[0]); - - sp_link* link = sp_link_create_from_artist(artist->pointer); - char url[256]; - // TODO handle truncated urls - sp_link_as_string(link, url, 256); - - return scope.Close(String::New(url)); + return scope.Close(String::New(url)); + } } -static Handle Link_Create_From_Playlist(const Arguments& args) { +template +static Handle Link_Create(const Arguments& args, sp_link* (*link_fn)(sp_type*, sp_param)) { HandleScope scope; // test arguments sanity - assert(args.Length() == 1); + assert(args.Length() == 2); assert(args[0]->IsObject()); + assert(args[1]->IsNumber()); - // gets sp_playlist pointer from given object - ObjectHandle* playlist = ObjectHandle::Unwrap(args[0]); + // gets sp_type pointer from given object + ObjectHandle* obj = ObjectHandle::Unwrap(args[0]); - sp_link* link = sp_link_create_from_playlist(playlist->pointer); - char url[256]; - // TODO handle truncated urls - sp_link_as_string(link, url, 256); + sp_param param = static_cast(args[1]->ToNumber()->Int32Value()); + sp_link* link = (*link_fn)(obj->pointer, param); - return scope.Close(String::New(url)); + if(link == NULL) { + return scope.Close(Null()); + } else { + // TODO handle truncated urls + char url[256]; + sp_link_as_string(link, url, 256); + + return scope.Close(String::New(url)); + } } -static Handle Link_As_Track(const Arguments& args) { +template +static Handle Link_As(const Arguments& args, sp_linktype type, sp_type* (*link_fn)(sp_link*), const char* name) { HandleScope scope; // test arguments sanity @@ -88,36 +85,46 @@ static Handle Link_As_Track(const Arguments& args) { String::Utf8Value url(args[0]); sp_link* link = sp_link_create_from_string(*url); - assert(sp_link_type(link) == SP_LINKTYPE_TRACK); + assert(sp_link_type(link) == type); - ObjectHandle* track = new ObjectHandle("sp_track"); - track->pointer = sp_link_as_track(link); + ObjectHandle* obj = new ObjectHandle(name); + obj->pointer = (*link_fn)(link); - return scope.Close(track->object); + return scope.Close(obj->object); } -static Handle Link_As_Artist(const Arguments& args) { +static Handle Link_As(const Arguments& args, sp_linktype type, sp_playlist* (*link_fn)(sp_session*, sp_link*), const char* name) { HandleScope scope; // test arguments sanity - assert(args.Length() == 1); + assert(type == SP_LINKTYPE_PLAYLIST); + assert(args.Length() == 2); assert(args[0]->IsString()); + assert(args[1]->IsObject()); String::Utf8Value url(args[0]); sp_link* link = sp_link_create_from_string(*url); - assert(sp_link_type(link) == SP_LINKTYPE_ARTIST); + assert(sp_link_type(link) == type); + + // gets sp_session pointer from given object + ObjectHandle* session = ObjectHandle::Unwrap(args[1]); + + ObjectHandle* obj = new ObjectHandle(name); + obj->pointer = link_fn(session->pointer, link); - ObjectHandle* artist = new ObjectHandle("sp_artist"); - artist->pointer = sp_link_as_artist(link); + // Add callbacks + sp_error error = sp_playlist_add_callbacks(obj->pointer, &nsp_playlist_callbacks, obj); + NSP_THROW_IF_ERROR(error); - return scope.Close(artist->object); + return scope.Close(obj->object); } -static Handle Link_As_Playlist(const Arguments& args) { +static Handle Link_As(const Arguments& args, sp_linktype type, sp_image* (*link_fn)(sp_session*, sp_link*), const char* name) { HandleScope scope; // test arguments sanity + assert(type == SP_LINKTYPE_IMAGE); assert(args.Length() == 2); assert(args[0]->IsString()); assert(args[1]->IsObject()); @@ -125,21 +132,80 @@ static Handle Link_As_Playlist(const Arguments& args) { String::Utf8Value url(args[0]); sp_link* link = sp_link_create_from_string(*url); - assert(sp_link_type(link) == SP_LINKTYPE_PLAYLIST); + assert(sp_link_type(link) == type); - // gets sp_session pointer from given object + // gets sp_session pointer from given object ObjectHandle* session = ObjectHandle::Unwrap(args[1]); - ObjectHandle* playlist = new ObjectHandle("sp_playlist"); - playlist->pointer = sp_playlist_create(session->pointer, link); - + ObjectHandle* obj = new ObjectHandle(name); + obj->pointer = link_fn(session->pointer, link); + // Add callbacks - sp_error error = sp_playlist_add_callbacks(playlist->pointer, &nsp_playlist_callbacks, playlist); - NSP_THROW_IF_ERROR(error); + sp_error error = sp_image_add_load_callback(obj->pointer, &cb_image_loaded, obj); + NSP_THROW_IF_ERROR(error); + + return scope.Close(obj->object); +} - return scope.Close(playlist->object); +static Handle Link_Create_From_Track(const Arguments& args) { + return Link_Create(args, sp_link_create_from_track); } +static Handle Link_Create_From_Album(const Arguments& args) { + return Link_Create(args, sp_link_create_from_album); +} + +static Handle Link_Create_From_Album_Cover(const Arguments& args) { + return Link_Create(args, sp_link_create_from_album_cover); +} + +static Handle Link_Create_From_Artist(const Arguments& args) { + return Link_Create(args, sp_link_create_from_artist); +} + +static Handle Link_Create_From_Artist_Portrait(const Arguments& args) { + return Link_Create(args, sp_link_create_from_artist_portrait); +} + +static Handle Link_Create_From_Search(const Arguments& args) { + return Link_Create(args, sp_link_create_from_search); +} + +static Handle Link_Create_From_Playlist(const Arguments& args) { + return Link_Create(args, sp_link_create_from_playlist); +} + +static Handle Link_Create_From_User(const Arguments& args) { + return Link_Create(args, sp_link_create_from_user); +} + +static Handle Link_Create_From_Image(const Arguments& args) { + return Link_Create(args, sp_link_create_from_image); +} + +static Handle Link_As_Track(const Arguments& args) { + return Link_As(args, SP_LINKTYPE_TRACK, sp_link_as_track, "sp_track"); +} + +static Handle Link_As_Album(const Arguments& args) { + return Link_As(args, SP_LINKTYPE_ALBUM, sp_link_as_album, "sp_album"); +} + +static Handle Link_As_Artist(const Arguments& args) { + return Link_As(args, SP_LINKTYPE_ARTIST, sp_link_as_artist, "sp_artist"); +} + +static Handle Link_As_User(const Arguments& args) { + return Link_As(args, SP_LINKTYPE_PROFILE, sp_link_as_user, "sp_user"); +} + +static Handle Link_As_Playlist(const Arguments& args) { + return Link_As(args, SP_LINKTYPE_PLAYLIST, sp_playlist_create, "sp_playlist"); +} + +static Handle Link_As_Image(const Arguments& args) { + return Link_As(args, SP_LINKTYPE_IMAGE, sp_image_create_from_link, "sp_image"); +} static Handle Link_Type(const Arguments& args) { HandleScope scope; @@ -151,36 +217,47 @@ static Handle Link_Type(const Arguments& args) { String::Utf8Value url(args[0]); sp_link* link = sp_link_create_from_string(*url); - if(!link) { - return scope.Close(Boolean::New(false)); - } - switch(sp_link_type(link)) { - case SP_LINKTYPE_PLAYLIST: - type = "playlist"; - break; - case SP_LINKTYPE_ARTIST: - type = "artist"; - break; - case SP_LINKTYPE_TRACK: - type = "track"; - break; - default: - return scope.Close(Boolean::New(false)); - break; + if(link) { + switch(sp_link_type(link)) { + case SP_LINKTYPE_INVALID: type = NULL; break; + case SP_LINKTYPE_TRACK: type = "track"; break; + case SP_LINKTYPE_ALBUM: type = "album"; break; + case SP_LINKTYPE_ARTIST: type = "artist"; break; + case SP_LINKTYPE_SEARCH: type = "search"; break; + case SP_LINKTYPE_PLAYLIST: type = "playlist"; break; + case SP_LINKTYPE_PROFILE: type = "profile"; break; + case SP_LINKTYPE_STARRED: type = "starred"; break; + case SP_LINKTYPE_LOCALTRACK: type = "localtrack"; break; + case SP_LINKTYPE_IMAGE: type = "image"; break; + default: assert(false); break; + } } - return scope.Close(String::New(type)); + if(type) { + return scope.Close(String::New(type)); + } else { + return scope.Close(Null()); + } } void nsp::init_link(Handle target) { NODE_SET_METHOD(target, "link_create_from_track", Link_Create_From_Track); + NODE_SET_METHOD(target, "link_create_from_album", Link_Create_From_Album); + NODE_SET_METHOD(target, "link_create_from_album_cover", Link_Create_From_Album_Cover); NODE_SET_METHOD(target, "link_create_from_artist", Link_Create_From_Artist); + NODE_SET_METHOD(target, "link_create_from_artist_portrait", Link_Create_From_Artist_Portrait); + NODE_SET_METHOD(target, "link_create_from_search", Link_Create_From_Search); NODE_SET_METHOD(target, "link_create_from_playlist", Link_Create_From_Playlist); + NODE_SET_METHOD(target, "link_create_from_user", Link_Create_From_User); + NODE_SET_METHOD(target, "link_create_from_image", Link_Create_From_Image); NODE_SET_METHOD(target, "link_as_track", Link_As_Track); + NODE_SET_METHOD(target, "link_as_album", Link_As_Album); NODE_SET_METHOD(target, "link_as_artist", Link_As_Artist); + NODE_SET_METHOD(target, "link_as_user", Link_As_User); NODE_SET_METHOD(target, "link_as_playlist", Link_As_Playlist); + NODE_SET_METHOD(target, "link_as_image", Link_As_Image); NODE_SET_METHOD(target, "link_type", Link_Type); } diff --git a/src/playlist.cc b/src/playlist.cc index 17f5ca5..61cdb87 100644 --- a/src/playlist.cc +++ b/src/playlist.cc @@ -17,6 +17,7 @@ */ #include "common.h" +#include "imagecallbacks.cc" #include "playlistcallbacks.cc" #include @@ -61,8 +62,76 @@ static Handle PlaylistContainer_Num_Playlists(const Arguments& args) { // actually call sp_playlistcontainer_num_playlists int numPlaylists = sp_playlistcontainer_num_playlists(playlistcontainer->pointer); - - return scope.Close(Number::New(numPlaylists)); + + return scope.Close(Number::New(numPlaylists)); +} + +/** + * JS playlist type implementation. + */ +static Handle PlaylistContainer_Playlist_Type(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 2); + assert(args[0]->IsObject()); + assert(args[1]->IsNumber()); + + // gets sp_playlistcontainer pointer from given object + ObjectHandle* playlistcontainer = ObjectHandle::Unwrap(args[0]); + + int index = args[1]->ToNumber()->Int32Value(); + + // actually call sp_playlistcontainer_playlist_type + sp_playlist_type playlistType = sp_playlistcontainer_playlist_type(playlistcontainer->pointer, index); + + return scope.Close(Number::New(static_cast(playlistType))); +} + +/** + * JS playlist folder id implementation. + */ +static Handle PlaylistContainer_Playlist_Folder_ID(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 2); + assert(args[0]->IsObject()); + assert(args[1]->IsNumber()); + + // gets sp_playlistcontainer pointer from given object + ObjectHandle* playlistcontainer = ObjectHandle::Unwrap(args[0]); + + int index = args[1]->ToNumber()->Int32Value(); + + // actually call sp_playlistcontainer_playlist_folder_id + sp_uint64 playlistFolderId = sp_playlistcontainer_playlist_folder_id(playlistcontainer->pointer, index); + + return scope.Close(Number::New(playlistFolderId)); +} + +/** + * JS playlist folder name implementation. + */ +static Handle PlaylistContainer_Playlist_Folder_Name(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 2); + assert(args[0]->IsObject()); + assert(args[1]->IsNumber()); + + // gets sp_playlistcontainer pointer from given object + ObjectHandle* playlistcontainer = ObjectHandle::Unwrap(args[0]); + + int index = args[1]->ToNumber()->Int32Value(); + char nameChars[256]; + + // actually call sp_playlistcontainer_playlist_folder_name + sp_error error = sp_playlistcontainer_playlist_folder_name(playlistcontainer->pointer, index, nameChars, sizeof(nameChars)); + NSP_THROW_IF_ERROR(error); + + return scope.Close(String::New(nameChars)); } /** @@ -77,36 +146,43 @@ static Handle PlaylistContainer_Playlist(const Arguments& args) { assert(args[1]->IsObject()); assert(args[2]->IsNumber()); - // gets sp_playlistcontainer pointer from given object + // gets sp_playlistcontainer pointer from given object ObjectHandle* playlistcontainer = ObjectHandle::Unwrap(args[0]); - // gets sp_session pointer from given object + // gets sp_session pointer from given object ObjectHandle* session = ObjectHandle::Unwrap(args[1]); - int index = args[2]->ToNumber()->Int32Value(); + int index = args[2]->ToNumber()->Int32Value(); assert(index >= 0); assert(index < sp_playlistcontainer_num_playlists(playlistcontainer->pointer)); // actually call sp_playlistcontainer_playlist sp_playlist* spplaylist = sp_playlistcontainer_playlist(playlistcontainer->pointer, index); - + // Set the playlist in RAM sp_playlist_set_in_ram(session->pointer, spplaylist, true); - + ObjectHandle* playlist = new ObjectHandle("sp_playlist"); playlist->pointer = spplaylist; - + sp_error error = sp_playlist_add_callbacks(spplaylist, &nsp_playlist_callbacks, playlist); - NSP_THROW_IF_ERROR(error); - - return scope.Close(playlist->object); + NSP_THROW_IF_ERROR(error); + + return scope.Close(playlist->object); } void nsp::init_playlistcontainer(Handle target) { - NODE_SET_METHOD(target, "playlistcontainer_is_loaded", PlaylistContainer_Is_Loaded); + NODE_SET_METHOD(target, "playlistcontainer_is_loaded", PlaylistContainer_Is_Loaded); NODE_SET_METHOD(target, "playlistcontainer_num_playlists", PlaylistContainer_Num_Playlists); + NODE_SET_METHOD(target, "playlistcontainer_playlist_type", PlaylistContainer_Playlist_Type); + NODE_SET_METHOD(target, "playlistcontainer_playlist_folder_id", PlaylistContainer_Playlist_Folder_ID); + NODE_SET_METHOD(target, "playlistcontainer_playlist_folder_name", PlaylistContainer_Playlist_Folder_Name); NODE_SET_METHOD(target, "playlistcontainer_playlist", PlaylistContainer_Playlist); + target->Set(v8::String::NewSymbol("SP_PLAYLIST_TYPE_PLAYLIST"), v8::Int32::New(static_cast(SP_PLAYLIST_TYPE_PLAYLIST)), ReadOnly); + target->Set(v8::String::NewSymbol("SP_PLAYLIST_TYPE_START_FOLDER"), v8::Int32::New(static_cast(SP_PLAYLIST_TYPE_START_FOLDER)), ReadOnly); + target->Set(v8::String::NewSymbol("SP_PLAYLIST_TYPE_END_FOLDER"), v8::Int32::New(static_cast(SP_PLAYLIST_TYPE_END_FOLDER)), ReadOnly); + target->Set(v8::String::NewSymbol("SP_PLAYLIST_TYPE_PLACEHOLDER"), v8::Int32::New(static_cast(SP_PLAYLIST_TYPE_PLACEHOLDER)), ReadOnly); } /* @@ -146,7 +222,7 @@ static Handle Playlist_Name(const Arguments& args) { ObjectHandle* playlist = ObjectHandle::Unwrap(args[0]); // actually call sp_playlist_name - const char* name = sp_playlist_name(playlist->pointer); + const char* name = sp_playlist_name(playlist->pointer); return scope.Close(String::New(name)); } @@ -166,8 +242,8 @@ static Handle Playlist_Num_Tracks(const Arguments& args) { // actually call sp_playlist_num_tracks int numTracks = sp_playlist_num_tracks(playlist->pointer); - - return scope.Close(Number::New(numTracks)); + + return scope.Close(Number::New(numTracks)); } /** @@ -181,21 +257,21 @@ static Handle Playlist_Track(const Arguments& args) { assert(args[0]->IsObject()); assert(args[1]->IsNumber()); - // gets sp_playlist pointer from given object + // gets sp_playlist pointer from given object ObjectHandle* playlist = ObjectHandle::Unwrap(args[0]); - int index = args[1]->ToNumber()->Int32Value(); + int index = args[1]->ToNumber()->Int32Value(); assert(index >= 0); assert(index < sp_playlist_num_tracks(playlist->pointer)); // actually call sp_playlist_track sp_track* sptrack = sp_playlist_track(playlist->pointer, index); - + ObjectHandle* track = new ObjectHandle("sp_track"); track->pointer = sptrack; - - return scope.Close(track->object); + + return scope.Close(track->object); } /** @@ -209,16 +285,16 @@ static Handle Playlist_Update_Subscribers(const Arguments& args) { assert(args[0]->IsObject()); assert(args[1]->IsObject()); - // gets sp_session pointer from given object + // gets sp_session pointer from given object ObjectHandle* session = ObjectHandle::Unwrap(args[0]); - // gets sp_playlist pointer from given object + // gets sp_playlist pointer from given object ObjectHandle* playlist = ObjectHandle::Unwrap(args[1]); - sp_error error = sp_playlist_update_subscribers(session->pointer, playlist->pointer); - NSP_THROW_IF_ERROR(error); - - return scope.Close(Undefined()); + sp_error error = sp_playlist_update_subscribers(session->pointer, playlist->pointer); + NSP_THROW_IF_ERROR(error); + + return scope.Close(Undefined()); } /** @@ -231,20 +307,50 @@ static Handle Playlist_Num_Subscribers(const Arguments& args) { assert(args.Length() == 1); assert(args[0]->IsObject()); - // gets sp_playlist pointer from given object + // gets sp_playlist pointer from given object ObjectHandle* playlist = ObjectHandle::Unwrap(args[0]); - int numSubscribers = sp_playlist_num_subscribers(playlist->pointer); + int numSubscribers = sp_playlist_num_subscribers(playlist->pointer); + + return scope.Close(Number::New(numSubscribers)); +} + +/** + * JS playlist image. Get the image for the playlist + */ +static Handle Playlist_Get_Image(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 2); + assert(args[0]->IsObject()); + assert(args[1]->IsObject()); + + byte image_id[20]; + ObjectHandle* session = ObjectHandle::Unwrap(args[0]); + ObjectHandle* playlist = ObjectHandle::Unwrap(args[1]); + + if(!sp_playlist_get_image(playlist->pointer, image_id)) { + return scope.Close(Null()); + } + + ObjectHandle* obj = new ObjectHandle("sp_image"); + obj->pointer = sp_image_create(session->pointer, image_id); + + // Add callbacks + sp_error error = sp_image_add_load_callback(obj->pointer, &cb_image_loaded, obj); + NSP_THROW_IF_ERROR(error); - return scope.Close(Number::New(numSubscribers)); + return scope.Close(obj->object); } void nsp::init_playlist(Handle target) { - NODE_SET_METHOD(target, "playlist_is_loaded", Playlist_Is_Loaded); - NODE_SET_METHOD(target, "playlist_name", Playlist_Name); - NODE_SET_METHOD(target, "playlist_num_tracks", Playlist_Num_Tracks); - NODE_SET_METHOD(target, "playlist_track", Playlist_Track); - NODE_SET_METHOD(target, "playlist_update_subscribers", Playlist_Update_Subscribers); - NODE_SET_METHOD(target, "playlist_num_subscribers", Playlist_Num_Subscribers); + NODE_SET_METHOD(target, "playlist_is_loaded", Playlist_Is_Loaded); + NODE_SET_METHOD(target, "playlist_name", Playlist_Name); + NODE_SET_METHOD(target, "playlist_num_tracks", Playlist_Num_Tracks); + NODE_SET_METHOD(target, "playlist_track", Playlist_Track); + NODE_SET_METHOD(target, "playlist_update_subscribers", Playlist_Update_Subscribers); + NODE_SET_METHOD(target, "playlist_num_subscribers", Playlist_Num_Subscribers); + NODE_SET_METHOD(target, "playlist_get_image", Playlist_Get_Image); } diff --git a/src/playlistcallbacks.cc b/src/playlistcallbacks.cc index e9ba3d8..99f12a5 100644 --- a/src/playlistcallbacks.cc +++ b/src/playlistcallbacks.cc @@ -241,8 +241,8 @@ static sp_playlist_callbacks nsp_playlist_callbacks = { &call_playlist_metadata_updated_callback, &call_track_created_changed_callback, &call_track_seen_changed_callback, - &call_description_changed_callback, - &call_image_changed_callback, - &call_track_message_changed_callback, - &call_subscribers_changed_callback + &call_description_changed_callback, + &call_image_changed_callback, + &call_track_message_changed_callback, + &call_subscribers_changed_callback }; diff --git a/src/search.cc b/src/search.cc index 24614a8..4a2f73c 100644 --- a/src/search.cc +++ b/src/search.cc @@ -3,7 +3,7 @@ * * Filename: search.cc * - * Description: bindings to the spotify search submodule + * Description: bindings to the spotify search submodule * * Version: 1.0 * Created: 23/12/2012 16:59:00 @@ -106,6 +106,36 @@ static Handle Search_Num_Tracks(const Arguments& args) { return scope.Close(Number::New(num)); } +/** + * JS search_num_albums implementation. + */ +static Handle Search_Num_Albums(const Arguments& args) { + HandleScope scope; + + assert(args.Length() == 1); + assert(args[0]->IsObject()); + + ObjectHandle* search = ObjectHandle::Unwrap(args[0]); + int num = sp_search_num_albums(search->pointer); + + return scope.Close(Number::New(num)); +} + +/** + * JS search_num_artists implementation. + */ +static Handle Search_Num_Artists(const Arguments& args) { + HandleScope scope; + + assert(args.Length() == 1); + assert(args[0]->IsObject()); + + ObjectHandle* search = ObjectHandle::Unwrap(args[0]); + int num = sp_search_num_artists(search->pointer); + + return scope.Close(Number::New(num)); +} + /** * JS search_track implementation. gets a track a the given index in a search result */ @@ -132,8 +162,64 @@ static Handle Search_Track(const Arguments& args) { return scope.Close(track->object); } +/** + * JS search_album implementation. gets a album a the given index in a search result + */ +static Handle Search_Album(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 2); + assert(args[0]->IsObject()); + assert(args[1]->IsNumber()); + + // gets sp_search pointer from given object + ObjectHandle* search = ObjectHandle::Unwrap(args[0]); + int index = args[1]->ToNumber()->Int32Value(); + + // check index is within search results range + assert(index >= 0); + assert(index < sp_search_num_albums(search->pointer)); + + // create new handle for this album + ObjectHandle* album = new ObjectHandle("sp_album"); + album->pointer = sp_search_album(search->pointer, index); + + return scope.Close(album->object); +} + +/** + * JS search_artist implementation. gets a artist a the given index in a search result + */ +static Handle Search_Artist(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 2); + assert(args[0]->IsObject()); + assert(args[1]->IsNumber()); + + // gets sp_search pointer from given object + ObjectHandle* search = ObjectHandle::Unwrap(args[0]); + int index = args[1]->ToNumber()->Int32Value(); + + // check index is within search results range + assert(index >= 0); + assert(index < sp_search_num_artists(search->pointer)); + + // create new handle for this artist + ObjectHandle* artist = new ObjectHandle("sp_artist"); + artist->pointer = sp_search_artist(search->pointer, index); + + return scope.Close(artist->object); +} + void nsp::init_search(Handle target) { NODE_SET_METHOD(target, "search_create", Search_Create); NODE_SET_METHOD(target, "search_num_tracks", Search_Num_Tracks); + NODE_SET_METHOD(target, "search_num_albums", Search_Num_Albums); + NODE_SET_METHOD(target, "search_num_artists", Search_Num_Artists); NODE_SET_METHOD(target, "search_track", Search_Track); + NODE_SET_METHOD(target, "search_album", Search_Album); + NODE_SET_METHOD(target, "search_artist", Search_Artist); } diff --git a/src/session.cc b/src/session.cc index 3bb7da1..7a612b9 100644 --- a/src/session.cc +++ b/src/session.cc @@ -81,7 +81,7 @@ static void call_logged_out_callback(sp_session* session) { * See https://developer.spotify.com/technologies/libspotify/docs/12.1.45/structsp__session__callbacks.html */ static void call_metadata_updated_callback(sp_session* session) { - ObjectHandle* s = (ObjectHandle*) sp_session_userdata(session); + ObjectHandle* s = (ObjectHandle*) sp_session_userdata(session); Handle o = s->object; Handle cbv = o->Get(String::New("metadata_updated")); if(!cbv->IsFunction()) { @@ -159,7 +159,7 @@ extern int call_music_delivery_callback(sp_session* session, const sp_audioforma * See https://developer.spotify.com/technologies/libspotify/docs/12.1.45/structsp__session__callbacks.html */ static void call_play_token_lost_callback(sp_session* session) { - ObjectHandle* s = (ObjectHandle*) sp_session_userdata(session); + ObjectHandle* s = (ObjectHandle*) sp_session_userdata(session); Handle o = s->object; Handle cbv = o->Get(String::New("play_token_lost")); if(!cbv->IsFunction()) { @@ -474,17 +474,40 @@ static Handle Session_PlaylistContainer(const Arguments& args) { ObjectHandle* session = ObjectHandle::Unwrap(args[0]); sp_playlistcontainer* spplaylistcontainer = sp_session_playlistcontainer(session->pointer); - + ObjectHandle* playlistcontainer = new ObjectHandle("sp_playlistcontainer"); playlistcontainer->pointer = spplaylistcontainer; - - // actually call sp_playlistcontainer_add_callbacks + + // actually call sp_playlistcontainer_add_callbacks sp_error error = sp_playlistcontainer_add_callbacks(spplaylistcontainer, &nsp_playlistcontainer_callbacks, playlistcontainer); - NSP_THROW_IF_ERROR(error); - + NSP_THROW_IF_ERROR(error); + return scope.Close(playlistcontainer->object); } +static Handle Session_Starred_Create(const Arguments& args) { + HandleScope scope; + + assert(args.Length() == 1); + assert(args[0]->IsObject()); + + ObjectHandle* session = ObjectHandle::Unwrap(args[0]); + + // actually call sp_session_starred_create + sp_playlist* spplaylist = sp_session_starred_create(session->pointer); + + // Set the playlist in RAM + sp_playlist_set_in_ram(session->pointer, spplaylist, true); + + ObjectHandle* playlist = new ObjectHandle("sp_playlist"); + playlist->pointer = spplaylist; + + sp_error error = sp_playlist_add_callbacks(spplaylist, &nsp_playlist_callbacks, playlist); + NSP_THROW_IF_ERROR(error); + + return scope.Close(playlist->object); +} + void nsp::init_session(Handle target) { NODE_SET_METHOD(target, "session_config", Session_Config); NODE_SET_METHOD(target, "session_create", Session_Create); @@ -493,4 +516,5 @@ void nsp::init_session(Handle target) { NODE_SET_METHOD(target, "session_logout", Session_Logout); NODE_SET_METHOD(target, "session_process_events", Session_Process_Events); NODE_SET_METHOD(target, "session_playlistcontainer", Session_PlaylistContainer); + NODE_SET_METHOD(target, "session_starred_create", Session_Starred_Create); } diff --git a/test/test-020-search-02-process.js b/test/test-020-search-02-process.js index e5903bb..cab29b7 100644 --- a/test/test-020-search-02-process.js +++ b/test/test-020-search-02-process.js @@ -27,3 +27,36 @@ exports.testGetTrackFromSearchResult = function(test) { test.done(); }); }; + +exports.testGetAlbumFromSearchResult = function(test) { + var search = new sp.Search('artist:"Hurts" album:"Exile"'); + search.trackCount = 0; + search.albumCount = 1; + search.execute(function() { + test.doesNotThrow(function() { + test.ok(search.albums.length > 0, "the search should return at least one result"); + var first = search.albums[0]; + test.ok(first instanceof sp.Album, "the album results should be loaded album objects"); + test.ok(first.isReady()); + test.equal('Hurts', first.artist, "the album should be a Hurts album"); + test.equal('Exile (Deluxe)', first.name, "the album should be Exile (Deluxe)"); + }); + test.done(); + }); +}; + +exports.testGetArtistFromSearchResult = function(test) { + var search = new sp.Search('artist:"Coldplay"'); + search.trackCount = 0; + search.artistCount = 1; + search.execute(function() { + test.doesNotThrow(function() { + test.ok(search.artists.length > 0, "the search should return at least one result"); + var first = search.artists[0]; + test.ok(first instanceof sp.Artist, "the artist results should be loaded artist objects"); + test.ok(first.isReady()); + test.equal('Coldplay', first.name, "the artist should be Coldplay"); + }); + test.done(); + }); +}; diff --git a/test/test-031-album.js b/test/test-031-album.js index f0fe6e9..e38f396 100644 --- a/test/test-031-album.js +++ b/test/test-031-album.js @@ -34,8 +34,8 @@ exports.album = { var album = first.album; test.doesNotThrow(function () { - album.coverImage(function(err, buffer) { - test.ok(err === null); + var img = album.coverImage() + img.whenReady(function() { return test.done(); }); }); @@ -49,8 +49,8 @@ exports.album = { var album = first.album; test.doesNotThrow(function () { - album.normalCoverImage(function(err, buffer) { - test.ok(err === null); + var img = album.normalCoverImage(); + img.whenReady(function() { return test.done(); }); }); @@ -64,8 +64,8 @@ exports.album = { var album = first.album; test.doesNotThrow(function () { - album.coverImage('small', function(err, buffer) { - test.ok(err === null); + var img = album.coverImage('small'); + img.whenReady(function() { return test.done(); }); }); @@ -79,8 +79,8 @@ exports.album = { var album = first.album; test.doesNotThrow(function () { - album.coverImage(album.IMAGE_SIZE_LARGE, function(err, buffer) { - test.ok(err === null); + img = album.coverImage(album.IMAGE_SIZE_LARGE); + img.whenReady(function() { return test.done(); }); }); @@ -94,7 +94,7 @@ exports.album = { var album = first.album; test.throws(function () { - album.coverImage('very-strange-size', function(err, buffer) {}); + album.coverImage('very-strange-size'); }, Error, 'Should fail with unknown size'); return test.done(); diff --git a/test/test-032-albumbrowse.js b/test/test-032-albumbrowse.js new file mode 100644 index 0000000..bb451fd --- /dev/null +++ b/test/test-032-albumbrowse.js @@ -0,0 +1,37 @@ +var sp = require('../lib/libspotify'); +var testutil = require('./util'); + +var getAlbum = function(test, cb) { + var search = new sp.Search('artist:"Hurts" album:"Exile"'); + search.trackCount = 1; + search.execute(function() { + test.ok(search.tracks.length > 0, 'the album was found'); + test.ok(search.tracks[0] instanceof sp.Track, 'track is an track'); + test.ok(search.tracks[0].album instanceof sp.Album, 'album is an album'); + cb(search.tracks[0].album); + }); +}; + +var session = null; + +exports.albumbrowse = { + setUp: function(cb) { + testutil.getDefaultTestSession(function(s) { + session = s; + cb(); + }); + }, + 'get tracks from album': function(test) { + getAlbum(test, function(album) { + album.getTracks(function(err, tracks) { + test.ifError(err); + test.equal(tracks.length, 14, 'the album has 14 tracks'); + test.equal(tracks.map(function(e) {return e instanceof sp.Track;}).indexOf(false), -1, 'It should only contain tracks'); + test.equal(tracks.reduce(function(prev, current) { + return prev && current.isReady(); + }, true), true, 'All tracks should be loaded'); + test.done(); + }); + }); + } +} diff --git a/test/test-034-artistbrowse.js b/test/test-034-artistbrowse.js new file mode 100644 index 0000000..7b6872d --- /dev/null +++ b/test/test-034-artistbrowse.js @@ -0,0 +1,39 @@ +var sp = require('../lib/libspotify'); +var testutil = require('./util'); + +var getArtist = function(test, cb) { + var search = new sp.Search('artist:"Hurts"'); + search.trackCount = 1; + search.execute(function() { + test.ok(search.tracks.length > 0, 'the track was found'); + test.ok(search.tracks[0] instanceof sp.Track, 'track is an track'); + test.ok(search.tracks[0].album instanceof sp.Album, 'album is an album'); + test.ok(search.tracks[0].album.artist instanceof sp.Artist, 'artist is an artist'); + cb(search.tracks[0].album.artist); + }); +}; + +var session = null; + +exports.artistbrowse = { + setUp: function(cb) { + testutil.getDefaultTestSession(function(s) { + session = s; + cb(); + }); + }, + 'get albums from artist': function(test) { + getArtist(test, function(artist) { + artist.getAvailableAlbums(function(err, albums) { + test.ifError(err); + /* FIXME: Should be 22, see comment in lib/Artist.js line 50 */ + test.equal(albums.length, 23, 'the artist has 23 available albums'); + test.equal(albums.map(function(e) {return e instanceof sp.Album;}).indexOf(false), -1, 'It should only contain albums'); + test.equal(albums.reduce(function(prev, current) { + return prev && current.isReady(); + }, true), true, 'All albums should be loaded'); + test.done(); + }); + }); + } +} diff --git a/test/test-035-image.js b/test/test-035-image.js new file mode 100644 index 0000000..e8efbe6 --- /dev/null +++ b/test/test-035-image.js @@ -0,0 +1,31 @@ +var sp = require('../lib/libspotify'); +var testutil = require('./util'); + +var session = null; + +exports.links = { + setUp: function(cb) { + testutil.getDefaultTestSession(function(s) { + session = s; + cb(); + }); + }, + 'get image for album cover': function(test) { + var img = sp.Image.getFromUrl('spotify:image:1ffad0da52c24ff70554dbc2b70d2265be777816'); + img.whenReady(function () { + var buffer = img.getData(); + test.ok(buffer instanceof Buffer, 'image data is a buffer'); + test.ok(buffer.length > 0, 'image buffer contains data'); + test.done(); + }); + }, + 'get image for artist portrait': function (test) { + var img = sp.Image.getFromUrl('spotify:image:32222dd4d53a339d92b1d8c72b678a1ec3e1840e'); + img.whenReady(function () { + var buffer = img.getData(); + test.ok(buffer instanceof Buffer, 'image data is a buffer'); + test.ok(buffer.length > 0, 'image buffer contains data'); + test.done(); + }); + } +}; diff --git a/test/test-060-link.js b/test/test-060-link.js index d52b68e..7483395 100644 --- a/test/test-060-link.js +++ b/test/test-060-link.js @@ -50,5 +50,19 @@ exports.links = { sp.Playlist.getFromUrl('spotify:track:4BdSLkzKO6iMVCgw7A7JBl'); }, URIError, 'We should get an URIError'); test.done(); + }, + 'get spotify url for album cover': function(test) { + var album = sp.Album.getFromUrl('spotify:album:2UGJa9DjYhXpBDKsCTyhSh'); + album.whenReady(function () { + test.equal(album.coverImageUrl(), 'spotify:image:1ffad0da52c24ff70554dbc2b70d2265be777816', 'the url should be correct'); + test.done(); + }); + }, + 'get spotify url for artist portrait': function (test) { + var artist = sp.Artist.getFromUrl('spotify:artist:4gzpq5DPGxSnKTe4SA8HAU'); + artist.whenReady(function () { + test.equal(artist.portraitImageUrl(), 'spotify:image:32222dd4d53a339d92b1d8c72b678a1ec3e1840e', 'the url should be correct'); + test.done(); + }); } }; diff --git a/test/test-065-link-types.js b/test/test-065-link-types.js index 9ffa5d9..b1d7fa8 100644 --- a/test/test-065-link-types.js +++ b/test/test-065-link-types.js @@ -28,10 +28,12 @@ exports.links = { }, "Getting link type from anything else than string should throw"); var track_link = 'spotify:track:4BdSLkzKO6iMVCgw7A7JBl'; + var album_link = 'spotify:album:2UGJa9DjYhXpBDKsCTyhSh'; var artist_link = 'spotify:artist:3zD5liDjbqljSRorrrcEjs'; var playlist_link = 'spotify:user:flobyiv:playlist:5ZMnMnJWGXZ9qm4gacHpQF'; test.doesNotThrow(function() { test.equal('track', sp.getLinkType(track_link), "Link type should be 'track'"); + test.equal('album', sp.getLinkType(album_link), "Link type should be 'album'"); test.equal('artist', sp.getLinkType(artist_link), "Link type should be 'artist'"); test.equal('playlist', sp.getLinkType(playlist_link), "Link type should be 'playlist'"); }, "Getting link types should not throw"); diff --git a/test/test-066-link-from-objects.js b/test/test-066-link-from-objects.js index 9e6cbdb..2caadab 100644 --- a/test/test-066-link-from-objects.js +++ b/test/test-066-link-from-objects.js @@ -27,6 +27,7 @@ exports.links = { }); }, "get link from track": testLink(sp.Track, 'spotify:track:4BdSLkzKO6iMVCgw7A7JBl'), + "get link from album": testLink(sp.Album, 'spotify:album:2UGJa9DjYhXpBDKsCTyhSh'), "get link from artist": testLink(sp.Artist, 'spotify:artist:4ZCLbhEKI7019HKbk5RsUq'), "get link from playlist": testLink(sp.Playlist, 'spotify:user:flobyiv:playlist:2t8yWR57SFWSKHtOlWr095'), 'get artist link from artist': function(test) { @@ -40,13 +41,21 @@ exports.links = { }); }, 'get artist from link': function(test) { - var track = sp.Artist.getFromUrl('spotify:artist:3zD5liDjbqljSRorrrcEjs'); - test.ok(track instanceof sp.Artist, 'the returned object should be an artist'); - track.on('ready', function() { - test.equal('Guillemots', track.name, 'this should be a guillemots track'); + var artist = sp.Artist.getFromUrl('spotify:artist:3zD5liDjbqljSRorrrcEjs'); + test.ok(artist instanceof sp.Artist, 'the returned object should be an artist'); + artist.on('ready', function() { + test.equal('Guillemots', artist.name, 'this should be the Guillemots artist'); test.done(); }); }, + 'get album from link': function(test) { + var album = sp.Album.getFromUrl('spotify:album:2UGJa9DjYhXpBDKsCTyhSh'); + test.ok(album instanceof sp.Album, 'the returned object should be an album'); + album.on('ready', function () { + test.equal('Exile (Deluxe)', album.name, 'this should be the Exile (Deluxe) album'); + test.done(); + }); + } }; diff --git a/test/test-080-playlist.js b/test/test-080-playlist.js index fc3c077..e33f530 100644 --- a/test/test-080-playlist.js +++ b/test/test-080-playlist.js @@ -19,6 +19,14 @@ exports.playlist = { test.done(); }, 'getting playlist from url should not throw'); }, + 'get playlist from Starred': function(test) { + var playlist; + test.doesNotThrow(function() { + playlist = session.getStarred() + test.ok(playlist instanceof sp.Playlist, 'We should get a playlist object'); + test.done(); + }, 'getting playlist from starred should not throw'); + }, 'attributes are mapped': function(test) { var playlist = sp.Playlist.getFromUrl('spotify:user:flobyiv:playlist:5ZMnMnJWGXZ9qm4gacHpQF'); playlist.whenReady(function() { @@ -30,7 +38,7 @@ exports.playlist = { }, "getting attributes should not throw"); }); }, - 'get tracks': function(test) { + 'get tracks from URI': function(test) { var playlist = sp.Playlist.getFromUrl('spotify:user:flobyiv:playlist:5ZMnMnJWGXZ9qm4gacHpQF'); playlist.whenReady(function() { playlist.getTracks(function(tracks) { @@ -43,5 +51,32 @@ exports.playlist = { test.done(); }); }); - }.timed(10000) + }.timed(10000), + 'get tracks from Starred': function(test) { + var playlist = session.getStarred(); + playlist.whenReady(function() { + playlist.getTracks(function(tracks) { + test.ok(Array.isArray(tracks), 'tracks should be an array'); + test.ok(tracks.length > 0, 'There should be tracks in the array'); + test.equal(tracks.map(function(e) {return e instanceof sp.Track;}).indexOf(false), -1, 'It should only contain tracks'); + test.equal(tracks.reduce(function(prev, current) { + return prev && current.isReady(); + }, true), true, 'All tracks should be loaded'); + test.done(); + }); + }); + }.timed(40000), + 'get playlist image': function(test) { + var playlist = sp.Playlist.getFromUrl('spotify:user:digster.se:playlist:1jGWX65tQPpwkuqG2OaRCN'); + playlist.whenReady(function () { + var img = playlist.getImage(); + test.ok(img !== null, 'the playlist has an image'); + img.whenReady(function () { + var buffer = img.getData(); + test.ok(buffer instanceof Buffer, 'image data is a buffer'); + test.ok(buffer.length > 0, 'image buffer contains data'); + test.done(); + }); + }); + }, };