@manhgdev/spotifyweb
Version:
Spotify library in typescript without using the Spotify Web API. No authentication required with automatic internal token generation.
125 lines (124 loc) • 5.65 kB
JavaScript
import crypto from 'crypto';
export class SpotiflyBase {
token = "";
tokenExpirationTimestampMs = -1;
cookie;
myProfileId = "";
constructor(cookie) {
this.cookie = cookie ?? "";
}
async refreshToken() {
if (this.tokenExpirationTimestampMs > Date.now())
return;
try {
// Nếu có cookie, ưu tiên sử dụng để xác thực
if (this.cookie) {
try {
const response = await fetch("https://open.spotify.com/get_access_token?reason=transport&productType=web_player", {
headers: {
cookie: this.cookie
}
});
if (response.ok) {
const data = await response.json();
this.token = "Bearer " + data.accessToken;
this.tokenExpirationTimestampMs = data.accessTokenExpirationTimestampMs;
return;
}
else {
console.warn("Không thể xác thực bằng cookie, cố gắng dùng TOTP...");
}
}
catch (cookieError) {
console.warn("Lỗi khi xác thực bằng cookie:", cookieError);
}
}
// Nếu không có cookie hoặc xác thực bằng cookie thất bại, dùng TOTP
const [totp, ts] = this.generateToken();
const params = new URLSearchParams({
reason: "transport",
productType: "embed",
totp,
totpVer: "5",
ts: ts.toString()
});
const tokenUrl = `https://open.spotify.com/get_access_token?${params.toString()}`;
let response = await fetch(tokenUrl);
// Nếu request thất bại, thử lại một lần
if (!response.ok) {
console.warn("Lần đầu request thất bại, đang thử lại...");
response = await fetch(tokenUrl);
if (!response.ok) {
throw new Error(`Failed to get token after retry. Status: ${response.status}`);
}
}
const responseText = await response.text();
try {
const data = JSON.parse(responseText);
this.token = "Bearer " + data.accessToken;
this.tokenExpirationTimestampMs = data.accessTokenExpirationTimestampMs;
}
catch (parseError) {
console.error("Lỗi khi parse response:", responseText);
throw parseError;
}
}
catch (error) {
console.error("Lỗi khi làm mới token:", error);
throw error;
}
}
generateToken() {
const totpSecret = new Uint8Array([
53, 53, 48, 55, 49, 52, 53, 56, 53, 51, 52, 56, 55, 52, 57, 57,
53, 57, 50, 50, 52, 56, 54, 51, 48, 51, 50, 57, 51, 52, 55
]);
// Note for me: Can also be used from Buffer.from("5507145853487499592248630329347", 'utf8');
const timeStep = Math.floor(Date.now() / 30000);
const counter = new Uint8Array(8);
const counterView = new DataView(counter.buffer);
counterView.setBigInt64(0, BigInt(timeStep));
const hmac = crypto.createHmac('sha1', totpSecret);
hmac.update(counter);
const hash = hmac.digest();
const offset = hash[hash.length - 1] & 0x0f;
const binCode = ((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
const token = (binCode % 1000000).toString().padStart(6, '0');
return [token, timeStep * 30000];
}
async fetch(url, optionalHeaders) {
await this.refreshToken();
return (await fetch(url, {
headers: { authorization: this.token, ...optionalHeaders }
})).json();
}
async post(url, body) {
await this.refreshToken();
return (await fetch(url, {
headers: {
authorization: this.token,
accept: "application/json",
"content-type": "application/json"
},
method: "POST",
body: body
})).json();
}
async getPlaylistMetadata(id, limit = 50) {
return this.fetch(`https://api-partner.spotify.com/pathfinder/v1/query?operationName=fetchPlaylistMetadata&variables=%7B%22uri%22%3A%22spotify%3Aplaylist%3A${id}%22%2C%22offset%22%3A0%2C%22limit%22%3A${limit}%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%226f7fef1ef9760ba77aeb68d8153d458eeec2dce3430cef02b5f094a8ef9a465d%22%7D%7D`);
}
async getPlaylistContents(id, limit = 50) {
return this.fetch(`https://api-partner.spotify.com/pathfinder/v1/query?operationName=fetchPlaylistContents&variables=%7B%22uri%22%3A%22spotify%3Aplaylist%3A${id}%22%2C%22offset%22%3A0%2C%22limit%22%3A${limit}%7D&extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A1%2C%22sha256Hash%22%3A%22c56c706a062f82052d87fdaeeb300a258d2d54153222ef360682a0ee625284d9%22%7D%7D`);
}
async getMyProfile() {
if (!this.cookie)
throw Error("no cookie provided");
return this.fetch("https://api.spotify.com/v1/me");
}
async getMyProfileId() {
return this.myProfileId === "" ? this.myProfileId = (await this.getMyProfile()).id : this.myProfileId;
}
}