add support for soundcloud non-progressive formats

This commit is contained in:
aria
2024-05-05 22:53:15 -04:00
parent cd6a8c69ff
commit f4b2e61db1
4 changed files with 104 additions and 27 deletions

View File

@@ -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 = <LoginResponse>await this.#getSigned('user/login', params)
this.token = loginResponse.user_auth_token
}
const loginResponse = <LoginResponse>await this.#getSigned('user/login', params)
this.token = loginResponse.user_auth_token
}
async search(query: string, limit = 10): Promise<SearchResults> {

View File

@@ -11,6 +11,7 @@ import {
parseAlbum,
parseTrack,
parseArtist,
parseHls,
RawSearchResults,
RawTrack,
RawAlbum,
@@ -258,17 +259,47 @@ async function getStream(
oauthToken?: string | undefined
): Promise<GetStreamResponse> {
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(<string>streamResp.headers.get('Content-Length')),
stream: <NodeJS.ReadableStream>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(<string>streamResp.headers.get('Content-Length')),
stream: <NodeJS.ReadableStream>streamResp.body
}
}

View File

@@ -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<Track> {
return track
}
export async function parseHls(url: string, codec: string): Promise<NodeJS.ReadableStream> {
const m3u8 = await (await fetch(url)).text()
const urls = <URL[]>[]
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

View File

@@ -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 {