mirror of
https://codeberg.org/lucida/lucida.git
synced 2026-11-12 22:57:22 -05:00
fix soundcloud hls ripping
This commit is contained in:
1499
pnpm-lock.yaml
generated
1499
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user