diff --git a/src/streamers/qobuz/main.ts b/src/streamers/qobuz/main.ts index d69e81a..ad85809 100644 --- a/src/streamers/qobuz/main.ts +++ b/src/streamers/qobuz/main.ts @@ -91,19 +91,20 @@ export default class Qobuz implements StreamerWithLogin { } async login(username: string, password: string) { - if (this.token) return - const params: { [key: string]: string } = { - username, - password: md5(password), - extra: 'partner', - app_id: this.appId - } + if (!this.token) { + const params: { [key: string]: string } = { + username, + password: md5(password), + extra: 'partner', + app_id: this.appId + } - interface LoginResponse { - user_auth_token: string + interface LoginResponse { + user_auth_token: string + } + const loginResponse = await this.#getSigned('user/login', params) + this.token = loginResponse.user_auth_token } - const loginResponse = await this.#getSigned('user/login', params) - this.token = loginResponse.user_auth_token } async search(query: string, limit = 10): Promise { diff --git a/src/streamers/soundcloud/main.ts b/src/streamers/soundcloud/main.ts index 5226057..1eb24e3 100644 --- a/src/streamers/soundcloud/main.ts +++ b/src/streamers/soundcloud/main.ts @@ -11,6 +11,7 @@ import { parseAlbum, parseTrack, parseArtist, + parseHls, RawSearchResults, RawTrack, RawAlbum, @@ -258,17 +259,47 @@ async function getStream( oauthToken?: string | undefined ): Promise { const progressive = transcodings.filter((x) => x.format?.protocol == 'progressive')[0] - const streamUrlResp = await fetch( - `${progressive.url}?client_id=${client.id}&track_authorization=${trackAuth}`, - { - headers: headers(oauthToken) + + if (progressive?.url) { + const streamUrlResp = await fetch( + `${progressive.url}?client_id=${client.id}&track_authorization=${trackAuth}`, + { + headers: headers(oauthToken) + } + ) + const json = <{ url: string }>await streamUrlResp.json() + const streamResp = await fetch(json.url) + return { + mimeType: progressive.format.mime_type, + sizeBytes: parseInt(streamResp.headers.get('Content-Length')), + stream: streamResp.body + } + } else { + const filteredTranscodings = transcodings.filter((x) => x?.preset.startsWith('opus_')) + let url + let selected + + if (filteredTranscodings[filteredTranscodings.length - 1]?.url) { + url = filteredTranscodings[filteredTranscodings.length - 1]?.url + selected = filteredTranscodings[filteredTranscodings.length - 1] + } else { + url = transcodings[0]?.url + selected = transcodings[0] + } + + const streamUrlResp = await fetch( + `${url}?client_id=${client.id}&track_authorization=${trackAuth}`, + { + headers: headers(oauthToken) + } + ) + + const json = <{ url: string }>await streamUrlResp.json() + const codec = selected.format.mime_type.split('/')[1].split(';')[0].split(' ')[0] + + return { + mimeType: selected.format.mime_type, + stream: await parseHls(json.url, codec) } - ) - const json = <{ url: string }>await streamUrlResp.json() - const streamResp = await fetch(json.url) - return { - mimeType: progressive.format.mime_type, - sizeBytes: parseInt(streamResp.headers.get('Content-Length')), - stream: streamResp.body } } diff --git a/src/streamers/soundcloud/parse.ts b/src/streamers/soundcloud/parse.ts index 44c5224..74ed45f 100644 --- a/src/streamers/soundcloud/parse.ts +++ b/src/streamers/soundcloud/parse.ts @@ -1,6 +1,8 @@ import { Artist, Album, Track } from '../../types.js' +import { spawn } from 'child_process' import fetch from 'node-fetch' import { imageSize as sizeOf } from 'image-size' +import Stream from 'stream' async function parseCoverArtwork(url: string) { const resp = await fetch(url) @@ -99,6 +101,51 @@ export async function parseTrack(raw: RawTrack): Promise { return track } +export async function parseHls(url: string, codec: string): Promise { + const m3u8 = await (await fetch(url)).text() + + const urls = [] + for (const i in m3u8.split('\n')) { + if (m3u8.split('\n')[i].startsWith('#')) continue + else urls.push(new URL(m3u8.split('\n')[i])) + } + + const stream = new Stream.Readable({ + // eslint-disable-next-line @typescript-eslint/no-empty-function + read() {} + }) + + async function load() { + for (const url of urls) { + const resp = await fetch(url) + if (!resp.body) throw new Error('Response has no body') + for await (const chunk of resp.body) { + stream.push(chunk) + } + } + stream.push(null) + } + + const ffmpegProc = spawn('ffmpeg', [ + '-hide_banner', + '-loglevel', + 'error', + '-i', + '-', + '-c:a', + 'copy', + '-f', + codec, + '-' + ]) + + stream.pipe(ffmpegProc.stdin) + ffmpegProc.stderr.pipe(process.stderr) + load() + + return stream +} + export interface RawSearchResults { collection: (RawAlbum | RawTrack | RawArtist)[] total_results: number diff --git a/src/streamers/tidal/main.ts b/src/streamers/tidal/main.ts index 80d3081..9de6ac0 100644 --- a/src/streamers/tidal/main.ts +++ b/src/streamers/tidal/main.ts @@ -277,11 +277,9 @@ export default class Tidal implements Streamer { prefetch: 'false' } ) - - if ( - playbackInfoResponse.audioQuality == 'HIGH' || - playbackInfoResponse.audioQuality == 'LOW' - ) throw new Error('This ripper is incompatible with AAC codecs formats at the moment.') + + if (playbackInfoResponse.audioQuality == 'HIGH' || playbackInfoResponse.audioQuality == 'LOW') + throw new Error('This ripper is incompatible with AAC codecs formats at the moment.') const manifestStr = Buffer.from(playbackInfoResponse.manifest, 'base64').toString('utf-8') interface Manifest {