diff --git a/src/streamers/deezer/main.ts b/src/streamers/deezer/main.ts index 9c2a95b..2a71983 100644 --- a/src/streamers/deezer/main.ts +++ b/src/streamers/deezer/main.ts @@ -1,4 +1,4 @@ -import { fetch } from 'undici' +import { Dispatcher, fetch } from 'undici' import { Album, Artist, @@ -29,6 +29,7 @@ import { Blowfish } from 'blowfish-cbc' interface DeezerOptions { arl?: string + dispatcher?: Dispatcher | undefined } interface APIMethod { @@ -86,11 +87,13 @@ export default class Deezer implements StreamerWithLogin { renewTimestamp?: number country?: string language?: string + dispatcher?: Dispatcher | undefined availableFormats: Set = new Set() constructor(options?: DeezerOptions) { if (options?.arl) this.arl = options.arl + //if (options?.dispatcher) this.dispatcher = options.dispatcher } async #apiCall( @@ -114,7 +117,12 @@ export default class Deezer implements StreamerWithLogin { } const body = JSON.stringify(data) - const req = await fetch(url, { method: 'POST', body, headers: this.headers }) + const req = await fetch(url, { + method: 'POST', + body, + headers: this.headers, + dispatcher: this.dispatcher + }) const { results, error } = await req.json() if (error.constructor.name == 'Object') { @@ -167,7 +175,10 @@ export default class Deezer implements StreamerWithLogin { async login(username: string, password: string): Promise { if (this.arl) await this.#loginViaArl(this.arl) else { - const resp = await fetch('https://www.deezer.com/', { headers: this.headers }) + const resp = await fetch('https://www.deezer.com/', { + headers: this.headers, + dispatcher: this.dispatcher + }) const setCookie = resp.headers.get('Set-Cookie') ?? '' const sid = setCookie.match(/sid=(fr[0-9a-f]+)/)![1] this.headers['Cookie'] = `sid=${sid}` @@ -181,7 +192,7 @@ export default class Deezer implements StreamerWithLogin { password, hash: this.#md5(CLIENT_ID + username + password + CLIENT_SECRET) })}`, - { headers: this.headers } + { headers: this.headers, dispatcher: this.dispatcher } ) const { error } = await loginReq.json() @@ -242,7 +253,7 @@ export default class Deezer implements StreamerWithLogin { /* ---------- GET INFO FROM URL ---------- */ async #unshortenUrl(url: URL): Promise { - const res = await fetch(url, { redirect: 'manual' }) + const res = await fetch(url, { redirect: 'manual', dispatcher: this.dispatcher }) const location = res.headers.get('Location') if (res.status != 302 || !location) throw new Error('URL not supported') @@ -351,7 +362,7 @@ export default class Deezer implements StreamerWithLogin { // download const url = await this.#getTrackUrl(id, trackToken, trackTokenExpiry, format) - const streamResp = await fetch(url) + const streamResp = await fetch(url, { dispatcher: this.dispatcher }) if (!streamResp.ok) throw new Error(`Failed to get track stream. Status code: ${streamResp.status}`) @@ -441,7 +452,8 @@ export default class Deezer implements StreamerWithLogin { } ], track_tokens: [trackToken] - }) + }), + dispatcher: this.dispatcher }) const res = await req.json() diff --git a/src/streamers/qobuz/main.ts b/src/streamers/qobuz/main.ts index 11e20fc..a9fc7d9 100644 --- a/src/streamers/qobuz/main.ts +++ b/src/streamers/qobuz/main.ts @@ -1,5 +1,5 @@ import crypto from 'crypto' -import { fetch } from 'undici' +import { fetch, Dispatcher } from 'undici' import { ItemType, StreamerWithLogin, @@ -27,6 +27,7 @@ interface QobuzOptions { appSecret: string appId: string token?: string + dispatcher?: Dispatcher } interface LoginResponse { @@ -66,11 +67,13 @@ export default class Qobuz implements StreamerWithLogin { token?: string appSecret: string appId: string + dispatcher: Dispatcher | undefined fetch?(url: URL | RequestInfo, init?: RequestInit): Promise constructor(options: QobuzOptions) { this.appSecret = options.appSecret this.appId = options.appId if (options.token) this.token = options.token + if (options.dispatcher) this.dispatcher = options.dispatcher } async #get(url: string, params: { [key: string]: string | number }) { @@ -83,7 +86,8 @@ export default class Qobuz implements StreamerWithLogin { )}`, { method: 'get', - headers: headers(this.token) + headers: headers(this.token), + dispatcher: this.dispatcher } ) if (!response.ok) { @@ -179,7 +183,7 @@ export default class Qobuz implements StreamerWithLogin { ) if (trackFileResponse.sample == true) throw new Error(`Could not get non-sample file. Make sure the track isn't purchase-only.`) - const streamResponse = await fetch(trackFileResponse.url) + const streamResponse = await fetch(trackFileResponse.url, { dispatcher: this.dispatcher }) return { mimeType: trackFileResponse.mime_type, sizeBytes: parseInt(streamResponse.headers.get('Content-Length')), diff --git a/src/streamers/soundcloud/main.ts b/src/streamers/soundcloud/main.ts index 069cfc9..e90b528 100644 --- a/src/streamers/soundcloud/main.ts +++ b/src/streamers/soundcloud/main.ts @@ -1,4 +1,4 @@ -import { fetch, HeadersInit } from 'undici' +import { Dispatcher, fetch, HeadersInit } from 'undici' import { DEFAULT_HEADERS } from './constants.js' import { ItemType, @@ -30,6 +30,7 @@ function headers(oauthToken?: string | undefined): HeadersInit { interface SoundcloudOptions { oauthToken?: string + dispatcher: Dispatcher | undefined } interface SoundcloudTranscoding { @@ -69,8 +70,10 @@ export default class Soundcloud implements Streamer { } as const oauthToken?: string client?: ScClient + dispatcher: Dispatcher | undefined constructor(options: SoundcloudOptions) { this.oauthToken = options?.oauthToken + this.dispatcher = options?.dispatcher } async search(query: string, limit = 20): Promise { const client = this.client || (await this.#getClient()) @@ -81,7 +84,7 @@ export default class Soundcloud implements Streamer { )}&offset=0&linked_partitioning=1&app_locale=en&limit=${limit}`, client ), - { method: 'get', headers: headers(this.oauthToken) } + { method: 'get', headers: headers(this.oauthToken), dispatcher: this.dispatcher } ) if (!response.ok) { const errMsg = await response.text() @@ -116,7 +119,8 @@ export default class Soundcloud implements Streamer { const response = await ( await fetch(`https://soundcloud.com/`, { method: 'get', - headers: headers() + headers: headers(this.oauthToken), + dispatcher: this.dispatcher }) ).text() @@ -163,7 +167,13 @@ export default class Soundcloud implements Streamer { url = url.replace('//m.', '//') // getting the IDs and track authorization - const html = await (await fetch(url, { method: 'get', headers: headers() })).text() + const html = await ( + await fetch(url, { + method: 'get', + headers: headers(this.oauthToken), + dispatcher: this.dispatcher + }) + ).text() switch (type) { case 'track': { @@ -177,7 +187,8 @@ export default class Soundcloud implements Streamer { await ( await fetch(this.#formatURL(naked, client), { method: 'get', - headers: headers(this.oauthToken) + headers: headers(this.oauthToken), + dispatcher: this.dispatcher }) ).text() ) @@ -248,7 +259,8 @@ export default class Soundcloud implements Streamer { await ( await fetch(this.#formatURL(`https://api-v2.soundcloud.com/tracks/${id}`, client), { method: 'get', - headers: headers(this.oauthToken) + headers: headers(this.oauthToken), + dispatcher: this.dispatcher }) ).text() ) @@ -265,7 +277,8 @@ export default class Soundcloud implements Streamer { ), { method: 'get', - headers: headers(this.oauthToken) + headers: headers(this.oauthToken), + dispatcher: this.dispatcher } ) ).json() diff --git a/src/streamers/tidal/main.ts b/src/streamers/tidal/main.ts index 53d83a2..085b1f4 100644 --- a/src/streamers/tidal/main.ts +++ b/src/streamers/tidal/main.ts @@ -1,4 +1,4 @@ -import { fetch } from 'undici' +import { Dispatcher, fetch } from 'undici' import { spawn } from 'child_process' import os from 'node:os' import fs from 'node:fs' @@ -32,6 +32,7 @@ interface TidalOptions { refreshToken: string expires: number countryCode: string + dispatcher?: Dispatcher } interface LoginData { @@ -71,6 +72,7 @@ export default class Tidal implements Streamer { expires: number countryCode: string userId: number | undefined + dispatcher: Dispatcher | undefined hostnames = ['tidal.com', 'www.tidal.com', 'listen.tidal.com'] testData = { 'https://tidal.com/browse/artist/3908662': { @@ -96,6 +98,7 @@ export default class Tidal implements Streamer { this.refreshToken = options.refreshToken this.expires = options.expires this.countryCode = options.countryCode + if (options.dispatcher) this.dispatcher = options.dispatcher const getReady = async () => { if (!this.refreshToken) return @@ -130,7 +133,8 @@ export default class Tidal implements Streamer { const response = await fetch( `${base}${url}?${new URLSearchParams(<{ [key: string]: string }>params)}`, { - headers: this.headers() + headers: this.headers(), + dispatcher: this.dispatcher } ) if (!response.ok) { @@ -156,13 +160,15 @@ export default class Tidal implements Streamer { } async sessionValid() { const resp = await fetch('https://api.tidal.com/v1/sessions', { - headers: this.headers() + headers: this.headers(), + dispatcher: this.dispatcher }) return resp.ok } async getCountryCode() { const sessionResponse = await fetch('https://api.tidal.com/v1/sessions', { - headers: this.headers() + headers: this.headers(), + dispatcher: this.dispatcher }) if (sessionResponse.status != 200) return false const sessionData = await sessionResponse.json() @@ -176,7 +182,8 @@ export default class Tidal implements Streamer { body: new URLSearchParams({ client_id: this.tvToken, scope: 'r_usr w_usr' - }) + }), + dispatcher: this.dispatcher }) if (deviceAuthResponse.status != 200) throw new Error(`Couldn't authorize Tidal`) interface DeviceAuth { @@ -199,7 +206,8 @@ export default class Tidal implements Streamer { await new Promise((r) => setTimeout(r, 1000)) const loginResponse = await fetch(`${TIDAL_AUTH_BASE}oauth2/token`, { method: 'post', - body: new URLSearchParams(params) + body: new URLSearchParams(params), + dispatcher: this.dispatcher }) statusCode = loginResponse.status loginData = await loginResponse.json() @@ -235,7 +243,8 @@ export default class Tidal implements Streamer { client_id: this.tvToken, client_secret: this.tvSecret, grant_type: 'refresh_token' - }) + }), + dispatcher: this.dispatcher }) if (refreshResponse.status == 200) { @@ -350,7 +359,7 @@ export default class Tidal implements Streamer { if (playbackInfoResponse.manifestMimeType != 'application/dash+xml') { const manifest = JSON.parse(manifestStr) - const streamResponse = await fetch(manifest.urls[0]) + const streamResponse = await fetch(manifest.urls[0], { dispatcher: this.dispatcher }) return { mimeType: manifest.mimeType, sizeBytes: parseInt(streamResponse.headers.get('Content-Length')), @@ -500,7 +509,8 @@ export default class Tidal implements Streamer { if (this.userId == undefined || this.countryCode == undefined) { if (Date.now() > this.expires) await this.refresh() const sessionResponse = await fetch('https://api.tidal.com/v1/sessions', { - headers: this.headers() + headers: this.headers(), + dispatcher: this.dispatcher }) const sessionData = await sessionResponse.json()