UNPKG

searchtify

Version:

a search package for spotify that requires no credentials!

265 lines (263 loc) 9.8 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/index.ts import crypto from "node:crypto"; var postJSON = /* @__PURE__ */ __name(async (url, body, headers = {}) => { const res = await fetch(url, { method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json", ...headers }, body: JSON.stringify(body) }); return res.json(); }, "postJSON"); var Spotify = class { static { __name(this, "Spotify"); } $latestSecret = { version: 1, secret: [] }; deviceId = ""; cookie = ""; customUserAgent = ""; accessToken = { clientId: "", accessToken: "", accessTokenExpirationTimestampMs: 0 }; clientToken = { token: "", refreshAt: 0 }; variables; constructor() { this.$fetchSecrets(); setInterval(() => this.$fetchSecrets(), 1e3 * 60 * 60 * 1).unref(); } async $fetchSecrets() { const req = await fetch("https://raw.githubusercontent.com/VillainsRule/searchtify/master/secrets/secretBytes.json"); const bytes = await req.json(); this.$latestSecret = bytes[bytes.length - 1]; if (!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 3" in the issue'); process.exit(1); } } setUserAgent(userAgent) { this.customUserAgent = userAgent; } async getVariables() { const mainReq = await fetch("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", "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" } }); const mainPage = await mainReq.text(); const deviceId = mainReq.headers.getSetCookie().find((h) => h.startsWith("sp_t="))?.split(";")[0].split("=")[1]; if (!deviceId) throw new Error("[searchtify] library broke. open an issue on Github with error code 210."); this.deviceId = deviceId; const mainScript = mainPage.match(/<\/script><script src="(.*?)"/)?.[1]; if (!mainScript) throw new Error("[searchtify] library broke. open an issue on Github with error code 293."); const scriptContent = await (await fetch(mainScript)).text(); this.variables = { buildVer: "unknown", buildDate: "unknown", clientVersion: scriptContent.match(/clientVersion:"(.*?)"/)?.[1] || "", serverTime: mainReq.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 digits = 6; const timeStep = 30; const time = Math.floor(timestamp / 1e3 / 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] & 15; const code = ((hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255) % 10 ** digits; return code.toString().padStart(digits, "0"); } async pullAccessToken() { 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.toString()); urlBase.search = params.toString(); const req = await fetch(urlBase, { headers: { "Accept": "*/*", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate, br", "Connection": "keep-alive" } }); const res = await req.json(); if (res.error) { 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 = res; } async pullClientToken() { if (!this.variables) await this.getVariables(); const data = await postJSON( "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" } } } ); this.clientToken = { ...data.granted_token, token: data.granted_token.token, refreshAt: Date.now() + 1209600 }; } async getHeaders() { if (!this.accessToken.accessToken) await this.pullAccessToken(); if (this.accessToken.accessTokenExpirationTimestampMs - Date.now() <= 1) await this.pullAccessToken(); 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", "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 data = await postJSON( "https://api-partner.spotify.com/pathfinder/v2/query", { operationName: "searchDesktop", variables: { ...opts, searchTerm: query, offset: opts.offset ?? 0, limit: opts.limit ?? 10, numberOfTopResults: opts.numberOfTopResults ?? 5, includeAudiobooks: opts.includeAudiobooks ?? false, includeArtistHasConcertsField: opts.includeArtistHasConcertsField ?? true, includePreReleases: opts.includePreReleases ?? true, includeLocalConcertsField: opts.includeLocalConcertsField ?? false, includeAuthors: opts.includeAuthors ?? true }, extensions: { persistedQuery: { version: 1, sha256Hash: "d9f785900f0710b31c07818d617f4f7600c1e21217e80f5b043d1e78d74e6026" } } }, await this.getHeaders() ); return data.data.searchV2; } async getPopular(timezone = Intl.DateTimeFormat().resolvedOptions().timeZone) { const data = await postJSON( "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" } } }, await this.getHeaders() ); return data.data.home.sectionContainer.sections.items; } async getAlbum(uri) { const data = await postJSON( "https://api-partner.spotify.com/pathfinder/v2/query", { operationName: "getAlbum", variables: { uri, locale: "", offset: 0, limit: 50 }, extensions: { persistedQuery: { version: 1, sha256Hash: "97dd13a1f28c80d66115a13697a7ffd94fe3bebdb94da42159456e1d82bfee76" } } }, await this.getHeaders() ); return data.data.albumUnion; } async getArtist(uri) { const data = await postJSON( "https://api-partner.spotify.com/pathfinder/v2/query", { operationName: "queryArtistOverview", variables: { uri, locale: "" }, extensions: { persistedQuery: { version: 1, sha256Hash: "1ac33ddab5d39a3a9c27802774e6d78b9405cc188c6f75aed007df2a32737c72" } } }, await this.getHeaders() ); return data.data.artistUnion; } }; var index_default = Spotify; export { index_default as default };