add dispatcher support for all platforms

This commit is contained in:
aria
2024-08-05 01:04:01 -04:00
parent 99df8075b1
commit d1c262e31e
4 changed files with 65 additions and 26 deletions

View File

@@ -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<DeezerFormat> = new Set()
constructor(options?: DeezerOptions) {
if (options?.arl) this.arl = options.arl
//if (options?.dispatcher) this.dispatcher = options.dispatcher
}
async #apiCall<T extends keyof APIMethod>(
@@ -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 } = <DeezerResponse>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<void> {
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 } = <DeezerLoginResponse>await loginReq.json()
@@ -242,7 +253,7 @@ export default class Deezer implements StreamerWithLogin {
/* ---------- GET INFO FROM URL ---------- */
async #unshortenUrl(url: URL): Promise<URL> {
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 = <DeezerMediaResponse>await req.json()

View File

@@ -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<Response>
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(<string>streamResponse.headers.get('Content-Length')),

View File

@@ -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<SearchResults> {
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()

View File

@@ -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 = <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 = <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 = <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(<string>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 = <SessionData>await sessionResponse.json()