├── .gitignore ├── LICENSE ├── README.md ├── examples └── 1 │ ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── packages │ ├── platforms │ ├── release │ └── versions │ ├── client │ ├── app │ │ ├── config.js │ │ ├── home.html │ │ ├── spotify.html │ │ └── spotify.js │ └── layouts │ │ └── applicationLayout.html │ ├── lib │ └── router.js │ ├── packages │ └── meteor-spotify-web-api │ ├── server │ ├── config.js │ └── methods.js │ └── styles.css ├── package.js └── spotify-api.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .npm/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Xinran Xiao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # meteor-spotify-web-api 2 | A meteor wrapper for Spotify's web API via the wonderful [spotify-web-api-node](https://github.com/thelinmichael/spotify-web-api-node). 3 | 4 | Check out an example (examples/1 folder) here: [http://spotify-web-api-example.meteor.com](http://spotify-web-api-example.meteor.com) 5 | 6 | ## Installation 7 | * `meteor add xinranxiao:spotify-web-api` 8 | 9 | ## Usage 10 | 11 | 1) Setup your clientId + clientSecret, either via the `service-configuration` package or directly through the api. 12 | 13 | ```javascript 14 | // This example is via `service-configuration` 15 | ServiceConfiguration.configurations.update( 16 | { "service": "spotify" }, 17 | { 18 | $set: { 19 | "clientId": "", 20 | "secret": "" 21 | } 22 | }, 23 | { upsert: true } 24 | ); 25 | ``` 26 | 27 | 2) Get an oauth `access_token`, either through [`xinranxiao:accounts-spotify`](https://github.com/xinranxiao/meteor-accounts-spotify), or directly through this API (refer [here](https://github.com/thelinmichael/spotify-web-api-node). 28 | ) for how). 29 | 30 | 3) Make a new instance of the API and use it! Currently only available on the server. 31 | 32 | ```javascript 33 | var spotifyApi = new SpotifyWebApi(); 34 | 35 | // credentials are optional 36 | var spotifyApi = new SpotifyWebApi({ 37 | clientId : 'fcecfc72172e4cd267473117a17cbd4d', 38 | clientSecret : 'a6338157c9bb5ac9c71924cb2940e1a7', 39 | redirectUri : 'http://www.example.com/callback' 40 | }); 41 | 42 | ``` 43 | 44 | ```javascript 45 | // Get Elvis' albums `synchronously` on the server. 46 | var response = spotifyApi.getArtistAlbums('43ZHCT0cAZBISjO8DG9PnE'); 47 | console.log(response); 48 | 49 | // Or use a classic callback approach. 50 | // Note that the OPTIONS parameter is always required for this format! 51 | spotifyApi.getArtistAlbums('43ZHCT0cAZBISjO8DG9PnE', {}, function(err, result) { 52 | console.log(result); 53 | }); 54 | ``` 55 | 56 | 4) Your `access_token` will expire at some point. 57 | ```javascript 58 | // Just refresh the token and manually deal with the response. 59 | // response contains the new access_token and the new expire_in 60 | var response = spotifyApi.refreshAccessToken(); 61 | 62 | // Refresh the access token, update the current instance with the token, 63 | // and update the user's credentials as well. 64 | spotifyApi.refreshAndUpdateAccessToken(); // All done here. 65 | ``` 66 | 67 | Currently, this package automatically sets the `clientId` and `clientSecret` if you have `service-configuration` configured. It also sets `accessToken` and `refreshToken` if you have a user connected with Spotify via `xinranxiao:accounts-spotify` (this won't work if you have your code inside a publication). 68 | 69 | ## Contribution 70 | 71 | If you have any problems with or suggestions for this package, please create a new issue. 72 | -------------------------------------------------------------------------------- /examples/1/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | -------------------------------------------------------------------------------- /examples/1/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/1/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | hofzmaaseqth1ym0o2j 8 | -------------------------------------------------------------------------------- /examples/1/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-platform 8 | autopublish 9 | insecure 10 | xinranxiao:spotify-web-api 11 | xinranxiao:accounts-spotify 12 | semantic:ui-css 13 | sergeyt:typeahead 14 | useraccounts:semantic-ui 15 | raix:handlebar-helpers 16 | accounts-ui 17 | accounts-base 18 | -------------------------------------------------------------------------------- /examples/1/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/1/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.1.0.2 2 | -------------------------------------------------------------------------------- /examples/1/.meteor/versions: -------------------------------------------------------------------------------- 1 | accounts-base@1.2.0 2 | accounts-oauth@1.1.5 3 | accounts-ui@1.1.5 4 | accounts-ui-unstyled@1.1.7 5 | autopublish@1.0.3 6 | autoupdate@1.2.1 7 | base64@1.0.3 8 | binary-heap@1.0.3 9 | blaze@2.1.2 10 | blaze-tools@1.0.3 11 | boilerplate-generator@1.0.3 12 | callback-hook@1.0.3 13 | check@1.0.5 14 | coffeescript@1.0.6 15 | ddp@1.1.0 16 | deps@1.0.7 17 | ejson@1.0.6 18 | fastclick@1.0.3 19 | geojson-utils@1.0.3 20 | html-tools@1.0.4 21 | htmljs@1.0.4 22 | http@1.1.0 23 | id-map@1.0.3 24 | insecure@1.0.3 25 | iron:controller@1.0.8 26 | iron:core@1.0.8 27 | iron:dynamic-template@1.0.8 28 | iron:layout@1.0.8 29 | iron:location@1.0.9 30 | iron:middleware-stack@1.0.9 31 | iron:router@1.0.9 32 | iron:url@1.0.9 33 | jquery@1.11.3_2 34 | json@1.0.3 35 | launch-screen@1.0.2 36 | less@1.0.14 37 | livedata@1.0.13 38 | localstorage@1.0.3 39 | logging@1.0.7 40 | meteor@1.1.6 41 | meteor-platform@1.2.2 42 | minifiers@1.1.5 43 | minimongo@1.0.8 44 | mobile-status-bar@1.0.3 45 | mongo@1.1.0 46 | oauth@1.1.4 47 | oauth2@1.1.3 48 | observe-sequence@1.0.6 49 | ordered-dict@1.0.3 50 | raix:handlebar-helpers@0.2.4 51 | random@1.0.3 52 | reactive-dict@1.1.0 53 | reactive-var@1.0.5 54 | reload@1.1.3 55 | retry@1.0.3 56 | routepolicy@1.0.5 57 | semantic:ui-css@1.12.3 58 | sergeyt:typeahead@0.11.1_1 59 | service-configuration@1.0.4 60 | session@1.1.0 61 | softwarerero:accounts-t9n@1.0.9 62 | spacebars@1.0.6 63 | spacebars-compiler@1.0.6 64 | templating@1.1.1 65 | tracker@1.0.7 66 | ui@1.0.6 67 | underscore@1.0.3 68 | url@1.0.4 69 | useraccounts:core@1.11.1 70 | useraccounts:semantic-ui@1.11.1 71 | webapp@1.2.0 72 | webapp-hashing@1.0.3 73 | xinranxiao:accounts-spotify@1.0.2 74 | xinranxiao:spotify@1.0.2 75 | xinranxiao:spotify-web-api@1.0.1 76 | -------------------------------------------------------------------------------- /examples/1/client/app/config.js: -------------------------------------------------------------------------------- 1 | var scopes = ['playlist-modify-private', 'user-library-read','user-follow-read', 'playlist-read-private']; 2 | Accounts.ui.config({'requestPermissions':{'spotify':scopes}}); -------------------------------------------------------------------------------- /examples/1/client/app/home.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /examples/1/client/app/spotify.html: -------------------------------------------------------------------------------- 1 | 85 | 86 | -------------------------------------------------------------------------------- /examples/1/client/app/spotify.js: -------------------------------------------------------------------------------- 1 | Template.spotify.onRendered(function() { 2 | // Initialize the typeahead DOM element. 3 | Meteor.typeahead.inject(); 4 | 5 | // We don't have playlists initially. 6 | Session.set('selectedTracks', []); 7 | 8 | // Get the counts. 9 | Meteor.call('getFollowerCount', function(err, count) { 10 | Session.set('followerCount', count); 11 | }); 12 | Meteor.call('getSavedTracksCount', function(err, count) { 13 | Session.set('trackCount', count); 14 | }); 15 | Meteor.call('getSavedPlaylists', function(err, response) { 16 | console.log(response); 17 | Session.set('playlistCount', response.total); 18 | Session.set('currentPlaylists', response.items); 19 | }); 20 | }); 21 | 22 | Template.spotify.helpers({ 23 | notEmpty: function() { 24 | return Session.get('selectedTracks') && Session.get('selectedTracks').length > 0; 25 | }, 26 | displayName: function() { 27 | if (Meteor.user()) return Meteor.user().profile.display_name; 28 | }, 29 | getTracks: function (query, sync, callback) { 30 | // Show loading. 31 | $('#spotifyTrackSearch').api('set loading'); 32 | 33 | Meteor.call('typeaheadTracks', query, {}, function(err, tracks) { 34 | // Not loading anymore. 35 | $('#spotifyTrackSearch').api('remove loading'); 36 | 37 | // Check for error. 38 | if (err) { 39 | $('#spotifyTrackSearch').api('set error'); 40 | alert("Something absolutely terrible happened. Here's some of the nonsense: " + err.toString()); 41 | return; 42 | } 43 | 44 | // Update the typeahead input field. 45 | callback(tracks); 46 | }); 47 | }, 48 | onTrackSelected: function(e, suggestion, dataset) { 49 | var selectedTracks = Session.get('selectedTracks'); 50 | selectedTracks.push(suggestion); 51 | Session.set('selectedTracks', selectedTracks); // Session is only reactive on *set* being called. 52 | } 53 | }); 54 | 55 | Template.spotify.events({ 56 | 'click #createPlaylistButton': function(e, template) { 57 | var playlistName = $('#playlistName').val(); 58 | if (playlistName.length <= 0) { 59 | // Check existence of the playlist name. 60 | return alert("Set playlist name first!"); 61 | } else { 62 | // Call method to create the playlist. 63 | Meteor.call('createPlaylist', Session.get('selectedTracks'), playlistName, function(err, playlist) { 64 | if (err) { 65 | alert("Unable to create playlist: " + err); 66 | } else { 67 | alert("Playlist created!"); 68 | 69 | // Update current playlists. 70 | var currPlaylists = Session.get('currentPlaylists'); 71 | currPlaylists.push(playlist); 72 | Session.set('currentPlaylists', currPlaylists); 73 | 74 | // Update playlist count. 75 | var playlistCount = Session.get('playlistCount'); 76 | Session.set('playlistCount', ++playlistCount); 77 | } 78 | }); 79 | } 80 | }, 81 | 'click #resetPlaylistButton': function(e, template) { 82 | Session.set('selectedTracks', []); 83 | } 84 | }); -------------------------------------------------------------------------------- /examples/1/client/layouts/applicationLayout.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /examples/1/lib/router.js: -------------------------------------------------------------------------------- 1 | Router.configure({ 2 | layoutTemplate: 'applicationLayout' 3 | }); 4 | 5 | Router.plugin('ensureSignedIn', { 6 | except: ['homepage', 'atSignIn', 'atSignUp', 'atForgotPassword'] 7 | }); 8 | 9 | Router.route('/', { 10 | name: 'homepage', 11 | onBeforeAction: function() { 12 | if (Meteor.user()) this.redirect('/spotify'); 13 | else this.next(); 14 | } 15 | }); 16 | 17 | Router.route('/spotify', { 18 | name: 'spotify' 19 | }); -------------------------------------------------------------------------------- /examples/1/packages/meteor-spotify-web-api: -------------------------------------------------------------------------------- 1 | ../../../../meteor-spotify-web-api/ -------------------------------------------------------------------------------- /examples/1/server/config.js: -------------------------------------------------------------------------------- 1 | ServiceConfiguration.configurations.update( 2 | { "service": "spotify" }, 3 | { 4 | $set: { 5 | "clientId": "", 6 | "secret": "" 7 | } 8 | }, 9 | { upsert: true } 10 | ); 11 | 12 | -------------------------------------------------------------------------------- /examples/1/server/methods.js: -------------------------------------------------------------------------------- 1 | Meteor.methods({ 2 | typeaheadTracks: function(query, options) { 3 | options = options || {}; 4 | 5 | // guard against client-side DOS: hard limit to 50 6 | if (options.limit) { 7 | options.limit = Math.min(6, Math.abs(options.limit)); 8 | } else { 9 | options.limit = 6; 10 | } 11 | 12 | // Spotify call. 13 | var spotifyApi = new SpotifyWebApi(); 14 | var response = spotifyApi.searchTracks(query, { limit: options.limit }); 15 | 16 | // Need to refresh token 17 | if (checkTokenRefreshed(response, spotifyApi)) { 18 | response = spotifyApi.searchTracks(query, { limit: options.limit }); 19 | } 20 | 21 | return response.data.body.tracks.items; 22 | }, 23 | createPlaylist: function(selectedTracks, playlistName) { 24 | if (!selectedTracks || !playlistName || selectedTracks.length > 20) throw new Error("No tracks or playlist name specified"); 25 | 26 | // Call 27 | var spotifyApi = new SpotifyWebApi(); 28 | var response = spotifyApi.createPlaylist(Meteor.user().services.spotify.id, playlistName, { public: false }); 29 | 30 | // Need to refresh token 31 | if (checkTokenRefreshed(response, spotifyApi)) { 32 | response = spotifyApi.createPlaylist(Meteor.user().services.spotify.id, playlistName, { public: false }); 33 | } 34 | 35 | // Put songs into the playlist. 36 | var uris = selectedTracks.map(function(track) { 37 | return track.uri; 38 | }); 39 | spotifyApi.addTracksToPlaylist(Meteor.user().services.spotify.id, response.data.body.id, uris, {}); 40 | 41 | return response.data.body; 42 | }, 43 | getFollowerCount: function() { 44 | var spotifyApi = new SpotifyWebApi(); 45 | var response = spotifyApi.getMe(); 46 | if (checkTokenRefreshed(response, spotifyApi)) { 47 | response = spotifyApi.getMySavedTracks({}); 48 | } 49 | 50 | return response.data.body.followers.total; 51 | 52 | }, 53 | getSavedTracksCount: function() { 54 | var spotifyApi = new SpotifyWebApi(); 55 | var response = spotifyApi.getMySavedTracks({}); 56 | if (checkTokenRefreshed(response, spotifyApi)) { 57 | response = spotifyApi.getMySavedTracks({}); 58 | } 59 | 60 | return response.data.body.total; 61 | }, 62 | getSavedPlaylists: function() { 63 | var spotifyApi = new SpotifyWebApi(); 64 | var response = spotifyApi.getUserPlaylists(Meteor.user().services.spotify.id, {}); 65 | 66 | if (checkTokenRefreshed(response, spotifyApi)) { 67 | response = spotifyApi.getUserPlaylists(Meteor.user().services.spotify.id, {}); 68 | } 69 | 70 | return response.data.body; 71 | } 72 | }); 73 | 74 | var checkTokenRefreshed = function(response, api) { 75 | if (response.error && response.error.statusCode === 401) { 76 | api.refreshAndUpdateAccessToken(); 77 | return true; 78 | } else { 79 | return false; 80 | } 81 | } -------------------------------------------------------------------------------- /examples/1/styles.css: -------------------------------------------------------------------------------- 1 | /* CSS declarations go here */ 2 | 3 | .typeahead, 4 | .tt-query, 5 | .tt-hint { 6 | width: 396px; 7 | padding: 8px 12px; 8 | line-height: 30px; 9 | border: 2px solid #ccc; 10 | -webkit-border-radius: 8px; 11 | -moz-border-radius: 8px; 12 | border-radius: 8px; 13 | outline: none; 14 | } 15 | 16 | .typeahead { 17 | background-color: #fff; 18 | } 19 | 20 | .typeahead:focus { 21 | border: 2px solid #0097cf; 22 | } 23 | 24 | .tt-query { 25 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 26 | -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 27 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); 28 | } 29 | 30 | .tt-hint { 31 | color: #999 32 | } 33 | 34 | .tt-menu { 35 | width: 422px; 36 | margin: 4px 0; 37 | padding: 8px 0; 38 | background-color: #fff; 39 | border: 1px solid #ccc; 40 | border: 1px solid rgba(0, 0, 0, 0.2); 41 | -webkit-border-radius: 8px; 42 | -moz-border-radius: 8px; 43 | border-radius: 8px; 44 | -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); 45 | -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); 46 | box-shadow: 0 5px 10px rgba(0,0,0,.2); 47 | } 48 | 49 | .tt-suggestion { 50 | padding: 3px 20px; 51 | } 52 | 53 | .tt-suggestion:hover { 54 | cursor: pointer; 55 | color: #fff; 56 | background-color: #0097cf; 57 | } 58 | 59 | .tt-suggestion.tt-cursor { 60 | color: #fff; 61 | background-color: #0097cf; 62 | 63 | } 64 | 65 | .tt-suggestion p { 66 | margin: 0; 67 | } 68 | 69 | body { 70 | background: url('https://ununsplash.imgix.net/uploads/141362941583982a7e0fc/abcfbca1?q=75&fm=jpg&s=5266baf09e0e878b72b2e34adf2f54a0') fixed; 71 | background-size: cover; 72 | padding: 0; 73 | margin: 0; 74 | } 75 | 76 | .form-holder { 77 | background: rgba(255,255,255,0.2); 78 | margin-top: 10%; 79 | border-radius: 3px; 80 | } 81 | 82 | .form-head { 83 | font-size: 30px; 84 | letter-spacing: 2px; 85 | text-transform: uppercase; 86 | color: #fff; 87 | text-shadow: 0 0 30px #000; 88 | margin: 15px auto 30px auto; 89 | } 90 | 91 | .remember-me { 92 | text-align: left; 93 | } 94 | .ui.checkbox label { 95 | color: #ddd; 96 | } 97 | 98 | .ui.segment.at-form { 99 | box-shadow: none; 100 | background-color: transparent; 101 | } 102 | 103 | #at-spotify { 104 | background-color: #84BD00; 105 | } 106 | 107 | .ui.main { 108 | padding-top:50px; 109 | } 110 | 111 | #mainSegment { 112 | box-shadow: none; 113 | border-radius: 0px; 114 | padding: 50px; 115 | background-color: #ECEBE8; 116 | } -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'xinranxiao:spotify-web-api', 3 | version: '1.0.3', 4 | summary: 'A wrapper for the Spotify Web API', 5 | git: 'https://github.com/xinranxiao/meteor-spotify-web-api.git', 6 | documentation: 'README.md' 7 | }); 8 | 9 | Package.onUse(function(api) { 10 | api.versionsFrom('1.0'); 11 | 12 | api.use('service-configuration', ['server']); 13 | api.use(['underscore'], ['client', 'server']); 14 | 15 | api.imply('service-configuration', ['server']); 16 | 17 | api.export('SpotifyWebApi'); 18 | 19 | api.addFiles('spotify-api.js', 'server'); 20 | }); 21 | 22 | Npm.depends({ 'spotify-web-api-node': '2.3.0'}); 23 | -------------------------------------------------------------------------------- /spotify-api.js: -------------------------------------------------------------------------------- 1 | var Future = Npm.require("fibers/future"); 2 | 3 | SpotifyWebApi = function(config) { 4 | config = config || {}; 5 | var SpotifyWebApi = Npm.require('spotify-web-api-node'); 6 | var api = new SpotifyWebApi(config); 7 | 8 | // Set the access token + refresh token (either provided, or retrieved from account) 9 | setAccessTokens(api, config); 10 | 11 | // Create a refresh method that updates everything after the refresh. 12 | api.refreshAndUpdateAccessToken = function(callback) { 13 | var response = api.refreshAccessToken(); 14 | 15 | if (response.error) { 16 | callback(response.error, null); 17 | } else { 18 | // Update the current API instance 19 | api.setAccessToken(response.data.body.access_token); 20 | 21 | // Update the current user (if available) 22 | if (Meteor.userId()) { 23 | Meteor.users.update({ _id: Meteor.userId() }, { $set: { 24 | 'services.spotify.accessToken': response.data.body.access_token, 25 | 'services.spotify.expiresAt': (+new Date) + (1000 * response.data.body.expires_in) 26 | }}); 27 | } 28 | 29 | callback(null, response); 30 | } 31 | } 32 | 33 | // Whitelist functions to be wrapped. This is ugly -- any alternatives? 34 | SpotifyWebApi.whitelistedFunctionNames = ['refreshAndUpdateAccessToken','getTrack','getTracks','getAlbum', 35 | 'getAlbums','getArtist','getArtists','searchAlbums', 36 | 'searchArtists','searchTracks','searchPlaylists','getArtistAlbums','getAlbumTracks','getArtistTopTracks', 37 | 'getArtistRelatedArtists','getUser','getMe','getUserPlaylists','getPlaylist','getPlaylistTracks','createPlaylist', 38 | 'followPlaylist','unfollowPlaylist','changePlaylistDetails','addTracksToPlaylist','removeTracksFromPlaylist', 39 | 'removeTracksFromPlaylistByPosition','replaceTracksInPlaylist','reorderTracksInPlaylist','clientCredentialsGrant', 40 | 'authorizationCodeGrant','refreshAccessToken','getMySavedTracks','containsMySavedTracks', 41 | 'removeFromMySavedTracks','addToMySavedTracks','followUsers','followArtists','unfollowUsers','unfollowArtists', 42 | 'isFollowingUsers','areFollowingPlaylist','isFollowingArtists', 'getFollowedArtists', 'getNewReleases','getFeaturedPlaylists', 43 | 'getCategories','getCategory','getPlaylistsForCategory', 'removeFromMySavedAlbums', 'addToMySavedAlbums', 'getMySavedAlbums', 'containsMySavedAlbums', 44 | 'getAudioFeaturesForTrack', 'getAudioFeaturesForTracks', 'getRecommendations', 'getAvailableGenreSeeds']; 45 | 46 | // Wrap all the functions to be able to be called synchronously on the server. 47 | _.each(SpotifyWebApi.whitelistedFunctionNames, function(functionName) { 48 | var fn = api[functionName]; 49 | if (_.isFunction(fn)) { 50 | api[functionName] = wrapAsync(fn, api); 51 | } 52 | }); 53 | 54 | return api; 55 | }; 56 | 57 | /* 58 | This is exactly the same as Meteor.wrapAsync except it properly returns the error. 59 | credit goes to @faceyspacey -- https://github.com/meteor/meteor/issues/2774#issuecomment-70782092 60 | */ 61 | var wrapAsync = function(fn, context) { 62 | return function (/* arguments */) { 63 | var self = context || this; 64 | var newArgs = _.toArray(arguments); 65 | var callback; 66 | 67 | for (var i = newArgs.length - 1; i >= 0; --i) { 68 | var arg = newArgs[i]; 69 | var type = typeof arg; 70 | if (type !== "undefined") { 71 | if (type === "function") { 72 | callback = arg; 73 | } 74 | break; 75 | } 76 | } 77 | 78 | if(!callback) { 79 | var fut = new Future(); 80 | callback = function(error, data) { 81 | fut.return({error: error, data: data}); 82 | }; 83 | 84 | ++i; 85 | } 86 | 87 | newArgs[i] = Meteor.bindEnvironment(callback); 88 | var result = fn.apply(self, newArgs); 89 | return fut ? fut.wait() : result; 90 | }; 91 | }; 92 | 93 | var setAccessTokens = function(api, config) { 94 | var serviceConfiguration = ServiceConfiguration.configurations.findOne({service: 'spotify'}); 95 | if (config.clientId && config.clientSecret) { 96 | api.setClientId(config.clientId); 97 | api.setClientSecret(config.clientSecret); 98 | } else if (serviceConfiguration) { 99 | api.setClientId(serviceConfiguration.clientId); 100 | api.setClientSecret(serviceConfiguration.secret); 101 | } else { 102 | throw new Error("No clientId/secret found. Please configure the `service-configuration` package."); 103 | } 104 | 105 | if (config.accessToken) { 106 | api.setAccessToken(config.accessToken); 107 | if (config.refreshToken) { 108 | api.setRefreshToken(config.refreshToken); 109 | } 110 | } else { 111 | var currUser = Meteor.user(); 112 | if (currUser && currUser.services && currUser.services.spotify && currUser.services.spotify.accessToken) { 113 | api.setAccessToken(currUser.services.spotify.accessToken); 114 | if (currUser.services.spotify.refreshToken) { 115 | api.setRefreshToken(currUser.services.spotify.refreshToken); 116 | } 117 | } 118 | } 119 | } 120 | --------------------------------------------------------------------------------