├── LICENSE ├── README.markdown ├── example ├── index.html └── js │ ├── example.js │ └── jquery.tmpl.js └── jquery.sc.api.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 SoundCloud Ltd. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## jQuery plugin: SoundCloud oAuth 2.0 API wrapper 2 | 3 | ### A simple usage example 4 | 5 | First and most important, you'll need to get a Client ID from SoundCloud. If 6 | you haven't got one already, just register an app on the SoundCloud [Apps][] page. 7 | 8 | [apps]: http://soundcloud.com/you/apps/new 9 | 10 | Include the plugin in your HTML code: 11 | 12 | ```html 13 | 14 | ``` 15 | 16 | and then, initialize it: 17 | 18 | ```javascript 19 | var api = $.sc.api('Enter your Client ID here'); 20 | ``` 21 | 22 | or handle the successful authorization yourself: 23 | 24 | ```javascript 25 | var api = $.sc.api('Enter your Client ID here', { 26 | onAuthSuccess: function(user, container) { 27 | alert('you are SoundCloud user ' + user.username); 28 | } 29 | }); 30 | ``` 31 | 32 | also instead of passing the callbacks you can use the custom events: 33 | 34 | ```javascript 35 | var api = $.sc.api('Enter your Client ID here'); 36 | $(document).bind($.sc.api.events.AuthSuccess, function(event) { 37 | var user = event.user; 38 | // do something with the user object or call the api 39 | api.get('/me/tracks', function(tracks) { 40 | console.log(tracks); 41 | }) 42 | }); 43 | ``` 44 | 45 | Please refer to the [wiki][] for full documentation. 46 | 47 | [wiki]: https://github.com/soundcloud/SoundCloud-API-jQuery-plugin/wiki 48 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 |
7 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /example/js/example.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | var soundCloudApiKey = 'Enter your Client ID here'; 3 | var api = $.sc.api(soundCloudApiKey, { 4 | onAuthSuccess: function(user, container) { 5 | $('Logged in as: ' + user.username + '').prependTo(container); 6 | console.log('you are SoundCloud user ' + user.username); 7 | } 8 | }); 9 | 10 | // wait for the API to be available 11 | $(document).bind($.sc.api.events.AuthSuccess, function(event) { 12 | var user = event.user; 13 | // call the api 14 | api.get('/me/tracks', function(data) { 15 | console.log('and here are your tracks', data); 16 | // you can use new jQuery templating for generating the track list 17 | $('#track').render(data).appendTo("#track-list"); 18 | }); 19 | }); 20 | 21 | 22 | })(jQuery); 23 | -------------------------------------------------------------------------------- /example/js/jquery.tmpl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Templating Plugin 3 | * NOTE: Created for demonstration purposes. 4 | * Copyright 2010, John Resig 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | */ 7 | (function(jQuery){ 8 | // Override the DOM manipulation function 9 | var oldManip = jQuery.fn.domManip; 10 | 11 | jQuery.fn.extend({ 12 | render: function( data ) { 13 | return this.map(function(i, tmpl){ 14 | return jQuery.render( tmpl, data ); 15 | }); 16 | }, 17 | 18 | // This will allow us to do: .append( "template", dataObject ) 19 | domManip: function( args ) { 20 | // This appears to be a bug in the appendTo, etc. implementation 21 | // it should be doing .call() instead of .apply(). See #6227 22 | if ( args.length > 1 && args[0].nodeType ) { 23 | arguments[0] = [ jQuery.makeArray(args) ]; 24 | } 25 | 26 | if ( args.length === 2 && typeof args[0] === "string" && typeof args[1] !== "string" ) { 27 | arguments[0] = [ jQuery.render( args[0], args[1] ) ]; 28 | } 29 | 30 | return oldManip.apply( this, arguments ); 31 | } 32 | }); 33 | 34 | jQuery.extend({ 35 | render: function( tmpl, data ) { 36 | var fn; 37 | 38 | // Use a pre-defined template, if available 39 | if ( jQuery.templates[ tmpl ] ) { 40 | fn = jQuery.templates[ tmpl ]; 41 | 42 | // We're pulling from a script node 43 | } else if ( tmpl.nodeType ) { 44 | var node = tmpl, elemData = jQuery.data( node ); 45 | fn = elemData.tmpl || jQuery.tmpl( node.innerHTML ); 46 | } 47 | 48 | fn = fn || jQuery.tmpl( tmpl ); 49 | 50 | // We assume that if the template string is being passed directly 51 | // in the user doesn't want it cached. They can stick it in 52 | // jQuery.templates to cache it. 53 | 54 | if ( jQuery.isArray( data ) ) { 55 | return jQuery.map( data, function( data, i ) { 56 | return fn.call( data, jQuery, data, i ); 57 | }); 58 | 59 | } else { 60 | return fn.call( data, jQuery, data, 0 ); 61 | } 62 | }, 63 | 64 | // You can stick pre-built template functions here 65 | templates: {}, 66 | 67 | /* 68 | * For example, someone could do: 69 | * jQuery.templates.foo = jQuery.tmpl("some long templating string"); 70 | * $("#test").append("foo", data); 71 | */ 72 | 73 | tmplcmd: { 74 | "each": { 75 | _default: [ null, "$i" ], 76 | prefix: "jQuery.each($1,function($2){with(this){", 77 | suffix: "}});" 78 | }, 79 | "if": { 80 | prefix: "if($1){", 81 | suffix: "}" 82 | }, 83 | "else": { 84 | prefix: "}else{" 85 | }, 86 | "html": { 87 | prefix: "_.push(typeof $1==='function'?$1.call(this):$1);" 88 | }, 89 | "=": { 90 | _default: [ "this" ], 91 | prefix: "_.push($.encode(typeof $1==='function'?$1.call(this):$1));" 92 | } 93 | }, 94 | 95 | encode: function( text ) { 96 | return text != null ? document.createTextNode( text.toString() ).nodeValue : ""; 97 | }, 98 | 99 | tmpl: function(str, data, i) { 100 | // Generate a reusable function that will serve as a template 101 | // generator (and which will be cached). 102 | var fn = new Function("jQuery","$data","$i", 103 | "var $=jQuery,_=[];_.data=$data;_.index=$i;" + 104 | 105 | // Introduce the data as local variables using with(){} 106 | "with($data){_.push('" + 107 | 108 | // Convert the template into pure JavaScript 109 | str 110 | .replace(/[\r\t\n]/g, " ") 111 | .replace(/\${([^}]*)}/g, "{{= $1}}") 112 | .replace(/{{(\/?)(\w+|.)(?:\((.*?)\))?(?: (.*?))?}}/g, function(all, slash, type, fnargs, args) { 113 | var tmpl = jQuery.tmplcmd[ type ]; 114 | 115 | if ( !tmpl ) { 116 | throw "Template not found: " + type; 117 | } 118 | 119 | var def = tmpl._default; 120 | 121 | return "');" + tmpl[slash ? "suffix" : "prefix"] 122 | .split("$1").join(args || def[0]) 123 | .split("$2").join(fnargs || def[1]) + "_.push('"; 124 | }) 125 | + "');}return $(_.join('')).get();"); 126 | 127 | // Provide some basic currying to the user 128 | return data ? fn.call( this, jQuery, data, i ) : fn; 129 | } 130 | }); 131 | })(jQuery); 132 | -------------------------------------------------------------------------------- /jquery.sc.api.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SoundCloud API wrapper for jQuery using oAuth2 3 | * Author: Matas Petrikas, matas@soundcloud.com 4 | * Copyright (c) 2010 SoundCloud Ltd. 5 | * Licensed under the MIT license: 6 | * http://www.opensource.org/licenses/mit-license.php 7 | */ 8 | 9 | // TODOs 10 | // implement HTML5 uplaod flow 11 | // encrypt the security token in the localStorage 12 | 13 | (function($) { 14 | $.sc = $.sc || {}; 15 | 16 | // api wrapper event dictionary 17 | var events = { 18 | AuthSuccess : 'onScAuth', 19 | AuthDenied : 'onScAuthDenied', 20 | AuthError : 'onScAuthError', 21 | NonAuth : 'onScNotAuth', 22 | ApiCall : 'onScApiCall', 23 | ApiError : 'onScApiError', 24 | UnSupported: 'onScApiUnsupported' 25 | }; 26 | 27 | $.sc.api = function(apiKey, callerSettings) { 28 | var settings = $.extend({ 29 | debug: true, // if enabled, will print most errors into browser console 30 | useSandbox: false, // set it to true, if you're working on your code, revert to false for live deploy 31 | host: this.useSandbox ? 'sandbox-soundcloud.com' : 'soundcloud.com', 32 | redirect: window.location.href.replace(/(\?|#).*$/, ''), // redirect after authorization, default is the current page 33 | container: $('
').prependTo(document.body), // the DOM node where the Auth status is displayed 34 | authRequired: true, // set to false if you need only public resources 35 | reAuthorize: true, // if true, wil try to re-authorize on expired token 36 | onAuthSuccess: function(user, container) { // called when valid token is detected 37 | $('Logged in as: ' + user.username + '').prependTo(container); 38 | }, 39 | onAuthDeny: function(error) { // called when user denied the authorization 40 | }, 41 | onNonAuth: function(connectUrl, container) { // called when no valid token is found 42 | $('', { 43 | html: 'Connect to SoundCloud', 44 | href: connectUrl, 45 | 'class': 'sc-authorize' 46 | }) 47 | .prependTo(container); 48 | }, 49 | onApiError: function(xhr, status, error) { // called when API returns an error 50 | } 51 | }, callerSettings || {}), 52 | log = function(args) { 53 | if(settings.debug && console && 'log' in console){ 54 | console.log.apply(console, arguments); 55 | } 56 | }, 57 | // trigger custom events on certain methods 58 | anounceEvent = function(eventType, obj) { 59 | $(document).trigger({type: eventType, scObj : obj}); 60 | }, 61 | // utility for parsing params from query strings 62 | getParamInQuery = function(path, param) { 63 | var match = path.match(new RegExp("(?:\\?|&|#)" + param + "=([^&]*)(?:&|$)")); 64 | if (match) { 65 | return decodeURIComponent(match[1]); 66 | } 67 | }, 68 | // API authorization token 69 | token, 70 | tokenId = 'scAuth', 71 | removeToken = function() { 72 | // remove the token from the client 73 | localStorage.removeItem(tokenId); 74 | }, 75 | // reading token from the location.hash or localStorage 76 | readToken = function() { 77 | var rToken = getParamInQuery(window.location.hash, 'access_token'), 78 | error = getParamInQuery(window.location.href, 'error'), 79 | now = new Date().getTime(), 80 | // for now, token is valid for one hour 81 | tokenLife = 3600, 82 | ls = window.localStorage, 83 | tokenObj; 84 | // in case we're returning from the SoundCloud connect authorization page, store the token in the localStorage 85 | if(rToken){ 86 | // clear the token from the hash 87 | window.location.hash = ''; 88 | // store token 89 | ls.setItem(tokenId, JSON.stringify({token: rToken, host: settings.host, date: now})); 90 | }else if(error && error === 'user_denied'){ 91 | // remove some eventual old token 92 | removeToken(); 93 | // if user denied access, let the DOM know about it 94 | log('getting token: user denied', arguments); 95 | settings.onAuthDeny(error); 96 | anounceEvent(events.AuthDenied, {host: settings.host}); 97 | }else if(error && error === 'invalid_client'){ 98 | log('getting token: wrong API key', arguments); 99 | throw "API key error, please make sure the key you are using is valid!"; 100 | } 101 | // TODO potentialy some other errors could be handled here too 102 | // read the token from the localStorage 103 | tokenObj = JSON.parse(localStorage.getItem(tokenId)); 104 | // check if the token is issued for the correct host 105 | // check the existing token time expiry 106 | if(tokenObj && tokenObj.token && tokenObj.host === settings.host && now - tokenObj.date < tokenLife * 1000){ 107 | return tokenObj.token; 108 | }else{ 109 | return undefined; 110 | } 111 | }, 112 | logout = function() { 113 | removeToken(); 114 | // reload the page without any tokens 115 | window.location = window.location.href.replace(/(#).*$/, ''); 116 | }, 117 | connectUrl = 'https://' + settings.host + '/connect?client_id=' + apiKey +'&response_type=token&redirect_uri=' + settings.redirect, 118 | authorize = function() { 119 | window.location = connectUrl; 120 | }, 121 | reqHeaders = function (xhr) { 122 | if(settings.authRequired){ 123 | xhr.setRequestHeader('Authorization', "OAuth " + token); 124 | } 125 | xhr.setRequestHeader('Accept', "application/json"); 126 | }, 127 | // call the SoundCloud API, supported signatures: callApi(resource, callback) or callApi(resource, paramsObj, callback) 128 | callApi = function(params) { 129 | var onError = function(xhr, status, error) { 130 | log('callApi: API error', arguments); 131 | settings.onApiError(xhr, status, error); 132 | anounceEvent(events.ApiError, {resource: params.resource, xhr: xhr, status: status, error: error}); 133 | // if the token expired, try to reauthorize automatically 134 | if(settings.reAuthorize){ 135 | authorize(); 136 | } 137 | }; 138 | 139 | // throw an error if calling api without token and auth needed 140 | if(settings.authRequired && !token){ 141 | throw "Please authorize before calling the API at " + settings.host; 142 | } 143 | 144 | return $.ajax({ 145 | url: 'https://api.' + settings.host + params.resource, 146 | beforeSend: reqHeaders, 147 | data: params.data || {}, 148 | success: function(data, textStatus, xhr) { 149 | // check if data is returned, in FF 401 would still trigger success 150 | if(!data){ 151 | onError(xhr, 'Authorization Unsuccessful, client-side catch', 401); 152 | return; 153 | } 154 | if($.isFunction(params.callback)){ 155 | params.callback(data); 156 | } 157 | anounceEvent(events.ApiCall, {resource: params.resource, data: data}); 158 | }, 159 | error: onError 160 | }); 161 | }, 162 | // handle multiple method signatures, jQuery style 163 | prepareCallObj = function(args, method) { 164 | var obj = {resource: args[0], method: method}; 165 | if(args.length === 3){ 166 | obj.callback = args[2]; 167 | obj.data = args[1]; 168 | }else{ 169 | obj.callback = $.isFunction(args[1]) ? args[1] : null; 170 | obj.data = $.isFunction(args[1]) ? {} : args[1]; 171 | } 172 | return obj; 173 | }, 174 | // generate API methods 175 | callMethod = function(method) { 176 | return function(resource, data, callback) { 177 | return callApi(prepareCallObj(arguments, method)); 178 | }; 179 | }, 180 | // gets the currently authorized user 181 | getMe = function(callback) { 182 | callMethod('GET')('/me', callback); 183 | }, 184 | // checks if the wrapper is supported on this client 185 | supported = true; 186 | 187 | // check if the browser supports the wrapper, check if we have a token available 188 | if((JSON && 'parse' in JSON) && (localStorage && 'getItem' in localStorage)){ 189 | token = readToken(); 190 | }else{ 191 | supported = false; 192 | log('SoundCloud API: the browser is not currently supported'); 193 | anounceEvent(events.UnSupported); 194 | } 195 | 196 | if(!apiKey){ 197 | // check if the developer provided an api key 198 | throw "Please provide an API key for " + settings.host; 199 | }else if(token){ 200 | // if already authorized, try to get the logged in user's data 201 | getMe(function(user) { 202 | settings.onAuthSuccess(user, settings.container); 203 | anounceEvent(events.AuthSuccess, {user: user}); 204 | }); 205 | }else{ 206 | // if not yet authorized, execute default callback 207 | if(settings.authRequired){ 208 | settings.onNonAuth(connectUrl, settings.container); 209 | } 210 | anounceEvent(events.NonAuth, {connectUrl: connectUrl}); 211 | } 212 | // if the wrapper is functional, return the public interface 213 | return { 214 | isAuthorized: !!token, 215 | supported: supported, 216 | authorize: authorize, 217 | logout: logout, 218 | callApi: callApi, 219 | get: callMethod('GET'), 220 | post: callMethod('POST'), 221 | put: callMethod('PUT'), 222 | 'delete': callMethod('DELETE') 223 | }; 224 | }; 225 | // make events available publicly 226 | $.sc.api.events = events; 227 | 228 | })(jQuery); --------------------------------------------------------------------------------