mirror of
https://git.lucida.su/lucida/verdana.git
synced 2026-08-18 21:37:01 +00:00
improve quality selector for all
This commit is contained in:
parent
58ceea862f
commit
4d561bc2fd
6 changed files with 103 additions and 41 deletions
|
|
@ -580,7 +580,7 @@ export default class Deezer implements StreamerWithLogin {
|
|||
if (readableFormats[i].size != '0') {
|
||||
formats.push({
|
||||
id: readableFormats[i].format,
|
||||
rippable: true,
|
||||
rippable: this.availableFormats.has(readableFormats[i].format),
|
||||
size: parseInt(readableFormats[i].size),
|
||||
...formatHelper(readableFormats[i].format)
|
||||
} as Format)
|
||||
|
|
@ -598,6 +598,6 @@ function formatHelper(format: DeezerFormat) {
|
|||
case DeezerFormat.MP3_320:
|
||||
return { extension: 'mp3', mimeType: 'audio/mp3', bitrate: 320 }
|
||||
case DeezerFormat.FLAC:
|
||||
return { extension: 'flac', mimeType: 'audio/flac' }
|
||||
return { extension: 'flac', mimeType: 'audio/flac', bitDepth: 16 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export default class Qobuz implements StreamerWithLogin {
|
|||
}
|
||||
}
|
||||
|
||||
async #getFileUrl(trackId: string, formatId: number = 27): Promise<GetStreamResponse> {
|
||||
async #getFileUrl(trackId: string, formatId: string | number): Promise<GetStreamResponse> {
|
||||
if (!this.token) throw new Error('Not logged in.')
|
||||
|
||||
const params: { [key: string]: string } = {
|
||||
|
|
@ -298,8 +298,9 @@ export default class Qobuz implements StreamerWithLogin {
|
|||
case 'track': {
|
||||
return {
|
||||
type,
|
||||
getStream: () => {
|
||||
return this.#getFileUrl(id)
|
||||
getStream: (formatId?: string | number) => {
|
||||
if (!formatId) formatId = 27
|
||||
return this.#getFileUrl(id, formatId)
|
||||
},
|
||||
metadata: await this.#getTrackMetadata(id)
|
||||
}
|
||||
|
|
@ -384,7 +385,7 @@ export default class Qobuz implements StreamerWithLogin {
|
|||
// slice by maximum rate provided by qobuz
|
||||
let maxIndex: number | undefined
|
||||
if (rawTrack.maximum_bit_depth == 16) maxIndex = 2
|
||||
else if (rawTrack.maximum_bit_depth == 24) maxIndex = 3
|
||||
else if (rawTrack.maximum_bit_depth == 24 && rawTrack.maximum_sampling_rate == 96) maxIndex = 3
|
||||
else if (rawTrack.maximum_sampling_rate == 192) maxIndex = 4
|
||||
else maxIndex = 1
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,8 @@
|
|||
import { TidalFormat } from './parse.js'
|
||||
|
||||
export const TIDAL_AUTH_BASE = 'https://auth.tidal.com/v1/'
|
||||
export const TIDAL_API_BASE = 'https://listen.tidal.com/v1/'
|
||||
export const TIDAL_LISTEN_BASE = 'https://listen.tidal.com/v1/'
|
||||
export const TIDAL_SUBSCRIPTION_BASE = 'https://api.tidal.com/v1/'
|
||||
|
||||
export const TIDAL_FORMATS = [
|
||||
'LOW',
|
||||
'HIGH',
|
||||
'LOSSLESS',
|
||||
'HI_RES',
|
||||
'HI_RES_LOSSLESS'
|
||||
] as TidalFormat[]
|
||||
export const TIDAL_NORMAL_FORMATS = ['LOW', 'HIGH', 'LOSSLESS', 'HI_RES_LOSSLESS'] as TidalFormat[]
|
||||
export const TIDAL_SPATIAL_FORMATS = ['DOLBY_ATMOS', 'SONY_360RA']
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// todo: add atmos support for format selection
|
||||
// might have to do it how orpheusdl does it?
|
||||
// todo: possible refactor
|
||||
|
||||
// https://github.com/Dniel97/orpheusdl-tidal/blob/master/interface.py#L41
|
||||
// https://github.com/Radexito/tidal-dl-ng-For-DJ/blob/87ec210dfeeef23441b7c99a16123a25ec63f207/tidal_dl_ng/config.py#L177
|
||||
|
||||
import { Dispatcher, fetch } from 'undici'
|
||||
import { spawn } from 'child_process'
|
||||
|
|
@ -19,9 +19,9 @@ import {
|
|||
} from '../../types.js'
|
||||
import {
|
||||
TIDAL_AUTH_BASE,
|
||||
TIDAL_API_BASE,
|
||||
TIDAL_LISTEN_BASE,
|
||||
TIDAL_SUBSCRIPTION_BASE,
|
||||
TIDAL_FORMATS
|
||||
TIDAL_NORMAL_FORMATS
|
||||
} from './constants.js'
|
||||
import {
|
||||
Contributor,
|
||||
|
|
@ -45,11 +45,20 @@ interface TidalOptions {
|
|||
accessToken: string
|
||||
refreshToken: string
|
||||
expires: number
|
||||
countryCode: string
|
||||
countryCode?: string
|
||||
atmos?: Client
|
||||
sessionId?: string
|
||||
dispatcher?: Dispatcher
|
||||
}
|
||||
|
||||
interface Client {
|
||||
tvToken: string
|
||||
tvSecret: string
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
expires: number
|
||||
}
|
||||
|
||||
interface LoginData {
|
||||
error: string
|
||||
access_token: string
|
||||
|
|
@ -73,7 +82,7 @@ interface SubscriptionData {
|
|||
type: string
|
||||
offlineGracePeriod: number
|
||||
}
|
||||
highestSoundQuality: TidalFormat
|
||||
highestSoundQuality: 'HI_RES' | 'LOSSLESS'
|
||||
premiumAccess: boolean
|
||||
canGetTrial: boolean
|
||||
paymentType: string
|
||||
|
|
@ -99,16 +108,17 @@ interface RawIsrcLookup {
|
|||
}
|
||||
|
||||
export default class Tidal implements Streamer {
|
||||
mode: 'atmos' | 'normal' = 'normal'
|
||||
tvToken: string
|
||||
tvSecret: string
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
expires: number
|
||||
countryCode: string
|
||||
countryCode: string | undefined
|
||||
userId: number | undefined
|
||||
dispatcher: Dispatcher | undefined
|
||||
sessionId: string | undefined
|
||||
highestQuality: TidalFormat | undefined
|
||||
highestQuality: 'HI_RES' | 'LOSSLESS' | undefined
|
||||
hostnames = ['tidal.com', 'www.tidal.com', 'listen.tidal.com']
|
||||
testData = {
|
||||
'https://tidal.com/browse/artist/3908662': {
|
||||
|
|
@ -129,12 +139,15 @@ export default class Tidal implements Streamer {
|
|||
this.tvToken = options.tvToken
|
||||
this.tvSecret = options.tvSecret
|
||||
|
||||
// this.atmosToken = options.atmosToken
|
||||
// this.atmosSecret = options.atmosSecret
|
||||
|
||||
// TODO: ideally this should be stored in a .config file
|
||||
this.accessToken = options.accessToken
|
||||
this.refreshToken = options.refreshToken
|
||||
this.expires = options.expires
|
||||
this.countryCode = options.countryCode
|
||||
this.sessionId = options.sessionId
|
||||
if (options.countryCode) this.countryCode = options.countryCode
|
||||
if (options.sessionId) this.sessionId = options.sessionId
|
||||
if (options.dispatcher) this.dispatcher = options.dispatcher
|
||||
}
|
||||
headers() {
|
||||
|
|
@ -153,7 +166,7 @@ export default class Tidal implements Streamer {
|
|||
async #get(
|
||||
url: string,
|
||||
params: { [key: string]: string | number } = {},
|
||||
base: string = TIDAL_API_BASE
|
||||
base: string = TIDAL_LISTEN_BASE
|
||||
): Promise<unknown> {
|
||||
if (this.failedAuth) throw new Error(`Last request failed to authorize, get new tokens`)
|
||||
if (Date.now() > this.expires) await this.refresh()
|
||||
|
|
@ -269,13 +282,17 @@ export default class Tidal implements Streamer {
|
|||
countryCode: this.countryCode
|
||||
}
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
// if (!clientId) clientId = this.tvToken
|
||||
// if (!clientSecret) clientSecret = this.tvSecret
|
||||
|
||||
const refreshResponse = await fetch(`${TIDAL_AUTH_BASE}oauth2/token`, {
|
||||
method: 'post',
|
||||
body: new URLSearchParams({
|
||||
refresh_token: this.refreshToken,
|
||||
client_id: this.tvToken,
|
||||
client_secret: this.tvSecret,
|
||||
client_secret: this.tvToken,
|
||||
grant_type: 'refresh_token'
|
||||
}),
|
||||
dispatcher: this.dispatcher
|
||||
|
|
@ -378,6 +395,14 @@ export default class Tidal implements Streamer {
|
|||
audioQuality: TidalFormat
|
||||
}
|
||||
|
||||
if (format == 'DOLBY_ATMOS') {
|
||||
// switch to atmos client
|
||||
|
||||
throw new Error('Not implemented.')
|
||||
format = 'LOW'
|
||||
// await this.refresh(this.atmosToken, this.atmosSecret)
|
||||
}
|
||||
|
||||
if (!this.sessionId) await this.getAccountInfo()
|
||||
|
||||
const playbackInfoResponse = <PlaybackInfo>await this.#get(
|
||||
|
|
@ -524,6 +549,7 @@ export default class Tidal implements Streamer {
|
|||
) {
|
||||
throw new Error('URL unrecognised')
|
||||
}
|
||||
if (urlParts[1].includes('/')) urlParts[1] = urlParts[1].split('/')[0]
|
||||
return [urlParts[0], urlParts[1]]
|
||||
}
|
||||
async getTypeFromUrl(url: string): Promise<ItemType> {
|
||||
|
|
@ -591,27 +617,39 @@ export default class Tidal implements Streamer {
|
|||
if (type != 'track') throw new Error('URL unrecognized')
|
||||
|
||||
// get raw track info
|
||||
const trackResponse = <RawTrack>await this.#get(`tracks/${id}`)
|
||||
const highestTrackQuality = TIDAL_FORMATS.indexOf(trackResponse.audioQuality) + 1
|
||||
const trackResponse = (await this.#get(`tracks/${id}`)) as RawTrack
|
||||
|
||||
// get the highest quality
|
||||
let highestTrackQuality = 1
|
||||
if (trackResponse.mediaMetadata.tags.includes('HIRES_LOSSLESS')) highestTrackQuality = 4
|
||||
else if (trackResponse.mediaMetadata.tags.includes('LOSSLESS')) highestTrackQuality = 3
|
||||
|
||||
// create formats array
|
||||
let formats: Format[] = []
|
||||
if (trackResponse.mediaMetadata.tags.includes('DOLBY_ATMOS')) {
|
||||
// handle atmos
|
||||
formats.push({
|
||||
id: 'DOLBY_ATMOS', // psudeo id, not actually handled by tidal
|
||||
rippable: true,
|
||||
...formatHelper('DOLBY_ATMOS')
|
||||
})
|
||||
return formats
|
||||
}
|
||||
|
||||
// if we don't have it already, get the account highest quality, to mark certain tracks as rippable
|
||||
if (!this.highestQuality) await this.getAccountInfo()
|
||||
const highestAccountQuality = TIDAL_FORMATS.indexOf(this.highestQuality as TidalFormat) + 1
|
||||
let highestAccountQuality = 1
|
||||
if (this.highestQuality == 'HI_RES') highestAccountQuality = 5
|
||||
else if (this.highestQuality == 'LOSSLESS') highestAccountQuality = 4
|
||||
|
||||
let formats: Format[] = []
|
||||
const acceptable = TIDAL_FORMATS.slice(0, highestTrackQuality)
|
||||
// delete formats we dont have
|
||||
const acceptable = TIDAL_NORMAL_FORMATS.slice(0, highestTrackQuality)
|
||||
|
||||
for (const i in acceptable) {
|
||||
let bitrate
|
||||
if (acceptable[i] == 'LOW') bitrate = 96
|
||||
if (acceptable[i] == 'HIGH') bitrate = 320
|
||||
|
||||
formats.push({
|
||||
id: acceptable[i] as TidalFormat,
|
||||
rippable: parseInt(i) < highestAccountQuality,
|
||||
mimeType: acceptable[i] == 'LOW' || acceptable[i] == 'HIGH' ? 'audio/mp4' : 'audio/flac',
|
||||
extension: acceptable[i] == 'LOW' || acceptable[i] == 'HIGH' ? 'm4a' : 'flac',
|
||||
bitrate
|
||||
rippable: parseInt(i) <= highestAccountQuality,
|
||||
...formatHelper(acceptable[i])
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -620,6 +658,8 @@ export default class Tidal implements Streamer {
|
|||
}
|
||||
|
||||
function getExtensionFromMime(mime: string) {
|
||||
mime = mime.split(';')[0]
|
||||
|
||||
switch (mime) {
|
||||
case 'audio/mp4':
|
||||
return 'm4a'
|
||||
|
|
@ -629,3 +669,20 @@ function getExtensionFromMime(mime: string) {
|
|||
throw new Error(`Unable to resolve extension from mime type: ${mime}`)
|
||||
}
|
||||
}
|
||||
|
||||
function formatHelper(format: TidalFormat) {
|
||||
switch (format) {
|
||||
case 'LOW':
|
||||
return { extension: 'm4a', mimeType: 'audio/m4a', bitrate: 96, spatial: false }
|
||||
case 'HIGH':
|
||||
return { extension: 'm4a', mimeType: 'audio/m4a', bitrate: 320, spatial: false }
|
||||
case 'LOSSLESS':
|
||||
return { extension: 'flac', mimeType: 'audio/flac', bitDepth: 16, spatial: false }
|
||||
case 'HI_RES_LOSSLESS':
|
||||
return { extension: 'flac', mimeType: 'audio/flac', bitDepth: 24, spatial: false }
|
||||
case 'DOLBY_ATMOS':
|
||||
return { extension: 'ac3', mimeType: 'audio/ac3', rippable: false, spatial: true }
|
||||
case 'SONY_360RA':
|
||||
return { extension: '360ra', spatial: true }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { Album, Artist, Playlist, Track } from '../../types.js'
|
||||
import { DOMParser } from 'xmldom-qsa'
|
||||
|
||||
export type TidalFormat = 'LOW' | 'HIGH' | 'LOSSLESS' | 'HI_RES' | 'HI_RES_LOSSLESS'
|
||||
export type TidalFormat =
|
||||
| 'LOW'
|
||||
| 'HIGH'
|
||||
| 'LOSSLESS'
|
||||
| 'HI_RES_LOSSLESS'
|
||||
| 'DOLBY_ATMOS'
|
||||
| 'SONY_360RA'
|
||||
|
||||
export interface RawArtist {
|
||||
id: string
|
||||
|
|
@ -140,8 +146,11 @@ export interface RawTrack {
|
|||
version?: string
|
||||
album: RawAlbum
|
||||
audioQuality: TidalFormat
|
||||
mediaMetadata: { tags: MediaMetadataTags[] }
|
||||
}
|
||||
|
||||
type MediaMetadataTags = 'LOSSLESS' | 'HIRES_LOSSLESS' | 'DOLBY_ATMOS' | 'SONY_360RA'
|
||||
|
||||
export function parseTrack(raw: RawTrack): Track {
|
||||
const track: Track = {
|
||||
url: raw.url,
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ export interface Format {
|
|||
sampleRate?: number
|
||||
bitDepth?: number // {x}-bit FLAC
|
||||
bitrate?: number // {x}kbps MP3
|
||||
atmos?: boolean
|
||||
spatial?: boolean // sony 360ra or dolby atmos
|
||||
}
|
||||
|
||||
export interface GetStreamResponse {
|
||||
|
|
|
|||
Loading…
Reference in a new issue