add playlists to qobuz and deezer

This commit is contained in:
aria
2024-09-05 23:17:20 -04:00
parent 901d326c8b
commit 0ec81acf40
5 changed files with 130 additions and 8 deletions

View File

@@ -1,6 +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 SIZES = [128, 256, 512, 1024]
export const BLOWFISH_SECRET = 'g4el58wc0zvf9na1'

View File

@@ -5,6 +5,7 @@ import {
GetByUrlResponse,
GetStreamResponse,
ItemType,
PlaylistGetByUrlResponse,
SearchResults,
StreamerAccount,
StreamerWithLogin,
@@ -15,6 +16,7 @@ import { createHash } from 'crypto'
import {
DeezerAlbum,
DeezerArtist,
DeezerPlaylistMetadata,
DeezerFormat,
DeezerLoginResponse,
DeezerMediaResponse,
@@ -22,7 +24,8 @@ import {
DeezerUserData,
parseAlbum,
parseArtist,
parseTrack
parseTrack,
parsePlaylistMetadata
} from './parse.js'
import { Readable, Transform } from 'stream'
import { Blowfish } from 'blowfish-cbc'
@@ -43,9 +46,15 @@ interface APIMethod {
DATA: DeezerAlbum
SONGS: { data: DeezerTrack[] }
}
'playlist.getSongs': {
data: DeezerTrack[]
}
'deezer.pageTrack': {
DATA: DeezerTrack
}
'deezer.pagePlaylist': {
DATA: DeezerPlaylistMetadata
}
'user.getArl': string
'search.music': { data: (DeezerArtist | DeezerAlbum | DeezerTrack)[] }
'song.getListData': { data: DeezerTrack[] }
@@ -272,7 +281,7 @@ export default class Deezer implements StreamerWithLogin {
async #getInfoFromUrl(url: URL): Promise<{ type: ItemType; id: number }> {
if (url.hostname == 'deezer.page.link') url = await this.#unshortenUrl(url)
const match = url.pathname.match(/^\/(?:[a-z]{2}\/)?(track|album|artist)\/(\d+)\/?$/)
const match = url.pathname.match(/^\/(?:[a-z]{2}\/)?(track|album|artist|playlist)\/(\d+)\/?$/)
if (!match) throw new Error('URL not supported')
const [, type, id] = match
@@ -317,6 +326,29 @@ export default class Deezer implements StreamerWithLogin {
return data
}
// Playlist
async #getPlaylist(id: number): Promise<PlaylistGetByUrlResponse> {
const playlistMetadata = (
await this.#apiCall('deezer.pagePlaylist', {
playlist_id: id
})
).DATA
const playlistTracks = (
await this.#apiCall('playlist.getSongs', {
playlist_id: id,
nb: 9999,
start: 0
})
).data
return {
type: 'playlist',
metadata: parsePlaylistMetadata(playlistMetadata),
tracks: playlistTracks.map(parseTrack)
}
}
// Track
async #getTrackData(id: number): Promise<DeezerTrack> {
@@ -501,6 +533,9 @@ export default class Deezer implements StreamerWithLogin {
}
}
}
case 'playlist': {
return await this.#getPlaylist(id)
}
default:
throw new Error('URL unrecognised')
}

View File

@@ -1,4 +1,4 @@
import { Album, Artist, CoverArtwork, Track } from '../../types.js'
import { Album, Artist, CoverArtwork, Playlist, Track } from '../../types.js'
import { SIZES } from './constants.js'
export enum DeezerFormat {
@@ -185,3 +185,22 @@ export function parseArtwork(picture: string): CoverArtwork[] {
width: size
}))
}
export interface DeezerPlaylistMetadata {
PLAYLIST_ID: string
TITLE: string
DESCRIPTION: string
PLAYLIST_PICTURE: string
DURATION: number
NB_SONG: number
}
export function parsePlaylistMetadata(raw: DeezerPlaylistMetadata) {
return <Playlist>{
id: raw.PLAYLIST_ID,
title: raw.TITLE,
url: `https://www.deezer.com/playlist/${raw.PLAYLIST_ID}`,
coverArtwork: parseArtwork(raw.PLAYLIST_PICTURE),
trackCount: raw.NB_SONG
}
}

View File

@@ -10,7 +10,16 @@ import {
StreamerAccount
} from '../../types.js'
import { DEFAULT_HEADERS } from './constants.js'
import { parseAlbum, parseTrack, parseArtist, RawAlbum, RawArtist, RawTrack } from './parse.js'
import {
parseAlbum,
parseTrack,
parseArtist,
RawAlbum,
RawArtist,
RawTrack,
RawPlaylist,
parsePlaylist
} from './parse.js'
import { Readable } from 'stream'
function headers(token?: string): HeadersInit {
@@ -207,6 +216,7 @@ export default class Qobuz implements StreamerWithLogin {
app_id: this.appId
})
return {
type: 'album',
metadata: { ...parseAlbum(albumResponse), trackCount: albumResponse.tracks?.items.length },
tracks: albumResponse.tracks?.items.map(parseTrack)
}
@@ -224,7 +234,21 @@ export default class Qobuz implements StreamerWithLogin {
)
}
#getUrlParts(url: string): ['artist' | 'album' | 'track', string] {
async #getPlaylist(id: string) {
const init = <RawPlaylist>await this.#get('playlist/get', {
playlist_id: id,
extra: 'tracks,getSimilarPlaylists',
offset: 0,
limit: 1000,
app_id: this.appId
})
if (init.tracks_count > 1000)
throw new Error('To be added, playlists with over a thousand tracks.')
else return parsePlaylist(init)
}
#getUrlParts(url: string): ['artist' | 'album' | 'track' | 'playlist', string] {
const urlObj = new URL(url)
if (urlObj.hostname == 'www.qobuz.com' || urlObj.hostname == 'qobuz.com') {
const urlParts = url
@@ -239,6 +263,8 @@ export default class Qobuz implements StreamerWithLogin {
case 'album':
case 'track':
return [type, id]
case 'playlists':
return ['playlist', id]
default:
throw new Error('URL unrecognised')
}
@@ -248,7 +274,12 @@ export default class Qobuz implements StreamerWithLogin {
?.slice(1, 3)
if (!urlParts) throw new Error('URL not supported')
urlParts[1] = urlParts[1].replace(/\?.*?$/, '')
if (urlParts[0] != 'artist' && urlParts[0] != 'album' && urlParts[0] != 'track') {
if (
urlParts[0] != 'artist' &&
urlParts[0] != 'album' &&
urlParts[0] != 'track' &&
urlParts[0] != 'playlist'
) {
throw new Error('URL unrecognised')
}
return [urlParts[0], urlParts[1]]
@@ -282,6 +313,9 @@ export default class Qobuz implements StreamerWithLogin {
metadata: await this.#getArtistMetadata(id)
}
}
case 'playlist': {
return await this.#getPlaylist(id)
}
default:
throw new Error('URL unrecognised')
}

View File

@@ -1,4 +1,4 @@
import { Artist, Album, Track } from '../../types.js'
import { Artist, Album, Track, PlaylistGetByUrlResponse } from '../../types.js'
export interface RawArtist {
id: number
@@ -154,3 +154,37 @@ function parsePerformers(performers: string, track: Track) {
return track
}
export interface RawPlaylist {
owner: {
id: number
name: string
}
id: number
name: string
images: string[]
is_collaborative: boolean
description: string
created_at: number
duration: number
tracks_count: number
tracks: {
offset?: number
limit?: number
total?: number
items: RawTrack[]
}
}
export function parsePlaylist(raw: RawPlaylist) {
return <PlaylistGetByUrlResponse>{
type: 'playlist',
metadata: {
id: raw.id,
title: raw.name,
trackCount: raw?.tracks_count || raw?.tracks?.total,
url: `https://open.qobuz.com/playlist/${raw.id}`
},
tracks: raw.tracks?.items.map(parseTrack)
}
}