mirror of
https://git.gay/lucida/lucida
synced 2025-12-11 20:15:14 +01:00
add deezer module
Co-authored-by: bernzrdo <bernardovs2003@gmail.com>
This commit is contained in:
parent
bdfd0d1b18
commit
a5a254467a
@ -29,6 +29,7 @@
|
||||
},
|
||||
"license": "OQL",
|
||||
"dependencies": {
|
||||
"blowfish-cbc": "^1.0.1",
|
||||
"image-size": "^1.1.1",
|
||||
"librespot": "^0.2.4",
|
||||
"node-fetch": "^3.3.2",
|
||||
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
blowfish-cbc:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1
|
||||
image-size:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
@ -225,6 +228,9 @@ packages:
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
blowfish-cbc@1.0.1:
|
||||
resolution: {integrity: sha512-o1JN6g6+ATW/4k7q1BZzy14VqLxwYa1mCii47qT4kmAaYD0NAfdM6/pz6uizbhTra/xunsPQI27LZt08OQS4sA==}
|
||||
|
||||
brace-expansion@1.1.11:
|
||||
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
||||
|
||||
@ -503,6 +509,10 @@ packages:
|
||||
node-addon-api@1.7.2:
|
||||
resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==}
|
||||
|
||||
node-addon-api@8.1.0:
|
||||
resolution: {integrity: sha512-yBY+qqWSv3dWKGODD6OGE6GnTX7Q2r+4+DfpqxHSHh8x0B4EKP9+wVGLS6U/AM1vxSNNmUEuIV5EGhYwPpfOwQ==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
@ -879,6 +889,10 @@ snapshots:
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
blowfish-cbc@1.0.1:
|
||||
dependencies:
|
||||
node-addon-api: 8.1.0
|
||||
|
||||
brace-expansion@1.1.11:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
@ -1182,6 +1196,8 @@ snapshots:
|
||||
|
||||
node-addon-api@1.7.2: {}
|
||||
|
||||
node-addon-api@8.1.0: {}
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@3.3.2:
|
||||
|
||||
6
src/streamers/deezer/constants.ts
Normal file
6
src/streamers/deezer/constants.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const GW_LIGHT_URL = 'https://www.deezer.com/ajax/gw-light.php'
|
||||
export const CLIENT_ID = '447462'
|
||||
export const CLIENT_SECRET = 'a83bf7f38ad2f137e444727cfc3775cf'
|
||||
export const SIZES = [32, 64, 128, 256, 512, 1024]
|
||||
|
||||
export const BLOWFISH_SECRET = 'g4el58wc0zvf9na1'
|
||||
471
src/streamers/deezer/main.ts
Normal file
471
src/streamers/deezer/main.ts
Normal file
@ -0,0 +1,471 @@
|
||||
import fetch from 'node-fetch'
|
||||
import {
|
||||
Album,
|
||||
Artist,
|
||||
GetByUrlResponse,
|
||||
GetStreamResponse,
|
||||
ItemType,
|
||||
SearchResults,
|
||||
StreamerAccount,
|
||||
StreamerWithLogin,
|
||||
Track
|
||||
} from '../../types.js'
|
||||
import { BLOWFISH_SECRET, CLIENT_ID, CLIENT_SECRET, GW_LIGHT_URL } from './constants.js'
|
||||
import { createHash } from 'crypto'
|
||||
import {
|
||||
DeezerAlbum,
|
||||
DeezerArtist,
|
||||
DeezerFormat,
|
||||
DeezerTrack,
|
||||
DeezerUserData,
|
||||
parseAlbum,
|
||||
parseArtist,
|
||||
parseTrack
|
||||
} from './parse.js'
|
||||
import { Transform } from 'stream'
|
||||
import { Blowfish } from 'blowfish-cbc'
|
||||
|
||||
interface DeezerOptions {
|
||||
arl?: string
|
||||
}
|
||||
|
||||
interface APIMethod {
|
||||
'deezer.getUserData': DeezerUserData
|
||||
'deezer.pageArtist': {
|
||||
DATA: DeezerArtist
|
||||
TOP: { data?: DeezerTrack[] }
|
||||
ALBUMS: { data?: DeezerAlbum[] }
|
||||
}
|
||||
'deezer.pageAlbum': {
|
||||
DATA: DeezerAlbum
|
||||
SONGS: { data: DeezerTrack[] }
|
||||
}
|
||||
'deezer.pageTrack': {
|
||||
DATA: DeezerTrack
|
||||
}
|
||||
'user.getArl': string
|
||||
'search.music': { data: (DeezerArtist | DeezerAlbum | DeezerTrack)[] }
|
||||
'song.getListData': { data: DeezerTrack[] }
|
||||
'song.getData': DeezerTrack
|
||||
}
|
||||
|
||||
export default class Deezer implements StreamerWithLogin {
|
||||
hostnames = ['deezer.com', 'www.deezer.com', 'deezer.page.link']
|
||||
|
||||
headers: { [header: string]: string } = {
|
||||
Accept: '*/*',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36',
|
||||
'Content-Type': 'text/plain;charset=UTF-8',
|
||||
Origin: 'https://www.deezer.com',
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
'Sec-Fetch-Mode': 'same-origin',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
Referer: 'https://www.deezer.com/',
|
||||
'Accept-Language': 'en-US,en;q=0.9'
|
||||
}
|
||||
|
||||
arl?: string
|
||||
apiToken?: string
|
||||
licenseToken?: string
|
||||
renewTimestamp?: number
|
||||
country?: string
|
||||
language?: string
|
||||
|
||||
availableFormats: Set<DeezerFormat> = new Set()
|
||||
|
||||
constructor(options?: DeezerOptions) {
|
||||
if (options?.arl) this.#loginViaArl(options.arl)
|
||||
}
|
||||
|
||||
async #apiCall<T extends keyof APIMethod>(method: T, data?: any): Promise<APIMethod[T]> {
|
||||
let apiToken = this.apiToken
|
||||
if (method == 'deezer.getUserData' || method == 'user.getArl') apiToken = ''
|
||||
|
||||
let url = `${GW_LIGHT_URL}?${new URLSearchParams({
|
||||
method,
|
||||
input: '3',
|
||||
api_version: '1.0',
|
||||
api_token: apiToken ?? '',
|
||||
cid: Math.floor(Math.random() * 1e9).toString()
|
||||
})}`
|
||||
|
||||
let body = data ? JSON.stringify(data) : undefined
|
||||
let req = await fetch(url, { method: 'POST', body, headers: this.headers })
|
||||
let { results: res, error, payload } = (await req.json()) as any
|
||||
|
||||
if (error.constructor.name == 'Object') {
|
||||
let [type, msg] = Object.entries(error)[0]
|
||||
throw new Error(`API Error: ${type}\n${msg}\n\nPayload: ${payload}`)
|
||||
}
|
||||
|
||||
if (method == 'deezer.getUserData') {
|
||||
const setCookie = req.headers.get('Set-Cookie') ?? ''
|
||||
const sid = setCookie.match(/sid=(fr[0-9a-f]+)/)![1]
|
||||
this.headers['Cookie'] += `arl=${this.arl}; sid=${sid}`
|
||||
|
||||
this.apiToken = res?.checkForm
|
||||
this.licenseToken = res?.USER?.OPTIONS?.license_token
|
||||
|
||||
this.country = res?.COUNTRY
|
||||
this.language = res?.USER?.SETTING?.global?.language
|
||||
|
||||
this.availableFormats = new Set([DeezerFormat.MP3_128])
|
||||
if (res?.USER?.OPTIONS?.web_hq) this.availableFormats.add(DeezerFormat.MP3_320)
|
||||
if (res?.USER?.OPTIONS?.web_lossless) this.availableFormats.add(DeezerFormat.FLAC)
|
||||
|
||||
this.renewTimestamp = Date.now()
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async #loginViaArl(arl: string) {
|
||||
this.arl = arl
|
||||
this.headers['Cookie'] = `arl=${arl}`
|
||||
let userData = await this.#apiCall('deezer.getUserData')
|
||||
|
||||
if (userData.USER.USER_ID == 0) {
|
||||
delete this.headers['Cookie']
|
||||
this.arl = undefined
|
||||
throw new Error('Invalid ARL')
|
||||
}
|
||||
|
||||
return userData
|
||||
}
|
||||
|
||||
#md5(str: string) {
|
||||
return createHash('md5').update(str).digest('hex')
|
||||
}
|
||||
|
||||
async login(username: string, password: string): Promise<void> {
|
||||
if (this.arl) {
|
||||
return
|
||||
}
|
||||
|
||||
const resp = await fetch('https://www.deezer.com/', { headers: this.headers })
|
||||
const setCookie = resp.headers.get('Set-Cookie') ?? ''
|
||||
const sid = setCookie.match(/sid=(fr[0-9a-f]+)/)![1]
|
||||
this.headers['Cookie'] = `sid=${sid}`
|
||||
|
||||
password = this.#md5(password)
|
||||
|
||||
let loginReq = await fetch(
|
||||
`https://connect.deezer.com/oauth/user_auth.php?${new URLSearchParams({
|
||||
app_id: CLIENT_ID,
|
||||
login: username,
|
||||
password,
|
||||
hash: this.#md5(CLIENT_ID + username + password + CLIENT_SECRET)
|
||||
})}`,
|
||||
{ headers: this.headers }
|
||||
)
|
||||
const { error } = (await loginReq.json()) as any
|
||||
|
||||
if (error) throw new Error('Error while getting access token, check your credentials')
|
||||
|
||||
let arl = await this.#apiCall('user.getArl')
|
||||
|
||||
await this.#loginViaArl(arl)
|
||||
}
|
||||
|
||||
/* ---------- SEARCH ---------- */
|
||||
|
||||
async search(query: string, limit: number): Promise<SearchResults> {
|
||||
let results = await Promise.all([
|
||||
this.#searchArtists(query, limit),
|
||||
this.#searchAlbums(query, limit),
|
||||
this.#searchTracks(query, limit)
|
||||
])
|
||||
|
||||
let [artists, albums, tracks] = results
|
||||
|
||||
return { query, albums, tracks, artists }
|
||||
}
|
||||
|
||||
async #searchArtists(query: string, limit: number): Promise<Artist[]> {
|
||||
let { data } = await this.#apiCall('search.music', {
|
||||
query,
|
||||
start: 0,
|
||||
nb: limit,
|
||||
filter: 'ALL',
|
||||
output: 'ARTIST'
|
||||
})
|
||||
return data.map((a) => parseArtist(a as DeezerArtist))
|
||||
}
|
||||
|
||||
async #searchAlbums(query: string, limit: number): Promise<Album[]> {
|
||||
let { data } = await this.#apiCall('search.music', {
|
||||
query,
|
||||
start: 0,
|
||||
nb: limit,
|
||||
filter: 'ALL',
|
||||
output: 'ALBUM'
|
||||
})
|
||||
return data.map((a) => parseAlbum(a as DeezerAlbum))
|
||||
}
|
||||
|
||||
async #searchTracks(query: string, limit: number): Promise<Track[]> {
|
||||
let { data } = await this.#apiCall('search.music', {
|
||||
query,
|
||||
start: 0,
|
||||
nb: limit,
|
||||
filter: 'ALL',
|
||||
output: 'TRACK'
|
||||
})
|
||||
return data.map((t) => parseTrack(t as DeezerTrack))
|
||||
}
|
||||
|
||||
/* ---------- GET INFO FROM URL ---------- */
|
||||
|
||||
async #unshortenUrl(url: URL): Promise<URL> {
|
||||
let res = await fetch(url, { redirect: 'manual' })
|
||||
let location = res.headers.get('Location')
|
||||
|
||||
if (res.status != 302 || !location) throw new Error('URL not supported')
|
||||
|
||||
return new URL(location)
|
||||
}
|
||||
|
||||
async #getInfoFromUrl(url: URL): Promise<{ type: ItemType; id: number }> {
|
||||
if (url.hostname == 'deezer.page.link') url = await this.#unshortenUrl(url)
|
||||
|
||||
let match = url.pathname.match(/^\/(?:[a-z]{2}\/)?(track|album|artist)\/(\d+)\/?$/)
|
||||
if (!match) throw new Error('URL not supported')
|
||||
|
||||
let [, type, id] = match
|
||||
|
||||
return { type: <ItemType>type, id: parseInt(id) }
|
||||
}
|
||||
|
||||
async getTypeFromUrl(url: string): Promise<ItemType> {
|
||||
let urlObj = new URL(url)
|
||||
let { type } = await this.#getInfoFromUrl(urlObj)
|
||||
return type
|
||||
}
|
||||
|
||||
/* ---------- GET BY URL ---------- */
|
||||
|
||||
// Artist
|
||||
|
||||
async #getArtist(id: number): Promise<Artist> {
|
||||
let { DATA, TOP, ALBUMS } = await this.#apiCall('deezer.pageArtist', {
|
||||
art_id: id,
|
||||
lang: 'en'
|
||||
})
|
||||
|
||||
return parseArtist(DATA, TOP.data, ALBUMS.data)
|
||||
}
|
||||
|
||||
// Album
|
||||
|
||||
async #getAlbum(id: number): Promise<{ metadata: Album; tracks: Track[] }> {
|
||||
let { DATA, SONGS } = await this.#apiCall('deezer.pageAlbum', {
|
||||
alb_id: id,
|
||||
lang: 'en'
|
||||
})
|
||||
|
||||
return {
|
||||
metadata: parseAlbum(DATA),
|
||||
tracks: SONGS.data.map(parseTrack)
|
||||
}
|
||||
}
|
||||
|
||||
// Track
|
||||
|
||||
async #getTrackData(id: number): Promise<DeezerTrack> {
|
||||
return (
|
||||
await this.#apiCall('deezer.pageTrack', {
|
||||
sng_id: id
|
||||
})
|
||||
).DATA
|
||||
}
|
||||
|
||||
async #getStream(track: DeezerTrack): Promise<GetStreamResponse> {
|
||||
if ('FALLBACK' in track) track = track.FALLBACK!
|
||||
|
||||
const countries = track.AVAILABLE_COUNTRIES.STREAM_ADS
|
||||
if (!countries.length) throw new Error('Track not available in any country')
|
||||
if (!countries.includes(this.country!))
|
||||
throw new Error("Track not available in the account's country")
|
||||
|
||||
let format: DeezerFormat = DeezerFormat.MP3_128
|
||||
const formatsToCheck = [
|
||||
{
|
||||
format: DeezerFormat.FLAC,
|
||||
filesize: track.FILESIZE_FLAC
|
||||
},
|
||||
{
|
||||
format: DeezerFormat.MP3_320,
|
||||
filesize: track.FILESIZE_MP3_320
|
||||
}
|
||||
]
|
||||
for (const f of formatsToCheck) {
|
||||
if (f.filesize != '0' && this.availableFormats.has(f.format)) {
|
||||
format = f.format
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const id = track.SNG_ID
|
||||
const trackToken = track.TRACK_TOKEN
|
||||
const trackTokenExpiry = track.TRACK_TOKEN_EXPIRE
|
||||
|
||||
let mimeType = ''
|
||||
switch (format) {
|
||||
case (DeezerFormat.MP3_128, DeezerFormat.MP3_320):
|
||||
mimeType = 'audio/mpeg'
|
||||
break
|
||||
case DeezerFormat.FLAC:
|
||||
mimeType = 'audio/flac'
|
||||
}
|
||||
|
||||
// download
|
||||
|
||||
const url = await this.#getTrackUrl(id, trackToken, trackTokenExpiry, format)
|
||||
const streamResp = await fetch(url)
|
||||
if (!streamResp.ok)
|
||||
throw new Error(`Failed to get track stream. Status code: ${streamResp.status}`)
|
||||
|
||||
const decryptionKey = this.#getTrackDecryptionKey(id)
|
||||
const blowfish = new Blowfish(decryptionKey)
|
||||
|
||||
const chunkSize = 2048 * 3
|
||||
const buf = Buffer.alloc(chunkSize)
|
||||
let bufSize = 0
|
||||
|
||||
const decryption = new Transform({
|
||||
transform(c, _, callback) {
|
||||
const chunk = <Buffer>c
|
||||
let chunkBytesRead = 0
|
||||
while (chunkBytesRead != chunk.length) {
|
||||
let slice = chunk.subarray(
|
||||
chunkBytesRead,
|
||||
chunkBytesRead + (chunkSize - (bufSize % chunkSize))
|
||||
)
|
||||
chunkBytesRead += slice.length
|
||||
|
||||
slice.copy(buf, bufSize)
|
||||
bufSize += slice.length
|
||||
|
||||
if (bufSize == chunkSize) {
|
||||
bufSize = 0
|
||||
const copy = Buffer.alloc(chunkSize)
|
||||
buf.copy(copy, 0)
|
||||
|
||||
if (copy.length >= 2048) {
|
||||
const encryptedChunk = copy.subarray(0, 2048)
|
||||
blowfish.decryptChunk(encryptedChunk)
|
||||
}
|
||||
|
||||
this.push(copy)
|
||||
}
|
||||
}
|
||||
callback()
|
||||
},
|
||||
|
||||
flush(callback) {
|
||||
if (bufSize != 0) {
|
||||
const final = buf.subarray(0, bufSize)
|
||||
if (final.length >= 2048) {
|
||||
const encryptedChunk = final.subarray(0, 2048)
|
||||
blowfish.decryptChunk(encryptedChunk)
|
||||
}
|
||||
this.push(final)
|
||||
}
|
||||
callback()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
stream: streamResp.body!.pipe(decryption),
|
||||
mimeType
|
||||
}
|
||||
}
|
||||
|
||||
async #getTrackUrl(
|
||||
id: string,
|
||||
trackToken: string,
|
||||
trackTokenExpiry: number,
|
||||
format: DeezerFormat
|
||||
): Promise<string> {
|
||||
if (Date.now() - (this.renewTimestamp ?? 0) >= 3600)
|
||||
// renew license token
|
||||
await this.#apiCall('deezer.getUserData')
|
||||
|
||||
if (Date.now() - trackTokenExpiry >= 0)
|
||||
// renew track token
|
||||
trackToken = (
|
||||
await this.#apiCall('song.getData', {
|
||||
sng_id: id,
|
||||
array_default: ['TRACK_TOKEN']
|
||||
})
|
||||
).TRACK_TOKEN
|
||||
|
||||
let req = await fetch('https://media.deezer.com/v1/get_url', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
license_token: this.licenseToken,
|
||||
media: [
|
||||
{
|
||||
type: 'FULL',
|
||||
formats: [{ cipher: 'BF_CBC_STRIPE', format: DeezerFormat[format] }]
|
||||
}
|
||||
],
|
||||
track_tokens: [trackToken]
|
||||
})
|
||||
})
|
||||
let res = (await req.json()) as any
|
||||
|
||||
return res.data[0].media[0].sources[0].url
|
||||
}
|
||||
|
||||
#getTrackDecryptionKey(id: string): Uint8Array {
|
||||
const hash = this.#md5(id)
|
||||
|
||||
const key = new Uint8Array(16)
|
||||
for (let i = 0; i < 16; i++) {
|
||||
key[i] = hash.charCodeAt(i) ^ hash.charCodeAt(i + 16) ^ BLOWFISH_SECRET.charCodeAt(i)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
async getByUrl(url: string, limit?: number | undefined): Promise<GetByUrlResponse> {
|
||||
let { type, id } = await this.#getInfoFromUrl(new URL(url))
|
||||
|
||||
switch (type) {
|
||||
case 'artist':
|
||||
return {
|
||||
type: 'artist',
|
||||
metadata: await this.#getArtist(id)
|
||||
}
|
||||
case 'album':
|
||||
return {
|
||||
type: 'album',
|
||||
...(await this.#getAlbum(id))
|
||||
}
|
||||
case 'track':
|
||||
let track = await this.#getTrackData(id)
|
||||
return {
|
||||
type: 'track',
|
||||
metadata: parseTrack(track),
|
||||
getStream: () => {
|
||||
return this.#getStream(track)
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error('URL unrecognised')
|
||||
}
|
||||
}
|
||||
|
||||
async getAccountInfo(): Promise<StreamerAccount> {
|
||||
const userData = await this.#apiCall('deezer.getUserData')
|
||||
|
||||
return {
|
||||
valid: userData.USER.USER_ID != 0,
|
||||
premium: userData.OFFER_ID != 0,
|
||||
country: userData.COUNTRY,
|
||||
explicit: userData.USER.EXPLICIT_CONTENT_LEVEL != 'explicit_hide'
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/streamers/deezer/parse.ts
Normal file
131
src/streamers/deezer/parse.ts
Normal file
@ -0,0 +1,131 @@
|
||||
import { Album, Artist, CoverArtwork, Track } from '../../types.js'
|
||||
import { SIZES } from './constants.js'
|
||||
|
||||
export enum DeezerFormat {
|
||||
MP3_128 = 1,
|
||||
MP3_320 = 3,
|
||||
FLAC = 9
|
||||
}
|
||||
|
||||
export interface DeezerUserData {
|
||||
USER: {
|
||||
USER_ID: number
|
||||
EXPLICIT_CONTENT_LEVEL: string
|
||||
}
|
||||
OFFER_ID: number
|
||||
COUNTRY: string
|
||||
}
|
||||
|
||||
export interface DeezerArtist {
|
||||
ART_ID: string
|
||||
ART_NAME: string
|
||||
ART_PICTURE: string
|
||||
}
|
||||
|
||||
export function parseArtist(
|
||||
artist: DeezerArtist,
|
||||
tracks?: DeezerTrack[],
|
||||
albums?: DeezerAlbum[]
|
||||
): Artist {
|
||||
let pictures
|
||||
if (artist.ART_PICTURE)
|
||||
pictures = SIZES.map(
|
||||
(s) =>
|
||||
`https://e-cdns-images.dzcdn.net/images/artist/${artist.ART_PICTURE}/${s}x${s}-000000-80-0-0.jpg`
|
||||
)
|
||||
|
||||
let parsedTracks
|
||||
if (tracks) parsedTracks = tracks.map(parseTrack)
|
||||
|
||||
let parsedAlbums
|
||||
if (albums) parsedAlbums = albums.map(parseAlbum)
|
||||
|
||||
return {
|
||||
id: artist.ART_ID,
|
||||
url: `https://www.deezer.com/artist/${artist.ART_ID}`,
|
||||
pictures,
|
||||
name: artist.ART_NAME,
|
||||
tracks: parsedTracks,
|
||||
albums: parsedAlbums
|
||||
}
|
||||
}
|
||||
|
||||
export interface DeezerAlbum {
|
||||
ALB_ID: string
|
||||
ALB_TITLE: string
|
||||
ALB_PICTURE: string
|
||||
ARTISTS?: DeezerArtist[]
|
||||
ORIGINAL_RELEASE_DATE?: string
|
||||
NUMBER_TRACK?: string
|
||||
}
|
||||
|
||||
export function parseAlbum(album: DeezerAlbum): Album {
|
||||
return {
|
||||
title: album.ALB_TITLE,
|
||||
id: album.ALB_ID,
|
||||
url: `https://www.deezer.com/album/${album.ALB_ID}`,
|
||||
trackCount: album.NUMBER_TRACK ? parseInt(album.NUMBER_TRACK) : undefined,
|
||||
releaseDate: album.ORIGINAL_RELEASE_DATE ? new Date(album.ORIGINAL_RELEASE_DATE) : undefined,
|
||||
coverArtwork: parseArtwork(album.ALB_PICTURE),
|
||||
artists: album.ARTISTS ? album.ARTISTS.map((a) => parseArtist(a)) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
export interface DeezerTrack {
|
||||
SNG_ID: string
|
||||
SNG_TITLE: string
|
||||
EXPLICIT_LYRICS: '0' | '1'
|
||||
TRACK_NUMBER: string
|
||||
DISK_NUMBER: string
|
||||
ARTISTS: DeezerArtist[]
|
||||
ISRC: string
|
||||
SNG_CONTRIBUTORS: { [role: string]: string[] }
|
||||
ALB_ID: string
|
||||
ALB_TITLE: string
|
||||
ALB_PICTURE: string
|
||||
DURATION: string
|
||||
AVAILABLE_COUNTRIES: { STREAM_ADS: string }
|
||||
COPYRIGHT: string
|
||||
|
||||
TRACK_TOKEN: string
|
||||
TRACK_TOKEN_EXPIRE: number
|
||||
MD5_ORIGIN: string
|
||||
MEDIA_VERSION: string
|
||||
|
||||
FILESIZE_MP3_320: string
|
||||
FILESIZE_FLAC: string
|
||||
|
||||
FALLBACK?: DeezerTrack
|
||||
}
|
||||
|
||||
export function parseTrack(track: DeezerTrack): Track {
|
||||
return {
|
||||
title: track.SNG_TITLE,
|
||||
id: track.SNG_ID,
|
||||
url: `https://www.deezer.com/track/${track.SNG_ID}`,
|
||||
explicit: track.EXPLICIT_LYRICS == '1',
|
||||
trackNumber: parseInt(track.TRACK_NUMBER),
|
||||
discNumber: parseInt(track.DISK_NUMBER),
|
||||
copyright: track.COPYRIGHT,
|
||||
artists: track.ARTISTS.map((a) => parseArtist(a)),
|
||||
isrc: track.ISRC,
|
||||
producers: track.SNG_CONTRIBUTORS?.producer,
|
||||
composers: track.SNG_CONTRIBUTORS?.composer,
|
||||
lyricists: track.SNG_CONTRIBUTORS?.lyricist,
|
||||
album: parseAlbum({
|
||||
ALB_ID: track.ALB_ID,
|
||||
ALB_PICTURE: track.ALB_PICTURE,
|
||||
ALB_TITLE: track.ALB_TITLE
|
||||
}),
|
||||
durationMs: parseInt(track.DURATION) * 1e3,
|
||||
coverArtwork: parseArtwork(track.ALB_PICTURE)
|
||||
}
|
||||
}
|
||||
|
||||
export function parseArtwork(picture: string): CoverArtwork[] {
|
||||
return SIZES.map((size) => ({
|
||||
url: `https://e-cdns-images.dzcdn.net/images/cover/${picture}/${size}x${size}-000000-80-0-0.jpg`,
|
||||
height: size,
|
||||
width: size
|
||||
}))
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user