chaosdc/node_modules/soundcloud.ts/API.ts

156 lines
6.5 KiB
TypeScript

import type {Dispatcher} from "undici"
import {Pool, request} from "undici"
const apiURL = "https://api.soundcloud.com"
const apiV2URL = "https://api-v2.soundcloud.com"
const webURL = "https://soundcloud.com"
export class API {
public static headers: Record<string, any> = {
Origin: "https://soundcloud.com",
Referer: "https://soundcloud.com/",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67",
}
public api = new Pool(apiURL)
public apiV2 = new Pool(apiV2URL)
public web = new Pool(webURL)
public proxy?: Pool
constructor(public clientId?: string, public oauthToken?: string, proxy?: string) {
if (oauthToken) API.headers.Authorization = `OAuth ${oauthToken}`
if (proxy) this.proxy = new Pool(proxy)
}
get headers() {
return API.headers
}
/**
* Gets an endpoint from the Soundcloud API.
*/
public get = (endpoint: string, params?: Record<string, any>) => {
return this.getRequest(this.api, apiURL, endpoint, params)
}
/**
* Gets an endpoint from the Soundcloud V2 API.
*/
public getV2 = (endpoint: string, params?: Record<string, any>) => {
return this.getRequest(this.apiV2, apiV2URL, endpoint, params)
}
/**
* Some endpoints use the main website as the URL.
*/
public getWebsite = (endpoint: string, params?: Record<string, any>) => {
return this.getRequest(this.web, webURL, endpoint, params)
}
/**
* Gets a URL, such as download, stream, attachment, etc.
*/
public getURL = (URI: string, params?: Record<string, any>) => {
if (this.proxy) return this.request(this.proxy, this.buildOptions(URI, "GET", params))
const options = {
query: params || {},
headers: API.headers,
maxRedirections: 5,
}
if (this.clientId) options.query.client_id = this.clientId
if (this.oauthToken) options.query.oauth_token = this.oauthToken
return request(URI, options).then(r => {
if (r.statusCode.toString().startsWith("2")) {
if (r.headers["content-type"] === "application/json") return r.body.json()
return r.body.text()
}
throw new Error(`Status code ${r.statusCode}`)
})
}
private readonly buildOptions = (path: string, method: Dispatcher.HttpMethod = "GET", params?: Record<string, any>) => {
const options: Dispatcher.RequestOptions = {
query: (method == "GET" && params) || {},
headers: API.headers,
method,
path,
maxRedirections: 5,
}
if (method === "POST" && params) options.body = JSON.stringify(params)
if (this.clientId) options.query.client_id = this.clientId
if (this.oauthToken) options.query.oauth_token = this.oauthToken
return options
}
private readonly request = (pool: Pool, options: Dispatcher.RequestOptions) => {
return pool.request(options).then(r => {
if (r.statusCode.toString().startsWith("2")) {
if (r.headers["content-type"].includes("application/json")) return r.body.json()
return r.body.text()
}
throw new Error(`Status code ${r.statusCode}`)
})
}
private readonly getRequest = async (pool: Pool, origin: string, endpoint: string, params?: Record<string, any>) => {
if (!this.clientId) await this.getClientId()
if (endpoint.startsWith("/")) endpoint = endpoint.slice(1)
const options = this.buildOptions(`${this.proxy ? origin : ""}/${endpoint}`, "GET", params)
try {
return await this.request(this.proxy || pool, options)
} catch {
await this.getClientId(true)
return this.request(this.proxy || pool, options)
}
}
public post = async (endpoint: string, params?: Record<string, any>) => {
if (!this.clientId) await this.getClientId()
if (endpoint.startsWith("/")) endpoint = endpoint.slice(1)
const options = this.buildOptions(`${this.proxy ? origin : ""}/${endpoint}`, "POST", params)
return this.request(this.proxy || this.api, options)
}
public getClientIdWeb = async () => {
const response = await this.request(this.proxy || this.web, this.buildOptions(this.proxy ? webURL : "/"))
if (!response || typeof response !== "string") throw new Error("Could not find client ID")
const urls = response.match(/(?!<script.*?src=")https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*\.js)(?=.*?>)/g)
if (!urls || urls.length === 0) throw new Error("Could not find script URLs")
do {
const script = await (this.proxy
? this.request(this.proxy, this.buildOptions(urls.pop()))
: request(urls.pop()).then(r => r.body.text()))
if (!script || typeof script !== "string") continue
const clientId = script.match(/[{,]client_id:"(\w+)"/)?.[1]
if (typeof clientId === "string") return clientId
} while (urls.length > 0)
throw new Error("Could not find client ID in script URLs")
}
public getClientIdMobile = async () => {
const response = await request("https://m.soundcloud.com/", {
headers: {
"User-Agent":
"Mozilla/5.0 (iPhone; CPU iPhone OS 16_5_1 like Mac OS X) " +
"AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/99.0.4844.47 Mobile/15E148 Safari/604.1",
},
}).then(r => r.body.text())
const clientId = response.match(/"clientId":"(\w+?)"/)?.[1]
if (typeof clientId === "string") return clientId
throw new Error("Could not find client ID")
}
public getClientId = async (reset?: boolean) => {
if (!this.oauthToken && (!this.clientId || reset)) {
this.clientId = await this.getClientIdWeb().catch(webError =>
this.getClientIdMobile().catch(mobileError => {
throw new Error(
"Could not find client ID. Please provide one in the constructor. (Guide: https://github.com/Tenpi/soundcloud.ts#getting-started)" +
`\nWeb error: ${webError}` +
`\nMobile error: ${mobileError}`
)
})
)
}
return this.clientId
}
}