296 lines
9.6 KiB
TypeScript
296 lines
9.6 KiB
TypeScript
import { URL } from 'node:url';
|
|
import { request, request_resolve_redirect } from '../Request';
|
|
import { DeezerAlbum, DeezerPlaylist, DeezerTrack } from './classes';
|
|
|
|
interface TypeData {
|
|
type: 'track' | 'playlist' | 'album' | 'search' | false;
|
|
id?: string;
|
|
error?: string;
|
|
}
|
|
|
|
interface DeezerSearchOptions {
|
|
/**
|
|
* The type to search for `'track'`, `'playlist'` or `'album'`. Defaults to `'track'`.
|
|
*/
|
|
type?: 'track' | 'playlist' | 'album';
|
|
/**
|
|
* The maximum number of results to return, maximum `100`, defaults to `10`.
|
|
*/
|
|
limit?: number;
|
|
/**
|
|
* Whether the search should be fuzzy or only return exact matches. Defaults to `true`.
|
|
*/
|
|
fuzzy?: boolean;
|
|
}
|
|
|
|
interface DeezerAdvancedSearchOptions {
|
|
/**
|
|
* The maximum number of results to return, maximum `100`, defaults to `10`.
|
|
*/
|
|
limit?: number;
|
|
/**
|
|
* The name of the artist.
|
|
*/
|
|
artist?: string;
|
|
/**
|
|
* The title of the album.
|
|
*/
|
|
album?: string;
|
|
/**
|
|
* The title of the track.
|
|
*/
|
|
title?: string;
|
|
/**
|
|
* The label that released the track.
|
|
*/
|
|
label?: string;
|
|
/**
|
|
* The minimum duration in seconds.
|
|
*/
|
|
minDurationInSec?: number;
|
|
/**
|
|
* The maximum duration in seconds.
|
|
*/
|
|
maxDurationInSec?: number;
|
|
/**
|
|
* The minimum BPM.
|
|
*/
|
|
minBPM?: number;
|
|
/**
|
|
* The minimum BPM.
|
|
*/
|
|
maxBPM?: number;
|
|
}
|
|
|
|
async function internalValidate(url: string): Promise<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(/^\d+$/)) {
|
|
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(/^\d+$/)
|
|
) {
|
|
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]+$/)) {
|
|
const resolved = await request_resolve_redirect(url).catch((err) => err);
|
|
|
|
if (resolved instanceof Error) {
|
|
return { type: false, error: resolved.message };
|
|
}
|
|
|
|
return await internalValidate(resolved);
|
|
} 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 = await internalValidate(url.trim());
|
|
|
|
if (typeData.error) {
|
|
throw new Error(`This is not a Deezer track, playlist or album URL:\n${typeData.error}`);
|
|
} else if (!typeData.type || typeData.type === 'search')
|
|
throw new Error('This is not a Deezer track, playlist or album URL');
|
|
|
|
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'` or `false`.
|
|
* `false` means that the provided URL was a wrongly formatted or an unsupported Deezer URL.
|
|
*/
|
|
export async function dz_validate(url: string): Promise<'track' | 'playlist' | 'album' | 'search' | false> {
|
|
const typeData = await internalValidate(url.trim());
|
|
return typeData.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;
|
|
}
|
|
|
|
/**
|
|
* Searches Deezer for tracks using the specified metadata.
|
|
* @param options The metadata and limit for the search
|
|
*
|
|
* * limit?: The maximum number of results to return, maximum `100`, defaults to `10`.
|
|
* * artist?: The name of the artist
|
|
* * album?: The title of the album
|
|
* * title?: The title of the track
|
|
* * label?: The label that released the track
|
|
* * minDurationInSec?: The minimum duration in seconds
|
|
* * maxDurationInSec?: The maximum duration in seconds
|
|
* * minBpm?: The minimum BPM
|
|
* * maxBpm?: The minimum BPM
|
|
* @returns An array of tracks matching the metadata
|
|
*/
|
|
export async function dz_advanced_track_search(options: DeezerAdvancedSearchOptions): Promise<DeezerTrack[]> {
|
|
const limit = options.limit ?? 10;
|
|
|
|
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');
|
|
|
|
const metadata: string[] = [];
|
|
if (options.artist) metadata.push(`artist:"${encodeURIComponent(options.artist.trim())}"`);
|
|
|
|
if (options.album) metadata.push(`album:"${encodeURIComponent(options.album.trim())}"`);
|
|
|
|
if (options.title) metadata.push(`track:"${encodeURIComponent(options.title.trim())}"`);
|
|
|
|
if (options.label) metadata.push(`label:"${encodeURIComponent(options.label.trim())}"`);
|
|
|
|
if (!isNaN(Number(options.minDurationInSec))) metadata.push(`dur_min:${options.minDurationInSec}`);
|
|
|
|
if (!isNaN(Number(options.maxDurationInSec))) metadata.push(`dur_max:${options.maxDurationInSec}`);
|
|
|
|
if (!isNaN(Number(options.minBPM))) metadata.push(`bpm_min:${options.minBPM}`);
|
|
|
|
if (!isNaN(Number(options.maxBPM))) metadata.push(`bpm_max:${options.maxBPM}`);
|
|
|
|
if (metadata.length === 0) throw new Error('At least one type of metadata is required.');
|
|
|
|
const response = await request(`https://api.deezer.com/search/track/?q=${metadata.join(' ')}&limit=${limit}`).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}`);
|
|
}
|
|
|
|
const results = jsonData.data.map((track: any) => new DeezerTrack(track, true));
|
|
|
|
return results;
|
|
}
|
|
|
|
export { DeezerTrack, DeezerAlbum, DeezerPlaylist };
|