searchtify
Version:
a search package for spotify that requires no credentials!
306 lines (256 loc) • 11.6 kB
JavaScript
import crypto from 'node:crypto';
import { axiosLike } from './axiosLike.js';
class Spotify {
constructor() {
this.$fetchSecrets();
setInterval(() => this.$fetchSecrets(), 1000 * 60 * 60 * 1).unref();
}
async $fetchSecrets() {
const bytes = await axiosLike.get('https://raw.githubusercontent.com/Thereallo1026/spotify-secrets/main/secrets/secretBytes.json');
this.$latestSecret = bytes.data[bytes.data.length - 1];
}
setUserAgent(userAgent) {
this.customUserAgent = userAgent;
}
async login(sp_dcCookie) {
if (!sp_dcCookie) throw new Error('specify the sp_dc cookie in logIn');
this.cookie = sp_dcCookie;
return await this.whoAmI();
}
async getVariables() {
const mainPage = await axiosLike.get('https://open.spotify.com', {
headers: {
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
cookie: this.cookie,
'user-agent': this.customUserAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36'
}
});
this.deviceId = mainPage.headers.getSetCookie().find(h => h.startsWith('sp_t=')).split(';')[0].split('=')[1];
const mainScript = mainPage.data.match(/<\/script><script src="(.*?)"/)[1];
const scriptContent = await axiosLike.get(mainScript);
this.variables = {
// spotify seems to have broken their own client...
buildVer: 'unknown', // scriptContent.data.match(/buildVer:"(.*?)"/)?.[1],
buildDate: 'unknown', // scriptContent.data.match(/buildDate:"(.*?)"/)?.[1],
clientVersion: scriptContent.data.match(/clientVersion:"(.*?)"/)?.[1],
serverTime: mainPage.headers.get('x-timer').match(/S([0-9]+)\./)?.[1]
};
return this.variables;
}
toSecret(input) {
const inputBytes = [...Buffer.from(input)];
const transformed = inputBytes.map((e, t) => e ^ ((t % 33) + 9));
const joined = transformed.map(num => num.toString()).join('');
const hex_str = Buffer.from(joined).toString('hex');
return Buffer.from(hex_str, 'hex');
}
generateTOTP(timestamp = Date.now()) {
const totpSecret = this.$latestSecret ? this.toSecret(this.$latestSecret.secret) : null;
if (!totpSecret) {
console.error('spotify patched searchtify yet again. here\'s how to fix:');
console.error('1. ensure you have the latest version of searchtify installed');
console.error('2. open an issue @ https://github.com/VillainsRule/searchtify');
console.error('3. make sure to specify "error code 2" in the issue');
process.exit(1);
}
const secretBuffer = Buffer.from(totpSecret);
const secret = secretBuffer.toString('hex');
const digits = 6;
const timeStep = 30;
const time = Math.floor(timestamp / 1000 / timeStep);
const counter = Buffer.alloc(8);
counter.writeBigUInt64BE(BigInt(time));
const hmac = crypto.createHmac('sha1', secretBuffer).update(counter).digest();
const offset = hmac[hmac.length - 1] & 0xf;
const code = (
((hmac[offset] & 0x7f) << 24) |
((hmac[offset + 1] & 0xff) << 16) |
((hmac[offset + 2] & 0xff) << 8) |
(hmac[offset + 3] & 0xff)
) % 10 ** digits;
return code.toString().padStart(digits, '0');
}
async getAccessToken() {
if (!this.variables) await this.getVariables();
const urlBase = new URL('https://open.spotify.com/api/token');
const params = new URLSearchParams();
const totp = this.generateTOTP();
params.append('reason', 'init');
params.append('productType', 'web-player');
params.append('totp', totp);
params.append('totpServer', totp);
params.append('totpVer', this.$latestSecret.version);
// params.append('sTime', this.variables.serverTime);
// params.append('cTime', Date.now().toString());
// params.append('buildVer', this.variables.buildVer);
// params.append('buildDate', this.variables.buildDate);
// params.append('totpValidUntil', '');
urlBase.search = params.toString();
const response = await axiosLike.get(urlBase, {
headers: {
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Cookie': this.cookie
}
});
if (response.data.error) {
// console.log(response.data, this.$latestSecret);
console.error('spotify patched searchtify yet again. here\'s how to fix:');
console.error('1. ensure you have the latest version of searchtify installed');
console.error('2. open an issue @ https://github.com/VillainsRule/searchtify');
console.error('3. make sure to specify "error code 1" in the issue');
process.exit(1);
}
this.accessToken = response.data;
}
async getClientToken() {
if (!this.variables) await this.getVariables();
const response = await axiosLike.post('https://clienttoken.spotify.com/v1/clienttoken', {
client_data: {
client_version: this.variables.clientVersion,
client_id: this.accessToken.clientId,
js_sdk_data: {
device_brand: 'Apple',
device_model: 'unknown',
os: 'macos',
os_version: '10.15.7',
device_id: this.deviceId,
device_type: 'computer'
}
}
}, {
headers: {
'Accept': 'application/json',
'Cookie': this.cookie,
'Content-Type': 'application/json'
}
});
this.clientToken = response.data.granted_token;
this.clientToken.refreshAt = Date.now() + 1209600;
}
async getHeaders() {
if (!this.accessToken) await this.getAccessToken();
// if (!this.clientToken) await this.getClientToken();
if (this.accessToken.accessTokenExpirationTimestampMs - Date.now() <= 1) await this.getAccessToken();
// if (this.clientToken.refreshAt <= Date.now()) await this.getClientToken();
return {
'Accept': 'application/json',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Accept-Language': 'en',
'App-Platform': 'WebPlayer',
'Authorization': `Bearer ${this.accessToken.accessToken}`,
'Cache-Control': 'no-cache',
// 'Client-Token': this.clientToken.token,
'Content-Type': 'application/json;charset=UTF-8',
'Origin': 'https://open.spotify.com',
'Referer': 'https://open.spotify.com/',
'Spotify-App-Version': this.variables.clientVersion,
'User-Agent': this.customUserAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36'
}
}
async search(query, opts = {}) {
const url = new URL('https://api-partner.spotify.com/pathfinder/v2/query');
const payload = {};
payload.operationName = 'searchDesktop';
const variables = opts;
variables.searchTerm = query;
if (!variables.offset) variables.offset = 0;
if (!variables.limit) variables.limit = 10;
if (!variables.numberOfTopResults) variables.numberOfTopResults = 5;
if (!variables.includeAudiobooks) variables.includeAudiobooks = true;
if (!variables.includeArtistHasConcertsField) variables.includeArtistHasConcertsField = false;
if (!variables.includePreReleases) variables.includePreReleases = true;
if (!variables.includeLocalConcertsField) variables.includeLocalConcertsField = false;
if (!variables.includeAuthors) variables.includeAuthors = false;
payload.variables = variables;
payload.extensions = {
persistedQuery: {
version: 1,
sha256Hash: 'd9f785900f0710b31c07818d617f4f7600c1e21217e80f5b043d1e78d74e6026'
}
};
let response = await axiosLike.post(url.toString(), payload, { headers: await this.getHeaders() });
return response.data.data.searchV2;
}
async getPopular(timezone = Intl.DateTimeFormat().resolvedOptions().timeZone) {
let response = await axiosLike.post('https://api-partner.spotify.com/pathfinder/v2/query', {
operationName: 'home',
variables: {
timeZone: timezone,
sp_t: this.deviceId,
facet: '',
sectionItemsLimit: 10
},
extensions: {
persistedQuery: {
version: 1,
sha256Hash: '72325e84c876c72564fb9ab012f602be8ef6a1fdd3039be2f8b4f2be4c229a30'
}
}
}, { headers: await this.getHeaders() });
return response.data.data.home.sectionContainer.sections.items;
}
async getAlbum(uri) {
let response = await axiosLike.post('https://api-partner.spotify.com/pathfinder/v2/query', {
operationName: 'getAlbum',
variables: {
uri: uri,
locale: '',
offset: 0,
limit: 50
},
extensions: {
persistedQuery: {
version: 1,
sha256Hash: '97dd13a1f28c80d66115a13697a7ffd94fe3bebdb94da42159456e1d82bfee76'
}
}
}, { headers: await this.getHeaders() });
return response.data.data.albumUnion;
}
async getArtist(uri) {
let response = await axiosLike.post('https://api-partner.spotify.com/pathfinder/v2/query', {
operationName: 'queryArtistOverview',
variables: {
uri: uri,
locale: ''
},
extensions: {
persistedQuery: {
version: 1,
sha256Hash: '1ac33ddab5d39a3a9c27802774e6d78b9405cc188c6f75aed007df2a32737c72'
}
}
}, { headers: await this.getHeaders() });
return response.data.data.artistUnion;
}
isLoggedIn() {
return this.accessToken?.isAnonymous;
}
async whoAmI() {
try {
const response = await axiosLike.post('https://api-partner.spotify.com/pathfinder/v2/query', {
operationName: 'profileAttributes',
variables: {},
extensions: {
persistedQuery: {
version: 1,
sha256Hash: '53bcb064f6cd18c23f752bc324a791194d20df612d8e1239c735144ab0399ced'
}
}
}, {
headers: {
...(await this.getHeaders()),
'Accept': 'application/json'
},
});
return response.data.data.me.profile;
} catch (error) {
if (error.response?.data) return error.response.data;
throw error;
}
}
}
export default Spotify;