Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ var codes = {
0: 'Account subscription status not Spotify Premium',
1: 'Failed to send to backend',
8: 'Rate limited',
11: 'Track Cap reached',
12: 'Time Cap reached',
408: 'Timeout',
429: 'Too many requests'
};
Expand Down
109 changes: 86 additions & 23 deletions lib/spotify.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var SpotifyError = require('./error');
var EventEmitter = require('events').EventEmitter;
var debug = require('debug')('spotify-web');
var package = require('../package.json');
var urlparse = require('url').parse;

/**
* Module exports.
Expand Down Expand Up @@ -84,14 +85,24 @@ function Spotify () {
this.connected = false; // true after the WebSocket "connect" message is sent
this._callbacks = Object.create(null);

this.authAttempts = 0;
this.authAttemptLimit = 2;
this.authServer = 'play.spotify.com';
this.authUrl = '/xhr/json/auth.php';
this.redirectUrl = '/redirect/facebook/notification.php';
this.landingUrl = '/';
this.userAgent = 'Mozilla/5.0 (Chrome/13.37 compatible-ish) spotify-web/' + package.version;

// the client version to "emulate"
this.clientVersion = 62300120; // client version: 0.6.23.120, deployed 2014-01-08 14:28 UTC

// the query-string to send along to the "redirect url"
this.redirectPayload = {
album: 'http://open.spotify.com/album/2mCuMNdJkoyiXFhsQCLLqw',
song: 'http://open.spotify.com/track/6JEK0CvvjDjjMUBFoXShNZ'
};
this.landingPayload = {};

// base URLs for Image files like album artwork, artist prfiles, etc.
// these values taken from "spotify.web.client.js"
this.sourceUrl = 'https://d3rt1990lpmkn.cloudfront.net';
Expand Down Expand Up @@ -161,7 +172,11 @@ Spotify.prototype.anonymousLogin = function (fn) {
debug('Spotify#anonymousLogin()');

// save credentials for later...
this.creds = { type: 'anonymous' };
this.creds = { type: 'anonymous', cf: 'facebook_play_to_webplayer', f: 'tosed', s: 'direct' };

// use redirect url as landing page
this.landingUrl = this.redirectUrl;
this.landingPayload = this.redirectPayload;

this._setLoginCallbacks(fn);
this._makeLandingPageRequest();
Expand Down Expand Up @@ -225,23 +240,35 @@ Spotify.prototype._makeLandingPageRequest = function() {
debug('GET %j', url);
this.agent.get(url)
.set({ 'User-Agent': this.userAgent })
.end(this._onsecret.bind(this));
.query(this.landingPayload)
.end(this._onlandingpage.bind(this));
};

/**
* Called when the Facebook redirect URL GET (and any necessary redirects) has
* responded.
* Called when the Landing Page / Facebook redirect URL GET
* (and any necessary redirects) has responded.
*
* @api private
*/

Spotify.prototype._onsecret = function (err, res) {
Spotify.prototype._onlandingpage = function (err, res) {
if (err) return this.emit('error', err);
if (res.error) return this.emit('error', res.error);
if (res.redirects && res.redirects.length) {
var redirecturl = urlparse(res.redirects[res.redirects.length-1], true);
if (this.authServer == redirecturl.host) {
debug('saving redirected landing page: %s', redirecturl.path);
this.landingUrl = redirecturl.pathname;
this.landingPayload = redirecturl.query;
} else {
debug('redirected to a different host - not saving redirected url: %s', redirecturl.href);
}
}

debug('landing page: %d status code, %j content-type', res.statusCode, res.headers['content-type']);
var $ = cheerio.load(res.text);

// need to grab the CSRF token and trackingId from the page.
// need to grab the settings from the page.
// currently, it's inside an Object that gets passed to a
// `new Spotify.Web.Login()` call as the second parameter.
var args;
Expand All @@ -257,24 +284,42 @@ Spotify.prototype._onsecret = function (err, res) {
vm.runInNewContext(code, { document: null, Spotify: { Web: { Login: login } } });
}
}
debug('login CSRF token: %j, tracking ID: %j', args.csrftoken, args.trackingId);

// construct credentials object to send from stored credentials
var creds = this.creds;
delete this.creds;
creds.secret = args.csrftoken;
creds.trackingId = args.trackingId;
creds.landingURL = args.landingURL;
creds.referrer = args.referrer;
creds.cf = null;
// save all settings, used later to authenticate
this.settings = args;

// bypass the authentication endpoint if we already authenticated
// and we only wanted an anonymous session anyway
if ('anonymous' == this.creds.type && args.credentials && args.credentials.length && args.credentials[0]) {
debug('skipping authentication request as credentials are already available...');
this._resolveAP();
} else {
this._authenticate();
}
};

/**
* Called to send the POST to the "auth" endpoint after
* the initial settings have been gathered from the landing page request
*
* @api private
*/
Spotify.prototype._authenticate = function() {
debug('login CSRF token: %j, tracking ID: %j', this.settings.csrftoken, this.settings.trackingId);

// amend credentials object with values from landing page
this.creds.secret = this.settings.csrftoken;
this.creds.trackingId = this.settings.trackingId;
this.creds.landingURL = this.settings.landingURL;
this.creds.referrer = this.settings.referrer;

// now we have to "auth" in order to get Spotify Web "credentials"
var url = 'https://' + this.authServer + this.authUrl;
debug('POST %j', url);
this.agent.post(url)
.set({ 'User-Agent': this.userAgent })
.type('form')
.send(creds)
.send(this.creds)
.end(this._onauth.bind(this));
};

Expand All @@ -285,18 +330,36 @@ Spotify.prototype._onsecret = function (err, res) {
*/

Spotify.prototype._onauth = function (err, res) {
if (err) return this.emit('error', err);
// count the number of auth attempts, and determine if we should keep trying
var attempt = ++this.authAttempts;
var retry = (attempt < this.authAttemptLimit);

debug('auth %d status code, %j content-type', res.statusCode, res.headers['content-type']);
// if an error occurs, try again from the landing page if we can,
// otherwise give up with an error event
var self = this;
var authError = function(err) {
if (!(err instanceof Error)) err = new Error(err);
debug('auth error: %s', err.message);
if (retry) return self._makeLandingPageRequest();
return self.emit('error', err);
};

// check for errors
if (err) return authError(err);
if (res.error) return authError(res.error);
if ('ERROR' == res.body.status) {
// got an error...
var msg = res.body.error;
if (res.body.message) msg += ': ' + res.body.message;
this.emit('error', new Error(msg));
} else {
this.settings = res.body.config;
this._resolveAP();
return authError(msg);
}
if (res.body.error) {
debug('auth status: %s', res.body.error);
}

// if no errors, save the config and delete the credentials
delete this.creds;
this.settings = res.body.config;
this._resolveAP();
};

/**
Expand Down