Add support for fetching information from Deezer
This commit is contained in:
parent
2d91c2437b
commit
2aea9f0deb
@ -47,3 +47,4 @@ const play = require('play-dl') //JS importing
|
||||
- [YouTube](https://github.com/play-dl/play-dl/tree/main/docs/YouTube#youtube)
|
||||
- [Spotify](https://github.com/play-dl/play-dl/tree/main/docs/Spotify#spotify)
|
||||
- [SoundCloud](https://github.com/play-dl/play-dl/tree/main/docs/SoundCloud)
|
||||
- [Deezer](https://github.com/play-dl/play-dl/tree/main/docs/Deezer)
|
||||
|
||||
169
docs/Deezer/README.md
Normal file
169
docs/Deezer/README.md
Normal file
@ -0,0 +1,169 @@
|
||||
# Deezer
|
||||
|
||||
## Main
|
||||
|
||||
### deezer(url : `string`)
|
||||
|
||||
_This returns data from a track | playlist | album url. Accepts share links as well, which it resolves first._
|
||||
|
||||
```js
|
||||
let data = await deezer(url); //Gets the data
|
||||
|
||||
console.log(data.type); // Console logs the type of data that you got.
|
||||
```
|
||||
|
||||
## Validate
|
||||
|
||||
### dz_validate(url : `string`)
|
||||
|
||||
_This checks that given url is Deezer url or not._
|
||||
|
||||
**Returns :** `track` | `album` | `playlist` | `search` | `share` | `false`
|
||||
|
||||
```js
|
||||
let check = dz_validate(url)
|
||||
|
||||
if(!check) // Invalid Deezer URL
|
||||
|
||||
if(check === 'track') // Deezer Track URL
|
||||
|
||||
if(check === 'share') // Deezer Share URL
|
||||
|
||||
if(check === "search") // Given term is a search query. Search it somewhere.
|
||||
```
|
||||
|
||||
## Search
|
||||
|
||||
### dz_search(query: `string`, options: `DeezerSearchOptions`)
|
||||
|
||||
_Searches for tracks, playlists and albums._
|
||||
|
||||
**Returns :** `Deezer[]` an array of tracks, playlists or albums
|
||||
|
||||
#### `DeezerSearchOptions`
|
||||
|
||||
- **type?** `'track'` | `'playlist'` | `'album'` The type to search for. Defaults to `'track'`.
|
||||
- **limit?** `number` The maximum number of results to return. Maximum `100`, defaults to `10`.
|
||||
- **fuzzy?** `boolean` Whether the search should be fuzzy or only return exact matches. Defaults to `true`.
|
||||
|
||||
```js
|
||||
const results = await dz_search(query, {
|
||||
limit: 1,
|
||||
type: 'track',
|
||||
fuzzy: false
|
||||
}); // Returns an array with one track, using exact matching
|
||||
```
|
||||
|
||||
## Resolving a share URL
|
||||
|
||||
### dz_resolve_share_url(url: `string`)
|
||||
|
||||
_Resolves a Deezer share link (deezer.page.link) returning the deezer.com URL._
|
||||
|
||||
**Returns :** `string` the resolved URL. Warning the returned URL might not be a track, album or playlist URL.
|
||||
|
||||
```js
|
||||
const resolvedURL = await dz_resolve_share_url(url);
|
||||
```
|
||||
|
||||
## Classes [ Returned by `deezer(url)` function ]
|
||||
|
||||
### DeezerTrack
|
||||
|
||||
_This is the class for a Deezer track._
|
||||
|
||||
##### type `property`
|
||||
|
||||
_This will always return as "track" for this class._
|
||||
|
||||
##### partial `property`
|
||||
|
||||
_Will return true for tracks in search results and false for tracks fetched directly or if the fetch function has been called. This being true means that the optional properties are undefined._
|
||||
|
||||
##### toJSON() `function`
|
||||
|
||||
_Converts the object to JSON_
|
||||
|
||||
##### fetch() `function`
|
||||
|
||||
_Fetches the missing data for a partial track._
|
||||
|
||||
```js
|
||||
const track = await deezer(track_url);
|
||||
|
||||
await track.fetch() // Fetches the missing data
|
||||
```
|
||||
|
||||
### DeezerPlaylist
|
||||
|
||||
_This is the class for a Deezer playlist._
|
||||
|
||||
##### fetch() `function`
|
||||
|
||||
_This will fetch up to 1000 tracks in a playlist as well as the missing data for a partial playlist._
|
||||
|
||||
```js
|
||||
let data = await deezer(playlist_url)
|
||||
|
||||
await data.fetch() // Fetches tracks more than 100 tracks in playlist
|
||||
```
|
||||
|
||||
##### tracksCount `property`
|
||||
|
||||
_This will return the total number of tracks in a playlist._
|
||||
|
||||
```js
|
||||
const data = await deezer(playlist_url)
|
||||
|
||||
console.log(data.tracksCount) // Total number of tracks in the playlist.
|
||||
```
|
||||
|
||||
##### type `property`
|
||||
|
||||
_This will always return as "playlist" for this class._
|
||||
|
||||
##### partial `property`
|
||||
|
||||
_Will return true for playlists in search results and false for playlists fetched directly or if the fetch function has been called. This being true means that the optional properties are undefined and `tracks` may be empty or partially filled._
|
||||
|
||||
##### tracks `property`
|
||||
|
||||
_The array of tracks in this album, this is always empty (length of 0) for partial playlists._
|
||||
|
||||
```js
|
||||
const data = await deezer(playlist_url);
|
||||
|
||||
if (data.tracks.length !== data.tracksCount) {
|
||||
await data.fetch();
|
||||
}
|
||||
|
||||
console.log(data.tracks); // returns all tracks in the playlist
|
||||
```
|
||||
|
||||
##### toJSON() `function`
|
||||
|
||||
_Converts the object to JSON_
|
||||
|
||||
### DeezerAlbum
|
||||
|
||||
_This is the class for a Deezer album._
|
||||
|
||||
##### type `property`
|
||||
|
||||
_This will always return as "track" for this class._
|
||||
|
||||
##### tracks `property`
|
||||
|
||||
_The array of tracks in this album, this is always empty (length of 0) for partial albums._
|
||||
|
||||
##### partial `property`
|
||||
|
||||
_Will return true for albums in search results and false for albums fetched directly or if the fetch function has been called. This being true means that the optional properties are undefined._
|
||||
|
||||
##### toJSON() `function`
|
||||
|
||||
_Converts the object to JSON_
|
||||
|
||||
##### fetch() `function`
|
||||
|
||||
_Fetches the missing data for a partial album._
|
||||
@ -12,7 +12,7 @@ For source specific commands :-
|
||||
|
||||
_This checks all type of urls that are supported by play-dl._
|
||||
|
||||
**Returns :** `so_playlist` | `so_track` | `sp_track` | `sp_album` | `sp_playlist` | `yt_video` | `yt_playlist` | `search` | `false`
|
||||
**Returns :** `so_playlist` | `so_track` | `sp_track` | `sp_album` | `sp_playlist` | `dz_track` | `dz_playlist` | `dz_album` | `dz_share` | `yt_video` | `yt_playlist` | `search` | `false`
|
||||
|
||||
`so` = **SoundCloud**
|
||||
|
||||
@ -20,6 +20,8 @@ _This checks all type of urls that are supported by play-dl._
|
||||
|
||||
`yt` = **YouTube**
|
||||
|
||||
`dz` = **Deezer**
|
||||
|
||||
```js
|
||||
let check = await validate(url)
|
||||
|
||||
@ -31,6 +33,8 @@ if(check === 'sp_track') // Spotify Track
|
||||
|
||||
if(check === 'so_track') // SoundCloud Track
|
||||
|
||||
if(check === 'dz_track') // Deezer Track
|
||||
|
||||
if(check === "search") // Given term is not a url. Search this term somewhere.
|
||||
```
|
||||
|
||||
@ -82,6 +86,8 @@ setToken({
|
||||
|
||||
soundcloud: `tracks` | `playlists` | `albums` ;
|
||||
|
||||
deezer: `track` | `playlist` | `album` ;
|
||||
|
||||
}
|
||||
|
||||
#### search(query : `string`, options? : [`SearchOptions`](https://github.com/play-dl/play-dl/tree/main/docs#searchoptions-))
|
||||
@ -98,13 +104,15 @@ let data = await search('Rick Roll', { limit : 1, source : { youtube : "video" }
|
||||
let data = await search('Rick Roll', { limit: 1, source : { spotify : "track" } }) // Searches for spotify track.
|
||||
|
||||
let data = await search('Rick Roll', { limit: 1, source : { soundcloud : "tracks" } }) // Searches for soundcloud track.
|
||||
|
||||
let data = await search('Rick Roll', { limit: 1, source : { deezer : "track" } }) // Searches for a Deezer track.
|
||||
```
|
||||
|
||||
### Stream
|
||||
|
||||
**Attaching events to player is important for stream to work.**
|
||||
|
||||
#### attachListeners(player : `AudioPlayer`, resource : `YouTubeStream | SoundCloudStream`)
|
||||
#### attachListeners(player : `AudioPlayer`, resource : `YouTubeStream | SoundCloudStream`)
|
||||
|
||||
_This is used for attaching pause and playing events to audioPlayer._
|
||||
|
||||
|
||||
500
play-dl/Deezer/classes.ts
Normal file
500
play-dl/Deezer/classes.ts
Normal file
@ -0,0 +1,500 @@
|
||||
import { request } from '../Request';
|
||||
|
||||
interface DeezerImage {
|
||||
xl: string;
|
||||
big: string;
|
||||
medium: string;
|
||||
small: string;
|
||||
}
|
||||
|
||||
interface DeezerGenre {
|
||||
name: string;
|
||||
picture: DeezerImage;
|
||||
}
|
||||
|
||||
interface DeezerUser {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for Deezer Tracks
|
||||
*/
|
||||
export class DeezerTrack {
|
||||
id: number;
|
||||
title: string;
|
||||
shortTitle: string;
|
||||
url: string;
|
||||
durationInSec: number;
|
||||
rank: number;
|
||||
explicit: boolean;
|
||||
previewURL: string;
|
||||
artist: DeezerArtist;
|
||||
album: DeezerTrackAlbum;
|
||||
type: 'track' | 'playlist' | 'album';
|
||||
|
||||
/**
|
||||
* true for tracks in search results and false if the track was fetched directly.
|
||||
*/
|
||||
partial: boolean;
|
||||
|
||||
trackPosition?: number;
|
||||
diskNumber?: number;
|
||||
releaseDate?: Date;
|
||||
bpm?: number;
|
||||
gain?: number;
|
||||
contributors?: DeezerArtist[];
|
||||
|
||||
constructor(data: any, partial: boolean) {
|
||||
this.id = data.id;
|
||||
this.title = data.title;
|
||||
this.shortTitle = data.title_short;
|
||||
this.url = data.link;
|
||||
this.durationInSec = data.duration;
|
||||
this.rank = data.rank;
|
||||
this.explicit = data.explicit_lyrics;
|
||||
this.previewURL = data.preview;
|
||||
this.artist = new DeezerArtist(data.artist);
|
||||
this.album = new DeezerTrackAlbum(data.album);
|
||||
this.type = 'track';
|
||||
|
||||
this.partial = partial;
|
||||
|
||||
if (!partial) {
|
||||
this.trackPosition = data.track_position;
|
||||
this.diskNumber = data.disk_number;
|
||||
this.releaseDate = new Date(data.release_date);
|
||||
this.bpm = data.bpm;
|
||||
this.gain = data.gain;
|
||||
this.contributors = [];
|
||||
|
||||
data.contributors.forEach((contributor: any) => {
|
||||
this.contributors?.push(new DeezerArtist(contributor));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the missing data for a partial {@link DeezerTrack}.
|
||||
*/
|
||||
async fetch(): Promise<DeezerTrack> {
|
||||
if (!this.partial) return this;
|
||||
|
||||
const response = await request(`https://api.deezer.com/track/${this.id}/`).catch((err: Error) => err);
|
||||
|
||||
if (response instanceof Error) throw response;
|
||||
const jsonData = JSON.parse(response);
|
||||
|
||||
this.partial = false;
|
||||
|
||||
this.trackPosition = jsonData.track_position;
|
||||
this.diskNumber = jsonData.disk_number;
|
||||
this.releaseDate = new Date(jsonData.release_date);
|
||||
this.bpm = jsonData.bpm;
|
||||
this.gain = jsonData.gain;
|
||||
this.contributors = [];
|
||||
|
||||
jsonData.contributors.forEach((contributor: any) => {
|
||||
this.contributors?.push(new DeezerArtist(contributor));
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
shortTitle: this.shortTitle,
|
||||
url: this.url,
|
||||
durationInSec: this.durationInSec,
|
||||
rank: this.rank,
|
||||
explicit: this.explicit,
|
||||
previewURL: this.previewURL,
|
||||
artist: this.artist,
|
||||
album: this.album,
|
||||
type: this.type,
|
||||
trackPosition: this.trackPosition,
|
||||
diskNumber: this.diskNumber,
|
||||
releaseDate: this.releaseDate,
|
||||
bpm: this.bpm,
|
||||
gain: this.gain,
|
||||
contributors: this.contributors
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Class for Deezer Albums
|
||||
*/
|
||||
export class DeezerAlbum {
|
||||
id: number;
|
||||
title: string;
|
||||
url: string;
|
||||
recordType: string;
|
||||
explicit: boolean;
|
||||
artist: DeezerArtist;
|
||||
cover: DeezerImage;
|
||||
type: 'track' | 'playlist' | 'album';
|
||||
tracksCount: number;
|
||||
|
||||
/**
|
||||
* true for albums in search results and false if the album was fetched directly.
|
||||
*/
|
||||
partial: boolean;
|
||||
|
||||
upc?: string;
|
||||
durationInSec?: number;
|
||||
numberOfFans?: number;
|
||||
releaseDate?: Date;
|
||||
available?: boolean;
|
||||
genres?: DeezerGenre[];
|
||||
contributors?: DeezerArtist[];
|
||||
|
||||
tracks: DeezerTrack[];
|
||||
|
||||
constructor(data: any, partial: boolean) {
|
||||
this.id = data.id;
|
||||
this.title = data.title;
|
||||
this.url = data.link;
|
||||
this.recordType = data.record_type;
|
||||
this.explicit = data.explicit_lyrics;
|
||||
this.artist = new DeezerArtist(data.artist);
|
||||
this.type = 'album';
|
||||
this.tracksCount = data.nb_tracks;
|
||||
this.contributors = [];
|
||||
this.genres = [];
|
||||
this.tracks = [];
|
||||
this.cover = {
|
||||
xl: data.cover_xl,
|
||||
big: data.cover_big,
|
||||
medium: data.cover_medium,
|
||||
small: data.cover_small
|
||||
};
|
||||
|
||||
this.partial = partial;
|
||||
|
||||
if (!partial) {
|
||||
this.upc = data.upc;
|
||||
this.durationInSec = data.duration;
|
||||
this.numberOfFans = data.fans;
|
||||
this.releaseDate = new Date(data.release_date);
|
||||
this.available = data.available;
|
||||
|
||||
data.contributors.forEach((contributor: any) => {
|
||||
this.contributors?.push(new DeezerArtist(contributor));
|
||||
});
|
||||
|
||||
data.genres.data.forEach((genre: any) => {
|
||||
this.genres?.push({
|
||||
name: genre.name,
|
||||
picture: {
|
||||
xl: `${genre.picture}?size=xl`,
|
||||
big: `${genre.picture}?size=big`,
|
||||
medium: `${genre.picture}?size=medium`,
|
||||
small: `${genre.picture}?size=small`
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const trackAlbum: any = {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
cover_xl: this.cover.xl,
|
||||
cover_big: this.cover.big,
|
||||
cover_medium: this.cover.medium,
|
||||
cover_small: this.cover.small,
|
||||
release_date: data.release_date
|
||||
};
|
||||
data.tracks.data.forEach((track: any) => {
|
||||
track.album = trackAlbum;
|
||||
this.tracks.push(new DeezerTrack(track, true));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the missing data for a partial {@link DeezerAlbum}.
|
||||
*/
|
||||
async fetch(): Promise<DeezerAlbum> {
|
||||
if (!this.partial) return this;
|
||||
|
||||
const response = await request(`https://api.deezer.com/album/${this.id}/`).catch((err: Error) => err);
|
||||
|
||||
if (response instanceof Error) throw response;
|
||||
const jsonData = JSON.parse(response);
|
||||
|
||||
this.partial = false;
|
||||
|
||||
this.upc = jsonData.upc;
|
||||
this.durationInSec = jsonData.duration;
|
||||
this.numberOfFans = jsonData.fans;
|
||||
this.releaseDate = new Date(jsonData.release_date);
|
||||
this.available = jsonData.available;
|
||||
this.contributors = [];
|
||||
this.genres = [];
|
||||
this.tracks = [];
|
||||
|
||||
jsonData.contributors.forEach((contributor: any) => {
|
||||
this.contributors?.push(new DeezerArtist(contributor));
|
||||
});
|
||||
|
||||
jsonData.genres.data.forEach((genre: any) => {
|
||||
this.genres?.push({
|
||||
name: genre.name,
|
||||
picture: {
|
||||
xl: `${genre.picture}?size=xl`,
|
||||
big: `${genre.picture}?size=big`,
|
||||
medium: `${genre.picture}?size=medium`,
|
||||
small: `${genre.picture}?size=small`
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const trackAlbum: any = {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
cover_xl: this.cover.xl,
|
||||
cover_big: this.cover.big,
|
||||
cover_medium: this.cover.medium,
|
||||
cover_small: this.cover.small,
|
||||
release_date: jsonData.release_date
|
||||
};
|
||||
jsonData.tracks.data.forEach((track: any) => {
|
||||
track.album = trackAlbum;
|
||||
this.tracks.push(new DeezerTrack(track, true));
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
url: this.url,
|
||||
recordType: this.recordType,
|
||||
explicit: this.explicit,
|
||||
artist: this.artist,
|
||||
cover: this.cover,
|
||||
type: this.type,
|
||||
upc: this.upc,
|
||||
tracksCount: this.tracksCount,
|
||||
durationInSec: this.durationInSec,
|
||||
numberOfFans: this.numberOfFans,
|
||||
releaseDate: this.releaseDate,
|
||||
available: this.available,
|
||||
genres: this.genres,
|
||||
contributors: this.contributors,
|
||||
tracks: this.tracks.map((track) => track.toJSON())
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Class for Deezer Albums
|
||||
*/
|
||||
export class DeezerPlaylist {
|
||||
id: number;
|
||||
title: string;
|
||||
public: boolean;
|
||||
url: string;
|
||||
picture: DeezerImage;
|
||||
creationDate: Date;
|
||||
type: 'track' | 'playlist' | 'album';
|
||||
creator: DeezerUser;
|
||||
tracksCount: number;
|
||||
|
||||
partial: boolean;
|
||||
|
||||
description?: string;
|
||||
durationInSec?: number;
|
||||
isLoved?: boolean;
|
||||
collaborative?: boolean;
|
||||
fans?: number;
|
||||
|
||||
tracks: DeezerTrack[];
|
||||
|
||||
constructor(data: any, partial: boolean) {
|
||||
this.id = data.id;
|
||||
this.title = data.title;
|
||||
this.public = data.public;
|
||||
this.url = data.link;
|
||||
this.creationDate = new Date(data.creation_date);
|
||||
this.type = 'playlist';
|
||||
this.tracksCount = data.nb_tracks;
|
||||
this.tracks = [];
|
||||
|
||||
this.picture = {
|
||||
xl: data.picture_xl,
|
||||
big: data.picture_big,
|
||||
medium: data.picture_medium,
|
||||
small: data.picture_small
|
||||
};
|
||||
|
||||
if (data.user) {
|
||||
this.creator = {
|
||||
id: data.user.id,
|
||||
name: data.user.name
|
||||
};
|
||||
} else {
|
||||
this.creator = {
|
||||
id: data.creator.id,
|
||||
name: data.creator.name
|
||||
};
|
||||
}
|
||||
|
||||
this.partial = partial;
|
||||
|
||||
if (!partial) {
|
||||
this.description = data.description;
|
||||
this.durationInSec = data.duration;
|
||||
this.isLoved = data.is_loved_track;
|
||||
this.collaborative = data.collaborative;
|
||||
this.fans = data.fans;
|
||||
|
||||
if (this.public) {
|
||||
this.tracks = data.tracks.data.map((track: any) => {
|
||||
return new DeezerTrack(track, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the missing data for a partial {@link DeezerPlaylist} as well as fetching all tracks.
|
||||
* @returns The Deezer playlist object this method was called on.
|
||||
*/
|
||||
async fetch(): Promise<DeezerPlaylist> {
|
||||
if (!this.partial && (this.tracks.length === this.tracksCount || !this.public)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (this.partial) {
|
||||
const response = await request(`https://api.deezer.com/playlist/${this.id}/`).catch((err: Error) => err);
|
||||
|
||||
if (response instanceof Error) throw response;
|
||||
const jsonData = JSON.parse(response);
|
||||
|
||||
this.partial = false;
|
||||
|
||||
this.description = jsonData.description;
|
||||
this.durationInSec = jsonData.duration;
|
||||
this.isLoved = jsonData.is_loved_track;
|
||||
this.collaborative = jsonData.collaborative;
|
||||
this.fans = jsonData.fans;
|
||||
|
||||
if (this.public) {
|
||||
this.tracks = jsonData.tracks.data.map((track: any) => {
|
||||
return new DeezerTrack(track, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const currentTracksCount = this.tracks.length;
|
||||
if (this.public && currentTracksCount !== this.tracksCount) {
|
||||
let missing = this.tracksCount - currentTracksCount;
|
||||
|
||||
if (missing > 1000) missing = 1000;
|
||||
|
||||
const promises: Promise<DeezerTrack[]>[] = [];
|
||||
for (let i = 1; i <= Math.ceil(missing / 100); i++) {
|
||||
promises.push(
|
||||
new Promise(async (resolve, reject) => {
|
||||
const response = await request(
|
||||
`https://api.deezer.com/playlist/${this.id}/tracks?limit=100&index=${i * 100}`
|
||||
).catch((err) => reject(err));
|
||||
|
||||
if (typeof response !== 'string') return;
|
||||
const jsonData = JSON.parse(response);
|
||||
const tracks = jsonData.data.map((track: any) => {
|
||||
return new DeezerTrack(track, true);
|
||||
});
|
||||
|
||||
resolve(tracks);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(promises);
|
||||
const newTracks: DeezerTrack[] = [];
|
||||
|
||||
for (const result of results) {
|
||||
if (result.status === 'fulfilled') {
|
||||
newTracks.push(...result.value);
|
||||
} else {
|
||||
throw result.reason;
|
||||
}
|
||||
}
|
||||
|
||||
this.tracks.push(...newTracks);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
public: this.public,
|
||||
url: this.url,
|
||||
picture: this.picture,
|
||||
creationDate: this.creationDate,
|
||||
type: this.type,
|
||||
creator: this.creator,
|
||||
tracksCount: this.tracksCount,
|
||||
description: this.description,
|
||||
durationInSec: this.durationInSec,
|
||||
isLoved: this.isLoved,
|
||||
collaborative: this.collaborative,
|
||||
fans: this.fans,
|
||||
tracks: this.tracks.map((track) => track.toJSON())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class DeezerTrackAlbum {
|
||||
id: number;
|
||||
title: string;
|
||||
url: string;
|
||||
cover: DeezerImage;
|
||||
releaseDate?: Date;
|
||||
|
||||
constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.title = data.title;
|
||||
this.url = `https://www.deezer.com/album/${data.id}/`;
|
||||
this.cover = {
|
||||
xl: data.cover_xl,
|
||||
big: data.cover_big,
|
||||
medium: data.cover_medium,
|
||||
small: data.cover_small
|
||||
};
|
||||
|
||||
if (data.release_date) this.releaseDate = new Date(data.release_date);
|
||||
}
|
||||
}
|
||||
|
||||
class DeezerArtist {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
picture?: DeezerImage;
|
||||
role?: string;
|
||||
|
||||
constructor(data: any) {
|
||||
this.id = data.id;
|
||||
this.name = data.name;
|
||||
|
||||
this.url = data.link ? data.link : `https://www.deezer.com/artist/${data.id}/`;
|
||||
|
||||
if (data.picture_xl)
|
||||
this.picture = {
|
||||
xl: data.picture_xl,
|
||||
big: data.picture_big,
|
||||
medium: data.picture_medium,
|
||||
small: data.picture_small
|
||||
};
|
||||
|
||||
if (data.role) this.role = data.role;
|
||||
}
|
||||
}
|
||||
212
play-dl/Deezer/index.ts
Normal file
212
play-dl/Deezer/index.ts
Normal file
@ -0,0 +1,212 @@
|
||||
import { URL } from 'url';
|
||||
import { request, request_resolve_redirect } from '../Request';
|
||||
import { DeezerAlbum, DeezerPlaylist, DeezerTrack } from './classes';
|
||||
|
||||
interface TypeData {
|
||||
type: 'track' | 'playlist' | 'album' | 'search' | 'share' | false;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
interface DeezerSearchOptions {
|
||||
type?: 'track' | 'playlist' | 'album';
|
||||
limit?: number;
|
||||
fuzzy?: boolean;
|
||||
}
|
||||
|
||||
function internalValidate(url: string): TypeData {
|
||||
let urlObj;
|
||||
try {
|
||||
// will throw a TypeError if the input is not a valid URL so we need to catch it
|
||||
urlObj = new URL(url);
|
||||
} catch {
|
||||
return { type: 'search' };
|
||||
}
|
||||
|
||||
if (urlObj.protocol !== 'https:' && urlObj.protocol !== 'http:') {
|
||||
return { type: 'search' };
|
||||
}
|
||||
|
||||
let pathname = urlObj.pathname;
|
||||
if (pathname.endsWith('/')) {
|
||||
pathname = pathname.slice(0, -1);
|
||||
}
|
||||
const path = pathname.split('/');
|
||||
switch (urlObj.hostname) {
|
||||
case 'deezer.com':
|
||||
case 'www.deezer.com': {
|
||||
if (path.length === 4) {
|
||||
const lang = path.splice(1, 1)[0];
|
||||
if (!lang.match(/^[a-z]{2}$/)) {
|
||||
return { type: false };
|
||||
}
|
||||
} else if (path.length !== 3) {
|
||||
return { type: false };
|
||||
}
|
||||
|
||||
if ((path[1] === 'track' || path[1] === 'album' || path[1] === 'playlist') && path[2].match(/^[0-9]+$/)) {
|
||||
return {
|
||||
type: path[1],
|
||||
id: path[2]
|
||||
};
|
||||
} else {
|
||||
return { type: false };
|
||||
}
|
||||
}
|
||||
case 'api.deezer.com': {
|
||||
if (
|
||||
path.length === 3 &&
|
||||
(path[1] === 'track' || path[1] === 'album' || path[1] === 'playlist') &&
|
||||
path[2].match(/^[0-9]+$/)
|
||||
) {
|
||||
return {
|
||||
type: path[1],
|
||||
id: path[2]
|
||||
};
|
||||
} else {
|
||||
return { type: false };
|
||||
}
|
||||
}
|
||||
case 'deezer.page.link': {
|
||||
if (path.length === 2 && path[1].match(/^[A-Za-z0-9]+$/)) {
|
||||
return { type: 'share' };
|
||||
} else {
|
||||
return { type: false };
|
||||
}
|
||||
}
|
||||
default:
|
||||
return { type: 'search' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared type for Deezer tracks, playlists and albums
|
||||
*/
|
||||
export type Deezer = DeezerTrack | DeezerPlaylist | DeezerAlbum;
|
||||
|
||||
/**
|
||||
* Fetches the information for a track, playlist or album on Deezer
|
||||
* @param url The track, playlist or album URL
|
||||
* @returns A {@link DeezerTrack}, {@link DeezerPlaylist} or {@link DeezerAlbum}
|
||||
* object depending on the provided URL.
|
||||
*/
|
||||
export async function deezer(url: string): Promise<Deezer> {
|
||||
const typeData = internalValidate(url);
|
||||
|
||||
if (!typeData.type || typeData.type === 'search')
|
||||
throw new Error('This is not a Deezer track, playlist or album URL');
|
||||
|
||||
if (typeData.type === 'share') {
|
||||
const resolvedURL = await internalResolve(url);
|
||||
return await deezer(resolvedURL);
|
||||
}
|
||||
|
||||
const response = await request(`https://api.deezer.com/${typeData.type}/${typeData.id}`).catch((err: Error) => err);
|
||||
|
||||
if (response instanceof Error) throw response;
|
||||
|
||||
const jsonData = JSON.parse(response);
|
||||
|
||||
if (jsonData.error) {
|
||||
throw new Error(`Deezer API Error: ${jsonData.error.type}: ${jsonData.error.message}`);
|
||||
}
|
||||
|
||||
switch (typeData.type) {
|
||||
case 'track':
|
||||
return new DeezerTrack(jsonData, false);
|
||||
case 'playlist':
|
||||
return new DeezerPlaylist(jsonData, false);
|
||||
case 'album':
|
||||
return new DeezerAlbum(jsonData, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a Deezer URL
|
||||
* @param url The URL to validate
|
||||
* @returns The type of the URL either 'track', 'playlist', 'album', 'search', 'share' or false.
|
||||
* false means that the provided URL was a wrongly formatted or unsupported Deezer URL.
|
||||
*/
|
||||
export function dz_validate(url: string): 'track' | 'playlist' | 'album' | 'search' | 'share' | false {
|
||||
return internalValidate(url).type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches Deezer for tracks, playlists or albums
|
||||
* @param query The search query
|
||||
* @param options Extra options to configure the search:
|
||||
*
|
||||
* type?: The type to search for `'track'`, `'playlist'` or `'album'`. Defaults to `'track'`.
|
||||
*
|
||||
* limit?: The maximum number of results to return, maximum `100`, defaults to `10`.
|
||||
*
|
||||
* fuzzy?: Whether the search should be fuzzy or only return exact matches. Defaults to `true`.
|
||||
* @returns An array of tracks, playlists or albums
|
||||
*/
|
||||
export async function dz_search(query: string, options: DeezerSearchOptions): Promise<Deezer[]> {
|
||||
let query_ = query.trim();
|
||||
|
||||
const type = options.type ?? 'track';
|
||||
const limit = options.limit ?? 10;
|
||||
const fuzzy = options.fuzzy ?? true;
|
||||
|
||||
if (query_.length === 0) throw new Error('A query is required to search.');
|
||||
if (limit > 100) throw new Error('The maximum search limit for Deezer is 100');
|
||||
if (limit < 1) throw new Error('The minimum search limit for Deezer is 1');
|
||||
if (type !== 'track' && type !== 'album' && type != 'playlist')
|
||||
throw new Error(`"${type}" is not a valid Deezer search type`);
|
||||
|
||||
query_ = encodeURIComponent(query_);
|
||||
const response = await request(
|
||||
`https://api.deezer.com/search/${type}/?q=${query_}&limit=${limit}${fuzzy ? '' : 'strict=on'}`
|
||||
).catch((err: Error) => err);
|
||||
|
||||
if (response instanceof Error) throw response;
|
||||
|
||||
const jsonData = JSON.parse(response);
|
||||
|
||||
if (jsonData.error) {
|
||||
throw new Error(`Deezer API Error: ${jsonData.error.type}: ${jsonData.error.message}`);
|
||||
}
|
||||
|
||||
let results: Deezer[] = [];
|
||||
switch (type) {
|
||||
case 'track':
|
||||
results = jsonData.data.map((track: any) => new DeezerTrack(track, true));
|
||||
break;
|
||||
case 'playlist':
|
||||
results = jsonData.data.map((playlist: any) => new DeezerPlaylist(playlist, true));
|
||||
break;
|
||||
case 'album':
|
||||
results = jsonData.data.map((album: any) => new DeezerAlbum(album, true));
|
||||
break;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function internalResolve(url: string): Promise<string> {
|
||||
const resolved = await request_resolve_redirect(url);
|
||||
const urlObj = new URL(resolved);
|
||||
urlObj.search = ''; // remove tracking parameters, not needed and also make that URL unnecessarily longer
|
||||
return urlObj.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a Deezer share link (deezer.page.link) to the equivalent Deezer link.
|
||||
*
|
||||
* The {@link deezer} function automatically does this if {@link dz_validate} returns 'share'.
|
||||
*
|
||||
* @param url The Deezer share link (deezer.page.link) to resolve
|
||||
* @returns The resolved URL.
|
||||
*/
|
||||
export async function dz_resolve_share_url(url: string): Promise<string> {
|
||||
const typeData = internalValidate(url);
|
||||
|
||||
if (typeData.type === 'share') {
|
||||
return await internalResolve(url);
|
||||
} else if (typeData.type === 'track' || typeData.type === 'playlist' || typeData.type === 'album') {
|
||||
return url;
|
||||
} else {
|
||||
throw new Error('This is not a valid Deezer URL');
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@ export type ProxyOptions = ProxyOpts | string;
|
||||
|
||||
interface RequestOpts extends RequestOptions {
|
||||
body?: string;
|
||||
method?: 'GET' | 'POST';
|
||||
method?: 'GET' | 'POST' | 'HEAD';
|
||||
proxies?: ProxyOptions[];
|
||||
cookies?: boolean;
|
||||
}
|
||||
@ -119,6 +119,23 @@ export function request(req_url: string, options: RequestOpts = { method: 'GET'
|
||||
});
|
||||
}
|
||||
|
||||
export function request_resolve_redirect(url: string): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let res = await https_getter(url, { method: 'HEAD' }).catch((err: Error) => err);
|
||||
if (res instanceof Error) {
|
||||
reject(res);
|
||||
return;
|
||||
}
|
||||
const statusCode = Number(res.statusCode);
|
||||
if (statusCode >= 300 && statusCode < 400) {
|
||||
const resolved = await request_resolve_redirect(res.headers.location as string);
|
||||
resolve(resolved);
|
||||
} else {
|
||||
resolve(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses one random number between max and min number.
|
||||
* @param min Minimum number
|
||||
|
||||
@ -10,6 +10,7 @@ export {
|
||||
} from './YouTube';
|
||||
export { spotify, sp_validate, refreshToken, is_expired, Spotify } from './Spotify';
|
||||
export { soundcloud, so_validate, SoundCloud, SoundCloudStream, getFreeClientID } from './SoundCloud';
|
||||
export { deezer, dz_validate, dz_search, dz_resolve_share_url, Deezer } from './Deezer';
|
||||
export { setToken } from './token';
|
||||
|
||||
enum AudioPlayerStatus {
|
||||
@ -26,6 +27,7 @@ interface SearchOptions {
|
||||
youtube?: 'video' | 'playlist' | 'channel';
|
||||
spotify?: 'album' | 'playlist' | 'track';
|
||||
soundcloud?: 'tracks' | 'playlists' | 'albums';
|
||||
deezer?: 'track' | 'playlist' | 'album';
|
||||
};
|
||||
}
|
||||
|
||||
@ -47,6 +49,7 @@ import { InfoData, stream as yt_stream, StreamOptions, stream_from_info as yt_st
|
||||
import { SoundCloudTrack } from './SoundCloud/classes';
|
||||
import { yt_search } from './YouTube/search';
|
||||
import { EventEmitter } from 'stream';
|
||||
import { Deezer, dz_search, dz_validate } from './Deezer';
|
||||
|
||||
/**
|
||||
* Main stream Command for streaming through various sources
|
||||
@ -62,6 +65,11 @@ export async function stream(url: string, options: StreamOptions = {}): Promise<
|
||||
'Streaming from Spotify is not supported. Please use search() to find a similar track on YouTube or SoundCloud instead.'
|
||||
);
|
||||
}
|
||||
if (url.indexOf('deezer') !== -1) {
|
||||
throw new Error(
|
||||
'Streaming from Deezer is not supported. Please use search() to find a similar track on YouTube or SoundCloud instead.'
|
||||
);
|
||||
}
|
||||
if (url.indexOf('soundcloud') !== -1) return await so_stream(url, options.quality);
|
||||
else return await yt_stream(url, options);
|
||||
}
|
||||
@ -70,17 +78,19 @@ export async function stream(url: string, options: StreamOptions = {}): Promise<
|
||||
* Main Search Command for searching through various sources
|
||||
* @param query string to search.
|
||||
* @param options contains limit and source to choose.
|
||||
* @returns Array of YouTube or Spotify or SoundCloud
|
||||
* @returns Array of YouTube or Spotify or SoundCloud or Deezer
|
||||
*/
|
||||
export async function search(
|
||||
query: string,
|
||||
options: SearchOptions = {}
|
||||
): Promise<YouTube[] | Spotify[] | SoundCloud[]> {
|
||||
): Promise<YouTube[] | Spotify[] | SoundCloud[] | Deezer[]> {
|
||||
if (!options.source) options.source = { youtube: 'video' };
|
||||
query = encodeURIComponent(query);
|
||||
if (options.source.youtube) return await yt_search(query, { limit: options.limit, type: options.source.youtube });
|
||||
else if (options.source.spotify) return await sp_search(query, options.source.spotify, options.limit);
|
||||
else if (options.source.soundcloud) return await so_search(query, options.source.soundcloud, options.limit);
|
||||
else if (options.source.deezer)
|
||||
return await dz_search(query, { limit: options.limit, type: options.source.deezer });
|
||||
else throw new Error('Not possible to reach Here LOL. Easter Egg of play-dl if someone get this.');
|
||||
}
|
||||
|
||||
@ -106,7 +116,19 @@ export async function stream_from_info(
|
||||
export async function validate(
|
||||
url: string
|
||||
): Promise<
|
||||
'so_playlist' | 'so_track' | 'sp_track' | 'sp_album' | 'sp_playlist' | 'yt_video' | 'yt_playlist' | 'search' | false
|
||||
| 'so_playlist'
|
||||
| 'so_track'
|
||||
| 'sp_track'
|
||||
| 'sp_album'
|
||||
| 'sp_playlist'
|
||||
| 'dz_track'
|
||||
| 'dz_playlist'
|
||||
| 'dz_album'
|
||||
| 'dz_share'
|
||||
| 'yt_video'
|
||||
| 'yt_playlist'
|
||||
| 'search'
|
||||
| false
|
||||
> {
|
||||
let check;
|
||||
if (!url.startsWith('https')) return 'search';
|
||||
@ -116,6 +138,9 @@ export async function validate(
|
||||
} else if (url.indexOf('soundcloud') !== -1) {
|
||||
check = await so_validate(url);
|
||||
return check !== false ? (('so_' + check) as 'so_playlist' | 'so_track') : false;
|
||||
} else if (url.indexOf('deezer') !== -1) {
|
||||
check = dz_validate(url);
|
||||
return check !== false ? (('dz_' + check) as 'dz_track' | 'dz_playlist' | 'dz_album' | 'dz_share') : false;
|
||||
} else {
|
||||
check = yt_validate(url);
|
||||
return check !== false ? (('yt_' + check) as 'yt_video' | 'yt_playlist') : false;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user