@manhgdev/spotifyweb
Version:
Spotify library in typescript without using the Spotify Web API. No authentication required with automatic internal token generation.
246 lines (245 loc) • 9.59 kB
JavaScript
/**
* Spotify Token Generator
* Extracts and implements the TOTP generation logic for Spotify API authentication
*/
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { puppeteer_extract } from './secrets/puppeteer_extract.js';
// Read secret data from JSON file
function loadSecretData() {
try {
// Try multiple possible paths for secrets file
const possiblePaths = [
// Development/test environment
path.join(process.cwd(), 'src/lib/secrets/secretBytes.json'),
// Production environment (original path)
path.join(process.cwd(), 'src/modules/carwl/spotify/lib/secrets/secretBytes.json'),
// NPM package path (relative to this file)
path.join(path.dirname(import.meta.url.replace('file://', '')), 'secrets/secretBytes.json')
];
let secretData = null;
for (const secretPath of possiblePaths) {
if (fs.existsSync(secretPath)) {
secretData = JSON.parse(fs.readFileSync(secretPath, 'utf8'));
break;
}
}
// Get a random version
const randomIndex = Math.floor(Math.random() * secretData.length);
const randomSecret = secretData[randomIndex];
return {
SECRET_DATA: randomSecret.secret,
TOTP_VERSION: randomSecret.version.toString()
};
}
catch (error) {
console.error('Failed to load secret data:', error.message);
puppeteer_extract();
// Fallback to hardcoded values if file reading fails
return {
SECRET_DATA: [59, 92, 64, 70, 99, 78, 117, 75, 99, 103, 116, 67, 103, 51, 87, 63, 93, 59, 70, 45, 32],
TOTP_VERSION: "13"
};
}
}
// Generate the TOTP secret from the data array
function generateTOTPSecret(secretData) {
// XOR each value with ((index % 33) + 9)
const mappedData = secretData.map((value, index) => value ^ ((index % 33) + 9));
// Convert to hex
const hexData = Buffer.from(mappedData.join(""), "utf8").toString("hex");
return hexData;
}
// TOTP generation function (simplified from the obfuscated code)
function generateTOTP(timestamp, secret) {
const period = 30; // 30 seconds period
const digits = 6; // 6 digit code
// Convert timestamp to counter (30-second periods since Unix epoch)
const counter = Math.floor(timestamp / 1000 / period);
// Convert counter to 8-byte buffer (big-endian)
const counterBuffer = Buffer.allocUnsafe(8);
counterBuffer.writeUInt32BE(0, 0);
counterBuffer.writeUInt32BE(counter, 4);
// HMAC-SHA1
const hmac = crypto.createHmac('sha1', Buffer.from(secret, 'hex'));
const hash = hmac.update(counterBuffer).digest();
// Dynamic truncation
const offset = hash[hash.length - 1] & 0x0f;
const truncatedHash = hash.slice(offset, offset + 4);
// Convert to number and apply modulo
const code = truncatedHash.readUInt32BE(0) & 0x7fffffff;
const otp = code % Math.pow(10, digits);
// Pad with leading zeros
return otp.toString().padStart(digits, '0');
}
// Get server time from Spotify
async function getServerTime(cookie) {
try {
const response = await fetch('https://open.spotify.com/api/server-time', {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
'Origin': 'https://open.spotify.com/',
'Referer': 'https://open.spotify.com/',
'Cookie': cookie
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
const time = Number(data.serverTime);
if (isNaN(time)) {
throw new Error('Invalid server time');
}
return time * 1000; // Convert to milliseconds
}
catch (error) {
console.warn('Failed to get server time, using local time:', error.message);
return Date.now();
}
}
// Generate authentication payload
async function generateAuthPayload(reason = "init", productType = "mobile-web-player", cookie = "") {
const localTime = Date.now();
const serverTime = await getServerTime(cookie);
// Load secret data once to ensure SECRET_DATA and TOTP_VERSION are from the same version
const { SECRET_DATA, TOTP_VERSION } = loadSecretData();
const secret = generateTOTPSecret(SECRET_DATA);
// Generate TOTP for current time
const totp = generateTOTP(localTime, secret);
// Generate TOTP for server time (divided by 30 for 30-second periods)
const totpServer = generateTOTP(Math.floor(serverTime / 30), secret);
return {
reason,
productType,
totp,
totpServer,
totpVer: TOTP_VERSION // Version loaded from secrets file
};
}
// Helper function để tạo và gửi token request
async function makeSpotifyTokenRequest(payload, cookies) {
const url = new URL("https://open.spotify.com/api/token");
Object.entries(payload).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
const config = {
method: 'GET',
url: url.toString(),
headers: {
'accept': '*/*',
'accept-language': 'en,vi;q=0.9,en-US;q=0.8',
'priority': 'u=1, i',
'referer': 'https://open.spotify.com/',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1',
'cookie': cookies
}
};
const response = await fetch(config.url, {
method: config.method,
headers: config.headers
});
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
throw error;
}
const result = await response.json();
return { response, result };
}
// Make actual HTTP request to test the generated parameters
async function spGetToken(cookie = "") {
try {
const payload = await generateAuthPayload("init", "mobile-web-player", cookie);
const { response, result } = await makeSpotifyTokenRequest(payload, cookie);
return result;
}
catch (error) {
// Handle 400 status - retry with new secrets
// console.log(error.status)
if (error.status == 400) {
console.log('Token request failed with 400, extracting new secrets...');
puppeteer_extract();
}
return {
success: false,
error: error.message,
payload: payload,
url: `https://open.spotify.com/api/token`
};
}
}
async function spRefreshToken(cookies = {}) {
try {
const response = await spGetToken(cookies);
const client_id = response.clientId;
let data = JSON.stringify({
"client_data": {
"client_version": "1.2.68.256.g7ba8ac84",
"client_id": client_id,
"js_sdk_data": {
"device_brand": "Apple",
"device_model": "unknown",
"os": "macos",
"os_version": "10.15.7",
"device_id": "",
"device_type": "computer"
}
}
});
let config = {
method: 'POST',
url: 'https://clienttoken.spotify.com/v1/clienttoken',
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
'Accept': 'application/json',
'Content-Type': 'application/json',
'accept-language': 'en',
'origin': 'https://open.spotify.com',
'referer': 'https://open.spotify.com/'
},
data: data
};
const response1 = await fetch(config.url, {
method: config.method,
headers: config.headers,
body: config.data
});
if (!response1.ok) {
throw new Error(`HTTP ${response1.status}: ${response1.statusText}`);
}
const result = await response1.json();
// Cache kết quả nếu thành công (có accessToken)
if (result.accessToken) {
// Cache với thời gian expiry từ token (nếu có)
const cacheTime = result.refresh_after_seconds
? (result.refresh_after_seconds - Date.now())
: 5 * 60 * 1000; // default 5 phút
}
return result.granted_token.token;
}
catch (error) {
console.log(error);
throw "Failed to refresh Spotify token: " + error.message;
}
}
// Export the refresh function for external use
export async function spCurlToken(cookie) {
try {
const result = await spGetToken(cookie);
// console.log(result)
return {
token: result.accessToken,
expirationTime: result.accessTokenExpirationTimestampMs
};
}
catch (error) {
throw "Failed to refresh Spotify token: " + error.message;
}
}
// Example usage
// spGetToken().then(console.log).catch(console.error);