-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathAPI.ts
155 lines (141 loc) · 6.49 KB
/
API.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
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"].includes("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
}
}