From e8a044d016a19d08a7d86d3823d2a27bcf19007e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Mon, 23 Sep 2013 13:03:01 +0200 Subject: [PATCH 1/9] playlistcontainer: folder support --- lib/PlaylistContainer.js | 48 ++++++++++++++++++-- src/playlist.cc | 97 +++++++++++++++++++++++++++++++++++----- 2 files changed, 130 insertions(+), 15 deletions(-) diff --git a/lib/PlaylistContainer.js b/lib/PlaylistContainer.js index 91cf0c0..ad0efe9 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++) { diff --git a/src/playlist.cc b/src/playlist.cc index 17f5ca5..e8ccc3e 100644 --- a/src/playlist.cc +++ b/src/playlist.cc @@ -61,8 +61,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)); } /** @@ -90,23 +158,30 @@ static Handle PlaylistContainer_Playlist(const Arguments& args) { // 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); } 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); } /* @@ -166,7 +241,7 @@ 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)); } @@ -191,10 +266,10 @@ static Handle Playlist_Track(const Arguments& args) { // 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); } @@ -217,7 +292,7 @@ static Handle Playlist_Update_Subscribers(const Arguments& args) { sp_error error = sp_playlist_update_subscribers(session->pointer, playlist->pointer); NSP_THROW_IF_ERROR(error); - + return scope.Close(Undefined()); } From fc8b6c06b2f2a12712d6b5875e628a02fcb1ce68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 26 Sep 2013 07:58:34 +0200 Subject: [PATCH 2/9] starred playlist support --- lib/Session.js | 7 +++++++ src/session.cc | 30 +++++++++++++++++++++++++++--- test/test-080-playlist.js | 24 +++++++++++++++++++++++- 3 files changed, 57 insertions(+), 4 deletions(-) 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/src/session.cc b/src/session.cc index 3bb7da1..c0eaa17 100644 --- a/src/session.cc +++ b/src/session.cc @@ -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 sp_error error = sp_playlistcontainer_add_callbacks(spplaylistcontainer, &nsp_playlistcontainer_callbacks, playlistcontainer); 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-080-playlist.js b/test/test-080-playlist.js index c9e2c51..e5eeca5 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': timed(10000, function(test) { + 'get tracks from URI': timed(10000, function(test) { var playlist = sp.Playlist.getFromUrl('spotify:user:flobyiv:playlist:5ZMnMnJWGXZ9qm4gacHpQF'); playlist.whenReady(function() { playlist.getTracks(function(tracks) { @@ -43,5 +51,19 @@ exports.playlist = { test.done(); }); }); + }), + 'get tracks from Starred': timed(40000, 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(); + }); + }); }) }; From 31a55f3024009b8ad74f5c4e8a655fa51636de2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 26 Sep 2013 09:29:47 +0200 Subject: [PATCH 3/9] search: album and artist support --- lib/Search.js | 20 +++++-- src/search.cc | 88 +++++++++++++++++++++++++++++- test/test-020-search-02-process.js | 33 +++++++++++ 3 files changed, 136 insertions(+), 5 deletions(-) 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/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/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(); + }); +}; From cad54f89dff3ce27e9eac2f54fc3201613fdaf8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 26 Sep 2013 10:33:39 +0200 Subject: [PATCH 4/9] link: album support --- src/link.cc | 45 ++++++++++++++++++++++++++++-- test/test-065-link-types.js | 2 ++ test/test-066-link-from-objects.js | 17 ++++++++--- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/link.cc b/src/link.cc index 947c0ea..93001d4 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 @@ -42,6 +42,24 @@ static Handle Link_Create_From_Track(const Arguments& args) { return scope.Close(String::New(url)); } +static Handle Link_Create_From_Album(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]); + + sp_link* link = sp_link_create_from_album(album->pointer); + char url[256]; + // TODO handle truncated urls + sp_link_as_string(link, url, 256); + + return scope.Close(String::New(url)); +} + static Handle Link_Create_From_Artist(const Arguments& args) { HandleScope scope; @@ -96,6 +114,24 @@ static Handle Link_As_Track(const Arguments& args) { return scope.Close(track->object); } +static Handle Link_As_Album(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 1); + assert(args[0]->IsString()); + + String::Utf8Value url(args[0]); + + sp_link* link = sp_link_create_from_string(*url); + assert(sp_link_type(link) == SP_LINKTYPE_ALBUM); + + ObjectHandle* album = new ObjectHandle("sp_album"); + album->pointer = sp_link_as_album(link); + + return scope.Close(album->object); +} + static Handle Link_As_Artist(const Arguments& args) { HandleScope scope; @@ -132,7 +168,7 @@ static Handle Link_As_Playlist(const Arguments& args) { ObjectHandle* playlist = new ObjectHandle("sp_playlist"); playlist->pointer = sp_playlist_create(session->pointer, link); - + // Add callbacks sp_error error = sp_playlist_add_callbacks(playlist->pointer, &nsp_playlist_callbacks, playlist); NSP_THROW_IF_ERROR(error); @@ -162,6 +198,9 @@ static Handle Link_Type(const Arguments& args) { case SP_LINKTYPE_ARTIST: type = "artist"; break; + case SP_LINKTYPE_ALBUM: + type = "album"; + break; case SP_LINKTYPE_TRACK: type = "track"; break; @@ -177,9 +216,11 @@ static Handle Link_Type(const Arguments& args) { 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_artist", Link_Create_From_Artist); NODE_SET_METHOD(target, "link_create_from_playlist", Link_Create_From_Playlist); 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_playlist", Link_As_Playlist); NODE_SET_METHOD(target, "link_type", Link_Type); 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(); + }); + } }; From a63464d180ab88c07373c190d56c9cd985f06397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 26 Sep 2013 11:29:02 +0200 Subject: [PATCH 5/9] album: getTracks --- binding.gyp | 1 + lib/Album.js | 13 +++++ src/albumbrowse.cc | 102 +++++++++++++++++++++++++++++++++++ src/binding.cc | 1 + src/common.h | 7 +-- test/test-032-albumbrowse.js | 37 +++++++++++++ 6 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 src/albumbrowse.cc create mode 100644 test/test-032-albumbrowse.js diff --git a/binding.gyp b/binding.gyp index f6caa2e..41f916e 100644 --- a/binding.gyp +++ b/binding.gyp @@ -4,6 +4,7 @@ "target_name": "libspotify", "sources": [ "src/album.cc", + "src/albumbrowse.cc", "src/artist.cc", "src/audio.cc", "src/binding.cc", diff --git a/lib/Album.js b/lib/Album.js index 29834ee..c8af14c 100644 --- a/lib/Album.js +++ b/lib/Album.js @@ -83,4 +83,17 @@ Album.prototype.smallCoverImage = function (cb) { this.coverImage(this.IMAGE_SIZ 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.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 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[2]->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/binding.cc b/src/binding.cc index f1484bf..9b8ab90 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -27,6 +27,7 @@ extern "C" { // initializing all modules nsp::init_album(target); + nsp::init_albumbrowse(target); nsp::init_artist(target); nsp::init_link(target); nsp::init_player(target); diff --git a/src/common.h b/src/common.h index 79b8e53..3ed9df9 100644 --- a/src/common.h +++ b/src/common.h @@ -140,6 +140,7 @@ 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 */ @@ -156,7 +157,7 @@ namespace nsp { * init the playlist related functions to the target module exports */ void init_playlist(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 +192,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 +230,7 @@ namespace nsp { object->SetPointerInInternalField(0, this); } - + template ObjectHandle* ObjectHandle::Unwrap(v8::Handle obj) { assert(obj->IsObject()); 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(); + }); + }); + } +} From 453c8775df37528f17d8a39b4570cad0628325c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 26 Sep 2013 10:33:39 +0200 Subject: [PATCH 6/9] link: album support --- src/link.cc | 45 ++++++++++++++++++++++++++++-- test/test-065-link-types.js | 2 ++ test/test-066-link-from-objects.js | 17 ++++++++--- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/link.cc b/src/link.cc index 947c0ea..93001d4 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 @@ -42,6 +42,24 @@ static Handle Link_Create_From_Track(const Arguments& args) { return scope.Close(String::New(url)); } +static Handle Link_Create_From_Album(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]); + + sp_link* link = sp_link_create_from_album(album->pointer); + char url[256]; + // TODO handle truncated urls + sp_link_as_string(link, url, 256); + + return scope.Close(String::New(url)); +} + static Handle Link_Create_From_Artist(const Arguments& args) { HandleScope scope; @@ -96,6 +114,24 @@ static Handle Link_As_Track(const Arguments& args) { return scope.Close(track->object); } +static Handle Link_As_Album(const Arguments& args) { + HandleScope scope; + + // test arguments sanity + assert(args.Length() == 1); + assert(args[0]->IsString()); + + String::Utf8Value url(args[0]); + + sp_link* link = sp_link_create_from_string(*url); + assert(sp_link_type(link) == SP_LINKTYPE_ALBUM); + + ObjectHandle* album = new ObjectHandle("sp_album"); + album->pointer = sp_link_as_album(link); + + return scope.Close(album->object); +} + static Handle Link_As_Artist(const Arguments& args) { HandleScope scope; @@ -132,7 +168,7 @@ static Handle Link_As_Playlist(const Arguments& args) { ObjectHandle* playlist = new ObjectHandle("sp_playlist"); playlist->pointer = sp_playlist_create(session->pointer, link); - + // Add callbacks sp_error error = sp_playlist_add_callbacks(playlist->pointer, &nsp_playlist_callbacks, playlist); NSP_THROW_IF_ERROR(error); @@ -162,6 +198,9 @@ static Handle Link_Type(const Arguments& args) { case SP_LINKTYPE_ARTIST: type = "artist"; break; + case SP_LINKTYPE_ALBUM: + type = "album"; + break; case SP_LINKTYPE_TRACK: type = "track"; break; @@ -177,9 +216,11 @@ static Handle Link_Type(const Arguments& args) { 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_artist", Link_Create_From_Artist); NODE_SET_METHOD(target, "link_create_from_playlist", Link_Create_From_Playlist); 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_playlist", Link_As_Playlist); NODE_SET_METHOD(target, "link_type", Link_Type); 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(); + }); + } }; From 3dbc5cc11796c065a1a161b6f26d586101c1d505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Thu, 26 Sep 2013 23:49:24 +0200 Subject: [PATCH 7/9] artist: getAlbums and getAvailableAlbums --- binding.gyp | 1 + lib/Artist.js | 35 ++++++++++++ src/album.cc | 20 +++++++ src/artistbrowse.cc | 102 ++++++++++++++++++++++++++++++++++ src/binding.cc | 1 + src/common.h | 7 ++- test/test-034-artistbrowse.js | 39 +++++++++++++ 7 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 src/artistbrowse.cc create mode 100644 test/test-034-artistbrowse.js diff --git a/binding.gyp b/binding.gyp index f6caa2e..11e7349 100644 --- a/binding.gyp +++ b/binding.gyp @@ -5,6 +5,7 @@ "sources": [ "src/album.cc", "src/artist.cc", + "src/artistbrowse.cc", "src/audio.cc", "src/binding.cc", "src/link.cc", diff --git a/lib/Artist.js b/lib/Artist.js index 8f75a4d..848141d 100644 --- a/lib/Artist.js +++ b/lib/Artist.js @@ -27,5 +27,40 @@ Artist.prototype.toString = function toString() { return this.name; }; +Artist.prototype.getAlbums = function getAlbums(cb) { + var browser = b.artistbrowse_create(this.getSession()._sp_session, this._sp_object, function () { + albums = new Array(b.artistbrowse_num_albums(browser)); + + for(var i = 0; i 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 */ @@ -196,6 +215,7 @@ static Handle Album_Cover(const Arguments& args) { 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/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/binding.cc b/src/binding.cc index f1484bf..c8b39a0 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -28,6 +28,7 @@ extern "C" { // initializing all modules nsp::init_album(target); nsp::init_artist(target); + nsp::init_artistbrowse(target); nsp::init_link(target); nsp::init_player(target); nsp::init_search(target); diff --git a/src/common.h b/src/common.h index 79b8e53..aa70b55 100644 --- a/src/common.h +++ b/src/common.h @@ -144,6 +144,7 @@ namespace nsp { * 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 */ @@ -156,7 +157,7 @@ namespace nsp { * init the playlist related functions to the target module exports */ void init_playlist(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 +192,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 +230,7 @@ namespace nsp { object->SetPointerInInternalField(0, this); } - + template ObjectHandle* ObjectHandle::Unwrap(v8::Handle obj) { assert(obj->IsObject()); 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(); + }); + }); + } +} From faa019715ab87faa93440708f3b5a7296c689a6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Tue, 1 Oct 2013 16:07:11 +0200 Subject: [PATCH 8/9] images and more link types supported --- binding.gyp | 3 +- lib/Album.js | 52 +++++++- lib/Artist.js | 23 +++- lib/Image.js | 34 ++++++ lib/Player.js | 14 +-- lib/Playlist.js | 60 +++++----- lib/PlaylistContainer.js | 2 +- lib/SpObject.js | 40 ++++--- lib/libspotify.js | 3 +- src/album.cc | 14 +-- src/audio.cc | 10 +- src/audio.h | 18 +-- src/binding.cc | 1 + src/common.h | 10 +- src/image.cc | 77 ++++++++++++ src/imagecallbacks.cc | 32 +++++ src/link.cc | 244 ++++++++++++++++++++++---------------- src/playlist.cc | 79 ++++++++---- src/playlistcallbacks.cc | 8 +- src/session.cc | 8 +- test/test-031-album.js | 18 +-- test/test-035-image.js | 31 +++++ test/test-060-link.js | 14 +++ test/test-080-playlist.js | 15 ++- 24 files changed, 577 insertions(+), 233 deletions(-) create mode 100644 lib/Image.js create mode 100644 src/image.cc create mode 100644 src/imagecallbacks.cc create mode 100644 test/test-035-image.js diff --git a/binding.gyp b/binding.gyp index eb8dffd..11e1dbf 100644 --- a/binding.gyp +++ b/binding.gyp @@ -9,12 +9,13 @@ "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 c8af14c..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,14 +83,45 @@ 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 () { diff --git a/lib/Artist.js b/lib/Artist.js index 848141d..ca307b5 100644 --- a/lib/Artist.js +++ b/lib/Artist.js @@ -14,8 +14,9 @@ function artist_object_type() { return 'artist'; }; Artist.__defineGetter__('_object_type', artist_object_type); Artist.prototype.__defineGetter__('_object_type', artist_object_type); - -Artist.prototype._object_type = 'artist'; +Artist.prototype.IMAGE_SIZE_NORMAL = b.SP_IMAGE_SIZE_NORMAL; +Artist.prototype.IMAGE_SIZE_SMALL = b.SP_IMAGE_SIZE_SMALL; +Artist.prototype.IMAGE_SIZE_LARGE = b.SP_IMAGE_SIZE_LARGE; Artist.prototype._populateAttributes = function _populateAttributes() { this.name = b.artist_name(this._sp_object); @@ -63,4 +64,22 @@ Artist.prototype.getAvailableAlbums = function getAvailableAlbums(cb) { }); }; +Artist.prototype.portraitImageUrl = function portraitImageUrl(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_artist_portrait(this._sp_object, imageSize); +} + module.exports = Artist; diff --git a/lib/Image.js b/lib/Image.js new file mode 100644 index 0000000..de22149 --- /dev/null +++ b/lib/Image.js @@ -0,0 +1,34 @@ +var b = require('bindings')('spotify.node'); +var sp = require('./libspotify'); +var util = require('util'); +var SpObject = require('./SpObject'); + +function Image (sp_image) { + this._sp_object = sp_image; + this._setupNativeCallbacks(); + SpObject.apply(this); +} +util.inherits(Image, SpObject); +Image.__proto__ = SpObject; + +function image_object_type() { return 'image'; }; +Image.__defineGetter__('_object_type', image_object_type); +Image.prototype.__defineGetter__('_object_type', image_object_type); + +Image.prototype.getData = function getData() { + this._readyOrThrow(); + return b.image_data(this._sp_object); +}; + +/** + * C callbacks for spotify events do nothing more than calling their JS equivalent + * here we setup the functions that must be called from C upon these events + */ +Image.prototype._setupNativeCallbacks = function _setupNativeCallbacks() { + var self = this; + this._sp_object.image_loaded = function(data) { + self.emit('ready', data); + }; +}; + +module.exports = Image; diff --git a/lib/Player.js b/lib/Player.js index 74f1bf6..e35807c 100644 --- a/lib/Player.js +++ b/lib/Player.js @@ -9,21 +9,21 @@ function Player (session) { assert(session instanceof sp.Session, "parameter is not a session"); this._session = session; - var self = this; + var self = this; - // Tell spotify that we're ready for more data - this._read = function() { - b.session_player_stream_resume(); - }; + // Tell spotify that we're ready for more data + this._read = function() { + b.session_player_stream_resume(); + }; this._session._sp_session.music_delivery = function(buffer) { - // Readable.push returns whether or not we should push more data, + // Readable.push returns whether or not we should push more data, // so we're returning that value so the underlying code knows what to do return self.push(buffer); }; this._session._sp_session.end_of_track = function() { - self.emit('track-end'); + self.emit('track-end'); }; stream.Readable.call(this); diff --git a/lib/Playlist.js b/lib/Playlist.js index 9885d70..fe2efd5 100644 --- a/lib/Playlist.js +++ b/lib/Playlist.js @@ -19,14 +19,6 @@ function playlist_object_type() { return 'playlist'; }; Playlist.__defineGetter__('_object_type', playlist_object_type); Playlist.prototype.__defineGetter__('_object_type', playlist_object_type); - -Playlist.getFromUrl = function getFromUrl(url) { - if(sp.getLinkType(url) !== 'playlist') { - throw new URIError("Not a playlist URI"); - } - return new this(b.link_as_playlist(url, sp.Session.currentSession._sp_session)); -}; - Playlist.prototype._populateAttributes = function _populateAttributes() { this.name = b.playlist_name(this._sp_object); this.numSubscribers = b.playlist_num_subscribers(this._sp_object); @@ -70,7 +62,22 @@ Playlist.prototype.getTracks = function getTracks(callback) { var checkTracksLoaded = function(numReady, array, callback) { if (numReady >= 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 ad0efe9..12802de 100644 --- a/lib/PlaylistContainer.js +++ b/lib/PlaylistContainer.js @@ -101,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/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 f1e03eb..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; @@ -189,26 +190,25 @@ 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()); } } 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 ec4498e..f4d9d98 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -37,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 e3a1fe9..c27e56d 100644 --- a/src/common.h +++ b/src/common.h @@ -154,10 +154,14 @@ 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 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 93001d4..40a25cc 100644 --- a/src/link.cc +++ b/src/link.cc @@ -18,164 +18,194 @@ #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); - - return scope.Close(String::New(url)); -} - -static Handle Link_Create_From_Album(const Arguments& args) { - HandleScope scope; - - // test arguments sanity - assert(args.Length() == 1); - assert(args[0]->IsObject()); + // gets sp_type pointer from given object + ObjectHandle* obj = ObjectHandle::Unwrap(args[0]); - // gets sp_album pointer from given object - ObjectHandle* album = ObjectHandle::Unwrap(args[0]); + sp_link* link = (*link_fn)(obj->pointer); - sp_link* link = sp_link_create_from_album(album->pointer); - char url[256]; - // TODO handle truncated urls - sp_link_as_string(link, url, 256); + 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)); + return scope.Close(String::New(url)); + } } -static Handle Link_Create_From_Artist(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_artist pointer from given object - ObjectHandle* artist = ObjectHandle::Unwrap(args[0]); + // gets sp_type pointer from given object + ObjectHandle* obj = 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); + 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_Create_From_Playlist(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 assert(args.Length() == 1); - assert(args[0]->IsObject()); + assert(args[0]->IsString()); - // gets sp_playlist pointer from given object - ObjectHandle* playlist = ObjectHandle::Unwrap(args[0]); + String::Utf8Value url(args[0]); + + sp_link* link = sp_link_create_from_string(*url); + assert(sp_link_type(link) == type); - sp_link* link = sp_link_create_from_playlist(playlist->pointer); - char url[256]; - // TODO handle truncated urls - sp_link_as_string(link, url, 256); + ObjectHandle* obj = new ObjectHandle(name); + obj->pointer = (*link_fn)(link); - return scope.Close(String::New(url)); + return scope.Close(obj->object); } -static Handle Link_As_Track(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_TRACK); + 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* track = new ObjectHandle("sp_track"); - track->pointer = sp_link_as_track(link); + // Add callbacks + sp_error error = sp_playlist_add_callbacks(obj->pointer, &nsp_playlist_callbacks, obj); + NSP_THROW_IF_ERROR(error); - return scope.Close(track->object); + return scope.Close(obj->object); } -static Handle Link_As_Album(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(args.Length() == 1); + assert(type == SP_LINKTYPE_IMAGE); + 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_ALBUM); + assert(sp_link_type(link) == type); - ObjectHandle* album = new ObjectHandle("sp_album"); - album->pointer = sp_link_as_album(link); + // 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); - return scope.Close(album->object); + // Add callbacks + sp_error error = sp_image_add_load_callback(obj->pointer, &cb_image_loaded, obj); + NSP_THROW_IF_ERROR(error); + + return scope.Close(obj->object); } -static Handle Link_As_Artist(const Arguments& args) { - HandleScope scope; +static Handle Link_Create_From_Track(const Arguments& args) { + return Link_Create(args, sp_link_create_from_track); +} - // test arguments sanity - assert(args.Length() == 1); - assert(args[0]->IsString()); +static Handle Link_Create_From_Album(const Arguments& args) { + return Link_Create(args, sp_link_create_from_album); +} - String::Utf8Value url(args[0]); +static Handle Link_Create_From_Album_Cover(const Arguments& args) { + return Link_Create(args, sp_link_create_from_album_cover); +} - sp_link* link = sp_link_create_from_string(*url); - assert(sp_link_type(link) == SP_LINKTYPE_ARTIST); +static Handle Link_Create_From_Artist(const Arguments& args) { + return Link_Create(args, sp_link_create_from_artist); +} - ObjectHandle* artist = new ObjectHandle("sp_artist"); - artist->pointer = sp_link_as_artist(link); +static Handle Link_Create_From_Artist_Portrait(const Arguments& args) { + return Link_Create(args, sp_link_create_from_artist_portrait); +} - return scope.Close(artist->object); +static Handle Link_Create_From_Search(const Arguments& args) { + return Link_Create(args, sp_link_create_from_search); } -static Handle Link_As_Playlist(const Arguments& args) { - HandleScope scope; +static Handle Link_Create_From_Playlist(const Arguments& args) { + return Link_Create(args, sp_link_create_from_playlist); +} - // test arguments sanity - assert(args.Length() == 2); - assert(args[0]->IsString()); - assert(args[1]->IsObject()); +static Handle Link_Create_From_User(const Arguments& args) { + return Link_Create(args, sp_link_create_from_user); +} - String::Utf8Value url(args[0]); +static Handle Link_Create_From_Image(const Arguments& args) { + return Link_Create(args, sp_link_create_from_image); +} - sp_link* link = sp_link_create_from_string(*url); - assert(sp_link_type(link) == SP_LINKTYPE_PLAYLIST); +static Handle Link_As_Track(const Arguments& args) { + return Link_As(args, SP_LINKTYPE_TRACK, sp_link_as_track, "sp_track"); +} - // gets sp_session pointer from given object - ObjectHandle* session = ObjectHandle::Unwrap(args[1]); +static Handle Link_As_Album(const Arguments& args) { + return Link_As(args, SP_LINKTYPE_ALBUM, sp_link_as_album, "sp_album"); +} - ObjectHandle* playlist = new ObjectHandle("sp_playlist"); - playlist->pointer = sp_playlist_create(session->pointer, link); +static Handle Link_As_Artist(const Arguments& args) { + return Link_As(args, SP_LINKTYPE_ARTIST, sp_link_as_artist, "sp_artist"); +} - // Add callbacks - sp_error error = sp_playlist_add_callbacks(playlist->pointer, &nsp_playlist_callbacks, playlist); - NSP_THROW_IF_ERROR(error); +static Handle Link_As_User(const Arguments& args) { + return Link_As(args, SP_LINKTYPE_PROFILE, sp_link_as_user, "sp_user"); +} - return scope.Close(playlist->object); +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; @@ -187,29 +217,28 @@ 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_ALBUM: - type = "album"; - 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()); + } } @@ -217,11 +246,18 @@ static Handle Link_Type(const Arguments& args) { 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 e8ccc3e..61cdb87 100644 --- a/src/playlist.cc +++ b/src/playlist.cc @@ -17,6 +17,7 @@ */ #include "common.h" +#include "imagecallbacks.cc" #include "playlistcallbacks.cc" #include @@ -145,13 +146,13 @@ 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)); @@ -166,9 +167,9 @@ static Handle PlaylistContainer_Playlist(const Arguments& args) { playlist->pointer = spplaylist; sp_error error = sp_playlist_add_callbacks(spplaylist, &nsp_playlist_callbacks, playlist); - NSP_THROW_IF_ERROR(error); + NSP_THROW_IF_ERROR(error); - return scope.Close(playlist->object); + return scope.Close(playlist->object); } void nsp::init_playlistcontainer(Handle target) { @@ -221,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)); } @@ -242,7 +243,7 @@ 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)); } /** @@ -256,10 +257,10 @@ 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)); @@ -270,7 +271,7 @@ static Handle Playlist_Track(const Arguments& args) { ObjectHandle* track = new ObjectHandle("sp_track"); track->pointer = sptrack; - return scope.Close(track->object); + return scope.Close(track->object); } /** @@ -284,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); + sp_error error = sp_playlist_update_subscribers(session->pointer, playlist->pointer); + NSP_THROW_IF_ERROR(error); - return scope.Close(Undefined()); + return scope.Close(Undefined()); } /** @@ -306,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/session.cc b/src/session.cc index c0eaa17..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()) { @@ -478,9 +478,9 @@ static Handle Session_PlaylistContainer(const Arguments& args) { 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); } 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-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-080-playlist.js b/test/test-080-playlist.js index 2cb4118..e33f530 100644 --- a/test/test-080-playlist.js +++ b/test/test-080-playlist.js @@ -65,5 +65,18 @@ exports.playlist = { test.done(); }); }); - }.timed(40000) + }.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(); + }); + }); + }, }; From f02ea7b9585d921743ab9592764a7e76b9e3b94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Unneb=C3=A4ck?= Date: Wed, 9 Oct 2013 13:24:05 +0200 Subject: [PATCH 9/9] albumbrowse: bugfix --- src/albumbrowse.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/albumbrowse.cc b/src/albumbrowse.cc index 150965e..e21e84a 100644 --- a/src/albumbrowse.cc +++ b/src/albumbrowse.cc @@ -70,7 +70,7 @@ static Handle AlbumBrowse_Track(const Arguments& args) { // input ObjectHandle *albumbrowse = ObjectHandle::Unwrap(args[0]); - int index = args[2]->ToNumber()->Int32Value(); + int index = args[1]->ToNumber()->Int32Value(); // output sp_track* sptrack = sp_albumbrowse_track(albumbrowse->pointer, index);