fix soundcloud hls ripping

This commit is contained in:
aria
2024-07-07 21:56:01 -04:00
parent fbc2cd3fa8
commit 0b111f3b0f
3 changed files with 887 additions and 741 deletions

1499
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -156,8 +156,10 @@ export default class Soundcloud implements Streamer {
return {
type: 'track',
getStream: async () => {
getStream: async (hq?: boolean) => {
if (!hq) hq = false
return await getStream(
hq,
api.media.transcodings,
api.track_authorization,
client,
@@ -253,53 +255,43 @@ async function fetchKey(response: string) {
}
async function getStream(
hq: boolean,
transcodings: Array<SoundcloudTranscoding>,
trackAuth: string,
client: ScClient,
oauthToken?: string | undefined
): Promise<GetStreamResponse> {
const progressive = transcodings.filter((x) => x.format?.protocol == 'progressive')[0]
let filter = transcodings.filter((x) => x.quality == 'hq')
if (hq == true && filter.length == 0) throw new Error('Could not find HQ format.')
if (filter.length == 0) filter = transcodings.filter((x) => x.preset.startsWith('aac_')) // prioritize aac (go+)
if (filter.length == 0) filter = transcodings.filter((x) => x.preset.startsWith('opus_')) // then opus
if (filter.length == 0) filter = transcodings.filter((x) => x.preset.startsWith('mp3_')) // then mp3
if (filter.length == 0) throw new Error('Could not find applicable format.') // and this is just in case none of those exist
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 transcoding = filter[0]
const streamUrlResp = await fetch(
`${transcoding.url}?client_id=${client.id}&track_authorization=${trackAuth}`,
{
headers: headers(oauthToken)
}
)
const json = <{ url: string }>await streamUrlResp.json()
if (!json.url) throw new Error('Stream URL could not be retreieved.')
if (transcoding.format.protocol == 'progressive') {
const streamResp = await fetch(json.url)
return {
mimeType: progressive.format.mime_type,
mimeType: transcoding.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]
const container = transcoding.format.mime_type.split('/')[1].split(';')[0].split('+')[0]
return {
mimeType: selected.format.mime_type,
stream: await parseHls(json.url, codec)
mimeType: transcoding.format.mime_type,
stream: (await parseHls(json.url, container))
}
}
}

View File

@@ -2,7 +2,9 @@ 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'
import os from 'node:os'
import fs from 'node:fs'
import path from 'node:path'
async function parseCoverArtwork(url: string) {
const resp = await fetch(url)
@@ -101,49 +103,34 @@ 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()
export async function parseHls(url: string, container: string): Promise<NodeJS.ReadableStream> {
return new Promise(async function(resolve, reject) {
const folder = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'lucida'))
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 ffmpegProc = spawn('ffmpeg', [
'-hide_banner',
'-loglevel',
'error',
'-i',
url,
'-c:a',
'copy',
'-f',
container,
`${folder}/hls.${container}`
])
const stream = new Stream.Readable({
// eslint-disable-next-line @typescript-eslint/no-empty-function
read() {}
let err: string
ffmpegProc.stderr.on('data', function(data) {
err = data.toString()
})
ffmpegProc.once('exit', function(code) {
if (code == 0) resolve(fs.createReadStream(`${folder}/hls.${container}`))
else reject((`FFMPEG HLS error: ${err}` || 'FFMPEG could not parse the HLS.'))
})
})
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 {