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
178 changes: 137 additions & 41 deletions lib/spotify.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
var vm = require('vm');
var util = require('./util');
var http = require('http');
var https = require('https');
var tls = require('tls');
var WebSocket = require('ws');
var cheerio = require('cheerio');
var schemas = require('./schemas');
Expand Down Expand Up @@ -67,6 +69,35 @@ Spotify.login = function (un, pw, fn) {
return spotify;
};

/**
* Patched version of `https.Agent.createConnection` that disables SNI on websocket connections.
*/

function createHttpsConnection(port, host, options) {
if (typeof port === 'object') {
options = port;
} else if (typeof host === 'object') {
options = host;
} else if (typeof options === 'object') {
options = options;
} else {
options = {};
}

if (typeof port === 'number') {
options.port = port;
}

if (typeof host === 'string') {
options.host = host;
}

// Disable SNI
options.servername = null;

return tls.connect(options);
}

/**
* Spotify Web base class.
*
Expand All @@ -86,7 +117,7 @@ function Spotify () {
this.authServer = 'play.spotify.com';
this.authUrl = '/xhr/json/auth.php';
this.landingUrl = '/';
this.userAgent = 'Mozilla/5.0 (Chrome/13.37 compatible-ish) spotify-web/' + pkg.version;
this.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.46 Safari/537.36';

// base URLs for Image files like album artwork, artist prfiles, etc.
// these values taken from "spotify.web.client.js"
Expand All @@ -105,6 +136,10 @@ function Spotify () {
this.sourceUrls.LARGE = this.sourceUrls.large;
this.sourceUrls.XLARGE = this.sourceUrls.avatar;

// WebSocket agent
this.wsAgent = new https.Agent();
this.wsAgent.createConnection = createHttpsConnection;

// WebSocket callbacks
this._onopen = this._onopen.bind(this);
this._onclose = this._onclose.bind(this);
Expand Down Expand Up @@ -333,7 +368,11 @@ Spotify.prototype._openWebsocket = function (err, res) {
var url = 'wss://' + ap_list[0] + '/';

debug('WS %j', url);
this.ws = new WebSocket(url, null, {"origin": "https://play.spotify.com", "headers":{"User-Agent": this.userAgent}});
this.ws = new WebSocket(url, null, {
"agent": this.wsAgent,
"origin": "https://play.spotify.com",
"headers":{"User-Agent": this.userAgent}
});
this.ws.on('open', this._onopen);
this.ws.on('close', this._onclose);
this.ws.on('message', this._onmessage);
Expand Down Expand Up @@ -713,14 +752,14 @@ Spotify.prototype.metadata = function (uris, fn) {
mtype = type;
requests.push({
method: 'GET',
uri: 'hm://metadata/' + type + '/' + id
uri: 'hm://metadata/3/' + type + '/' + id
});
});


var header = {
method: 'GET',
uri: 'hm://metadata/' + mtype + 's'
uri: 'hm://metadata/3/' + mtype + 's'
};
var multiGet = true;
if (requests.length == 1) {
Expand Down Expand Up @@ -929,50 +968,107 @@ Spotify.prototype.isTrackAvailable = function (track, country) {
if (!country) country = this.country;
debug('isTrackAvailable()');

var allowed = [];
var forbidden = [];
var available = false;
var restriction;

if (Array.isArray(track.restriction)) {
for (var i = 0; i < track.restriction.length; i++) {
restriction = track.restriction[i];
allowed.push.apply(allowed, restriction.allowed);
forbidden.push.apply(forbidden, restriction.forbidden);

var isAllowed = !restriction.hasOwnProperty('countriesAllowed') || has(allowed, country);
var isForbidden = has(forbidden, country) && forbidden.length > 0;

// guessing at names here, corrections welcome...
var accountTypeMap = {
premium: 'SUBSCRIPTION',
unlimited: 'SUBSCRIPTION',
free: 'AD'
};

if (has(allowed, country) && has(forbidden, country)) {
isAllowed = true;
isForbidden = false;
var account = {
catalogue: this.accountType,
country: country
};

return this.isPlayable(
this.parseRestrictions(track.restriction, account),
account
);
};

/**
* @param {String} availability Track availability
* @param {Object} account Account details {catalogue, country}
* @api public
*/

Spotify.prototype.isPlayable = function(availability, account) {
if(availability === "premium" && account.catalogue === "premium") {
return true;
}

return availability === "available";
};

/**
* @param {Array} restrictions Track restrictions
* @param {Object} account Account details {catalogue, country}
* @api public
*/

Spotify.prototype.parseRestrictions = function(restrictions, account) {
debug('parseRestrictions() account: %j', account);

var catalogues = {},
available = false;

if ("undefined" === typeof restrictions || 0 === restrictions.length) {
// Track has no restrictions
return "available";
}

for (var ri = 0; ri < restrictions.length; ++ri) {
var restriction = restrictions[ri],
valid = true,
allowed;

if(restriction.countriesAllowed != void 0) {
// Check if account region is allowed
valid = restriction.countriesAllowed.length !== 0;
allowed = has(restriction.allowed, account.country);
} else {
// Check if account region is forbidden
if(restriction.countriesForbidden != void 0) {
allowed = !has(restriction.forbidden, account.country);
} else {
allowed = true;
}
}

if (allowed && restriction.catalogueStr != void 0) {
// Update track catalogues
for (var ci = 0; ci < restriction.catalogueStr.length; ++ci) {
var key = restriction.catalogueStr[ci];

catalogues[key] = true;
}
}

var type = accountTypeMap[this.accountType] || 'AD';
var applicable = has(restriction.catalogue, type);
if (restriction.type == void 0 || "streaming" == restriction.type.toLowerCase()) {
available |= valid;
}
}

available = isAllowed && !isForbidden && applicable;
debug('parseRestrictions() catalogues: %j', catalogues);

//debug('restriction: %j', restriction);
debug('type: %j', type);
debug('allowed: %j', allowed);
debug('forbidden: %j', forbidden);
debug('isAllowed: %j', isAllowed);
debug('isForbidden: %j', isForbidden);
debug('applicable: %j', applicable);
debug('available: %j', available);
if(available && account.catalogue === "all") {
// Account can stream anything
return "available";
}

if (available) break;
if(catalogues[account.catalogue]) {
// Track can be streamed by this account
if(account.catalogue === "premium") {
return "premium";
} else {
return "available";
}
}
return available;

if(catalogues.premium) {
// Premium account required
return "premium";
}

if(available) {
// Track not available in the account region
return "regional";
}

return "unavailable";
};

/**
Expand Down
2 changes: 1 addition & 1 deletion lib/track.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Track.prototype.metadata = function (fn) {
if (err) return fn(err);
// extend this Track instance with the new one's properties
Object.keys(track).forEach(function (key) {
if (!self.hasOwnProperty(key)) {
if (track.hasOwnProperty(key)) {
self[key] = track[key];
}
});
Expand Down
Binary file modified proto/metadata.desc
Binary file not shown.
7 changes: 1 addition & 6 deletions proto/metadata.proto
Original file line number Diff line number Diff line change
Expand Up @@ -116,18 +116,13 @@ message Copyright {
optional string text = 2;
}
message Restriction {
enum Catalogue {
AD = 0;
SUBSCRIPTION = 1;
SHUFFLE = 3;
}
enum Type {
STREAMING = 0;
}
repeated Catalogue catalogue = 1;
optional string countries_allowed = 2;
optional string countries_forbidden = 3;
optional Type type = 4;
repeated string catalogue_str = 5;
}

message SalePeriod {
Expand Down