mirror of
https://git.gay/lucida/lucida
synced 2025-12-11 20:15:14 +01:00
improvement to soundcloud's get client function, add aac support to tidal
This commit is contained in:
parent
5b87eebb34
commit
528778f983
2
.gitignore
vendored
2
.gitignore
vendored
@ -7,7 +7,7 @@ node_modules
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
/test.js
|
||||
/test.*
|
||||
/tmp
|
||||
/run.js
|
||||
|
||||
|
||||
@ -12,4 +12,3 @@ export const DEFAULT_HEADERS = {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36'
|
||||
}
|
||||
export const SC_VERSION = '1712837702'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { fetch, HeadersInit } from 'undici'
|
||||
import { DEFAULT_HEADERS, SC_VERSION } from './constants.js'
|
||||
import { DEFAULT_HEADERS } from './constants.js'
|
||||
import {
|
||||
ItemType,
|
||||
Streamer,
|
||||
@ -44,6 +44,17 @@ interface SoundcloudTranscoding {
|
||||
quality: string
|
||||
}
|
||||
|
||||
interface SoundCloudSubscriptionData {
|
||||
active_subscription: {
|
||||
state: string
|
||||
subscription_period_started_at: string
|
||||
expires_at: string
|
||||
recurring: boolean
|
||||
trial: boolean
|
||||
is_eligible: boolean
|
||||
} | null
|
||||
}
|
||||
|
||||
export default class Soundcloud implements Streamer {
|
||||
hostnames = ['soundcloud.com', 'm.soundcloud.com', 'www.soundcloud.com']
|
||||
testData = {
|
||||
@ -110,7 +121,7 @@ export default class Soundcloud implements Streamer {
|
||||
).text()
|
||||
|
||||
const client = {
|
||||
version: SC_VERSION,
|
||||
version: response.split(`__sc_version="`)[1].split(`"</script>`)[0],
|
||||
anonId: response.split(`[{"hydratable":"anonymousId","data":"`)[1].split(`"`)[0],
|
||||
id: await fetchKey(response)
|
||||
}
|
||||
@ -135,7 +146,7 @@ export default class Soundcloud implements Streamer {
|
||||
#formatURL(og: string, client: ScClient): string {
|
||||
const parsed = new URL(og)
|
||||
|
||||
if (client.anonId) parsed.searchParams.append('user_id', client.anonId)
|
||||
if (client.anonId && !this.oauthToken) parsed.searchParams.append('user_id', client.anonId)
|
||||
if (client.id) parsed.searchParams.append('client_id', client.id)
|
||||
if (client.version) parsed.searchParams.append('app_version', client.version)
|
||||
if (!parsed.searchParams.get('app_locale')) parsed.searchParams.append('app_locale', 'en')
|
||||
@ -245,14 +256,25 @@ export default class Soundcloud implements Streamer {
|
||||
return { ...api, id }
|
||||
}
|
||||
async getAccountInfo(): Promise<StreamerAccount> {
|
||||
const track = <TrackGetByUrlResponse>(
|
||||
await this.getByUrl('https://soundcloud.com/ween/polka-dot-tail')
|
||||
const client = this.client || (await this.#getClient())
|
||||
const subscriptionQuery = <SoundCloudSubscriptionData>await (
|
||||
await fetch(
|
||||
this.#formatURL(
|
||||
`https://api-v2.soundcloud.com/payments/quotations/consumer-subscription`,
|
||||
client
|
||||
),
|
||||
{
|
||||
method: 'get',
|
||||
headers: headers(this.oauthToken)
|
||||
}
|
||||
)
|
||||
const stream = await track.getStream()
|
||||
if (stream.mimeType.startsWith('audio/mp4')) {
|
||||
stream.stream.unpipe()
|
||||
return { valid: true, premium: true, explicit: true }
|
||||
} else return { valid: true, premium: false, explicit: true }
|
||||
).json()
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
premium: subscriptionQuery?.active_subscription?.state == 'active',
|
||||
explicit: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { fetch } from 'undici'
|
||||
import { spawn } from 'child_process'
|
||||
import os from 'node:os'
|
||||
import fs from 'node:fs'
|
||||
import {
|
||||
ItemType,
|
||||
GetByUrlResponse,
|
||||
@ -329,6 +331,7 @@ export default class Tidal implements Streamer {
|
||||
manifestMimeType: string
|
||||
audioQuality: 'LOW' | 'HIGH' | 'LOSSLESS' | 'HI_RES' | 'HI_RES_LOSSLESS'
|
||||
}
|
||||
|
||||
const playbackInfoResponse = <PlaybackInfo>await this.#get(
|
||||
`tracks/${trackId}/playbackinfopostpaywall/v4`,
|
||||
{
|
||||
@ -339,9 +342,6 @@ export default class Tidal implements Streamer {
|
||||
}
|
||||
)
|
||||
|
||||
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 {
|
||||
mimeType: string
|
||||
@ -359,20 +359,13 @@ export default class Tidal implements Streamer {
|
||||
}
|
||||
|
||||
const trackUrls = parseMpd(manifestStr)
|
||||
const args = await this.#getFfArgs(playbackInfoResponse.audioQuality)
|
||||
|
||||
const ffmpegProc = spawn('ffmpeg', [
|
||||
'-hide_banner',
|
||||
'-loglevel',
|
||||
'error',
|
||||
'-i',
|
||||
'-',
|
||||
'-c:a',
|
||||
'copy',
|
||||
'-f',
|
||||
'flac',
|
||||
'-'
|
||||
])
|
||||
|
||||
const ffmpegProc = spawn('ffmpeg', args)
|
||||
switch (playbackInfoResponse.audioQuality) {
|
||||
case 'LOW':
|
||||
case 'HIGH': {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const stream = new Stream.Readable({
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
read() {}
|
||||
@ -388,7 +381,53 @@ export default class Tidal implements Streamer {
|
||||
stream.push(null)
|
||||
}
|
||||
stream.pipe(ffmpegProc.stdin)
|
||||
ffmpegProc.stderr.pipe(process.stderr)
|
||||
|
||||
let err: string
|
||||
|
||||
ffmpegProc.stderr.on('data', function (data) {
|
||||
err = data.toString()
|
||||
})
|
||||
|
||||
load()
|
||||
|
||||
ffmpegProc.once('exit', async function (code) {
|
||||
const folder = args[args.length - 1]
|
||||
.split('/')
|
||||
.slice(0, args[args.length - 1].split('/').length - 1)
|
||||
.join('/')
|
||||
if (code == 0)
|
||||
resolve({
|
||||
mimeType: 'audio/mp4',
|
||||
stream: fs.createReadStream(args[args.length - 1]).once('end', async function () {
|
||||
await fs.promises.rm(folder, { recursive: true })
|
||||
})
|
||||
})
|
||||
else {
|
||||
reject(`FFMPEG error: ${err}` || 'FFMPEG could not handle the file.')
|
||||
await fs.promises.rm(folder, { recursive: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
default: {
|
||||
const stream = new Stream.Readable({
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
read() {}
|
||||
})
|
||||
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
async function load() {
|
||||
for (const url of trackUrls) {
|
||||
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)
|
||||
}
|
||||
stream.pipe(ffmpegProc.stdin)
|
||||
load()
|
||||
|
||||
return {
|
||||
@ -396,6 +435,29 @@ export default class Tidal implements Streamer {
|
||||
stream: ffmpegProc.stdout
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async #getFfArgs(audioQuality: 'LOW' | 'HIGH' | 'LOSSLESS' | 'HI_RES' | 'HI_RES_LOSSLESS') {
|
||||
switch (audioQuality) {
|
||||
case 'LOW':
|
||||
case 'HIGH':
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const folder = await fs.promises.mkdtemp(`${os.tmpdir()}/lucida`)
|
||||
return [
|
||||
'-hide_banner',
|
||||
'-loglevel',
|
||||
'error',
|
||||
'-i',
|
||||
'-',
|
||||
'-c:a',
|
||||
'copy',
|
||||
'-y',
|
||||
`${folder}/data.m4a`
|
||||
]
|
||||
default:
|
||||
return ['-hide_banner', '-loglevel', 'error', '-i', '-', '-c:a', 'copy', '-f', 'flac', '-']
|
||||
}
|
||||
}
|
||||
#getUrlParts(url: string): ['artist' | 'album' | 'track', string] {
|
||||
const urlParts = url
|
||||
.match(/^https?:\/\/(?:www\.|listen\.)?tidal\.com\/(?:browse\/)?(.*?)\/(.*?)\/?$/)
|
||||
|
||||
@ -24,9 +24,7 @@ if (!process.env.TEST_OPTIONS) {
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
} else {
|
||||
testOptions = JSON.parse(process.env.TEST_OPTIONS)
|
||||
}
|
||||
} else testOptions = JSON.parse(process.env.TEST_OPTIONS)
|
||||
|
||||
async function start() {
|
||||
const lucidaOptions: LucidaOptions = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user