mirror of
https://codeberg.org/lucida/lucida.git
synced 2026-11-12 06:37:49 -05:00
add support for soundcloud non-progressive formats
This commit is contained in:
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user