import { URL } from '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 { type?: 'track' | 'playlist' | 'album'; limit?: number; fuzzy?: boolean; } async function internalValidate(url: string): Promise { 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]+$/)) { 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 { const typeData = await internalValidate(url); 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 unsupported Deezer URL. */ export async function dz_validate(url: string): Promise<'track' | 'playlist' | 'album' | 'search' | false> { const typeData = await internalValidate(url); 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 { 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; }