From cb777457768175260c8b5fe25dd64399f22843d8 Mon Sep 17 00:00:00 2001 From: Roberto Tonino Date: Tue, 1 Jun 2021 22:35:49 +0200 Subject: [PATCH] feat(server): analyzeLink endpoint; test(server): analyzeLink unit tests; chore(server): linting --- server/src/main.ts | 27 ++-- server/src/routes/api/get/analyzeLink.spec.ts | 30 ++++ server/src/routes/api/get/analyzeLink.ts | 47 ++++++ server/src/routes/api/get/getQueue.ts | 2 +- server/src/routes/api/get/getTracklist.ts | 34 ++--- .../routes/api/get/getUserSpotifyPlaylists.ts | 34 ++--- server/src/routes/api/get/index.ts | 2 + server/src/routes/api/post/index.ts | 16 +-- .../routes/api/post/loginWithCredentials.ts | 2 +- server/src/routes/api/post/removeFromQueue.ts | 14 +- server/src/routes/api/post/saveSettings.ts | 7 +- server/src/types.ts | 135 ++++++++++++++++++ 12 files changed, 283 insertions(+), 67 deletions(-) create mode 100644 server/src/routes/api/get/analyzeLink.spec.ts diff --git a/server/src/main.ts b/server/src/main.ts index dcee43a..b13469e 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -27,7 +27,7 @@ export const listener = { send(key: string, data?: any) { if (data) console.log(key, data) else console.log(key) - if (["downloadInfo", "downloadWarn"].includes(key)) return + if (['downloadInfo', 'downloadWarn'].includes(key)) return wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ key, data })) @@ -37,7 +37,7 @@ export const listener = { } export function getSettings(): any { - return {settings, defaultSettings, spotifySettings: plugins.spotify.getCredentials()} + return { settings, defaultSettings, spotifySettings: plugins.spotify.getCredentials() } } export function saveSettings(newSettings: any, newSpotifySettings: any) { @@ -56,33 +56,33 @@ export async function addToQueue(dz: any, url: string[], bitrate: number) { if (!dz.logged_in) throw new NotLoggedIn() let downloadObjs: any[] = [] - let link: string = "" + let link: string = '' const requestUUID = uuidv4() - if (url.length > 1){ - listener.send("startGeneratingItems", {uuid: requestUUID, total: url.length}) + if (url.length > 1) { + listener.send('startGeneratingItems', { uuid: requestUUID, total: url.length }) } - for (let i = 0; i < url.length; i++){ + for (let i = 0; i < url.length; i++) { link = url[i] console.log(`Adding ${link} to queue`) - let downloadObj = await deemix.generateDownloadObject(dz, link, bitrate, plugins, listener) - if (Array.isArray(downloadObj)){ + const downloadObj = await deemix.generateDownloadObject(dz, link, bitrate, plugins, listener) + if (Array.isArray(downloadObj)) { downloadObjs = downloadObjs.concat(downloadObj) } else { downloadObjs.push(downloadObj) } } - if (url.length > 1){ - listener.send("finishGeneratingItems", {uuid: requestUUID, total: downloadObjs.length}) + if (url.length > 1) { + listener.send('finishGeneratingItems', { uuid: requestUUID, total: downloadObjs.length }) } const slimmedObjects: any[] = [] downloadObjs.forEach((downloadObj: any, pos: number) => { // Check if element is already in queue - if (Object.keys(queue).includes(downloadObj.uuid)){ + if (Object.keys(queue).includes(downloadObj.uuid)) { listener.send('alreadyInQueue', downloadObj.getEssentialDict()) delete downloadObjs[pos] return @@ -133,7 +133,10 @@ export async function startQueue(dz: any): Promise { case 'Convertable': downloadObject = new Convertable(currentItem) downloadObject = await plugins[downloadObject.plugin].convert(dz, downloadObject, settings, listener) - fs.writeFileSync(configFolder + `queue${sep}${downloadObject.uuid}.json`, JSON.stringify({...downloadObject.toDict(), status: 'inQueue'})) + fs.writeFileSync( + configFolder + `queue${sep}${downloadObject.uuid}.json`, + JSON.stringify({ ...downloadObject.toDict(), status: 'inQueue' }) + ) break } currentJob = new Downloader(dz, downloadObject, settings, listener) diff --git a/server/src/routes/api/get/analyzeLink.spec.ts b/server/src/routes/api/get/analyzeLink.spec.ts new file mode 100644 index 0000000..5810501 --- /dev/null +++ b/server/src/routes/api/get/analyzeLink.spec.ts @@ -0,0 +1,30 @@ +import { appSendGet } from '../../../../tests/utils' + +describe('analyzeLink requests', () => { + it('should respond 200 to calls with supported term', async () => { + const res = await appSendGet('/api/analyzeLink/?term=https://www.deezer.com/en/album/100896762') + + expect(res.status).toBe(200) + }) + + it('should respond with an error to calls with not supported term', async () => { + const res = await appSendGet('/api/analyzeLink/?term=https://www.deezer.com/en/artist/15166511') + + expect(res.status).toBe(400) + expect(res.body.errorMessage).toBe('Not supported') + }) + + it('should respond album analyzed data', async () => { + const res = await appSendGet('/api/analyzeLink/?term=https://www.deezer.com/en/album/100896762') + + expect(res.body.type).toBe('album') + expect(res.body.artist.name).toBe('Lil Nas X') + }) + + it('should respond track analyzed data', async () => { + const res = await appSendGet('/api/analyzeLink/?term=https://www.deezer.com/en/track/1283264142') + + expect(res.body.type).toBe('track') + expect(res.body.artist.name).toBe('Lil Nas X') + }) +}) diff --git a/server/src/routes/api/get/analyzeLink.ts b/server/src/routes/api/get/analyzeLink.ts index e69de29..4d5a150 100644 --- a/server/src/routes/api/get/analyzeLink.ts +++ b/server/src/routes/api/get/analyzeLink.ts @@ -0,0 +1,47 @@ +import type { RequestHandler } from 'express' +// @ts-expect-error +import deemix from 'deemix' +// @ts-expect-error +import { Deezer } from 'deezer-js' + +import type { ApiHandler, GetTrackResponse, GetAlbumResponse } from '../../../types' +import { sessionDZ } from '../../../main' + +export interface AnalyzeQuery { + term?: string +} + +type ResBody = GetAlbumResponse | GetTrackResponse + +const path: ApiHandler['path'] = '/analyzeLink' + +const handler: RequestHandler = async (req, res) => { + try { + if (!req.query || !req.query.term) { + return res.status(400).send({ errorMessage: 'No term specified', errorCode: 'AL01' }) + } + + const { term: linkToAnalyze } = req.query + const [, linkType, linkId] = await deemix.parseLink(linkToAnalyze) + const isTrackOrAlbum = ['track', 'album'].includes(linkType) + + if (isTrackOrAlbum) { + if (!sessionDZ[req.session.id]) sessionDZ[req.session.id] = new Deezer() + const dz = sessionDZ[req.session.id] + const apiMethod = linkType === 'track' ? 'get_track' : 'get_album' + const resBody: ResBody = await dz.api[apiMethod](linkId) + + return res.status(200).send(resBody) + } + + return res.status(400).send({ errorMessage: 'Not supported', errorCode: 'AL02' }) + } catch (error) { + return res + .status(500) + .send({ errorMessage: 'The server had a problem. Please try again', errorObject: error, errorCode: 'AL03' }) + } +} + +const apiHandler: ApiHandler = { path, handler } + +export default apiHandler diff --git a/server/src/routes/api/get/getQueue.ts b/server/src/routes/api/get/getQueue.ts index 479d890..37eda9e 100644 --- a/server/src/routes/api/get/getQueue.ts +++ b/server/src/routes/api/get/getQueue.ts @@ -7,7 +7,7 @@ const path: ApiHandler['path'] = '/getQueue' // let homeCache: any const handler: ApiHandler['handler'] = (_, res) => { - const result:any = { + const result: any = { queue, order: queueOrder } diff --git a/server/src/routes/api/get/getTracklist.ts b/server/src/routes/api/get/getTracklist.ts index c68abb7..7f34061 100644 --- a/server/src/routes/api/get/getTracklist.ts +++ b/server/src/routes/api/get/getTracklist.ts @@ -20,42 +20,42 @@ const handler: ApiHandler['handler'] = async (req, res) => { } case 'spotifyplaylist': case 'spotify_playlist': { - if (!plugins.spotify.enabled){ + if (!plugins.spotify.enabled) { res.send({ collaborative: false, - description: "", - external_urls: {spotify: null}, - followers: {total: 0, href: null}, + description: '', + external_urls: { spotify: null }, + followers: { total: 0, href: null }, id: null, images: [], - name: "Something went wrong", + name: 'Something went wrong', owner: { - display_name: "Error", + display_name: 'Error', id: null }, public: true, - tracks : [], + tracks: [], type: 'playlist', uri: null }) break } - let sp = plugins.spotify.sp + const sp = plugins.spotify.sp let playlist = await sp.getPlaylist(list_id) playlist = playlist.body let tracklist = playlist.tracks.items while (playlist.tracks.next) { - let regExec = /offset=(\d+)&limit=(\d+)/g.exec(playlist.tracks.next) - let offset = regExec![1] - let limit = regExec![2] - let playlistTracks = await sp.getPlaylistTracks(list_id, { offset, limit }) - playlist.tracks = playlistTracks.body - tracklist = tracklist.concat(playlist.tracks.items) - } - tracklist.forEach((item:any, i:number) => { + const regExec = /offset=(\d+)&limit=(\d+)/g.exec(playlist.tracks.next) + const offset = regExec![1] + const limit = regExec![2] + const playlistTracks = await sp.getPlaylistTracks(list_id, { offset, limit }) + playlist.tracks = playlistTracks.body + tracklist = tracklist.concat(playlist.tracks.items) + } + tracklist.forEach((item: any, i: number) => { tracklist[i] = item.track tracklist[i].selected = false - }); + }) playlist.tracks = tracklist res.send(playlist) break diff --git a/server/src/routes/api/get/getUserSpotifyPlaylists.ts b/server/src/routes/api/get/getUserSpotifyPlaylists.ts index 0aecd00..77ecfcb 100644 --- a/server/src/routes/api/get/getUserSpotifyPlaylists.ts +++ b/server/src/routes/api/get/getUserSpotifyPlaylists.ts @@ -6,25 +6,25 @@ const path: ApiHandler['path'] = '/getUserSpotifyPlaylists' const handler: ApiHandler['handler'] = async (req, res) => { let data - if (plugins.spotify.enabled){ - let sp = plugins.spotify.sp + if (plugins.spotify.enabled) { + const sp = plugins.spotify.sp const username = req.query.spotifyUser - data = [] - let playlists = await sp.getUserPlaylists(username) - let playlistList = playlists.body.items - while (playlists.next) { - let regExec = /offset=(\d+)&limit=(\d+)/g.exec(playlists.next) - let offset = regExec![1] - let limit = regExec![2] - let newPlaylists = await sp.getUserPlaylists(username, { offset, limit }) - playlists = newPlaylists.body - playlistList = playlistList.concat(playlists.items) - } - playlistList.forEach((playlist: any) => { - data.push(plugins.spotify._convertPlaylistStructure(playlist)) - }) + data = [] + let playlists = await sp.getUserPlaylists(username) + let playlistList = playlists.body.items + while (playlists.next) { + const regExec = /offset=(\d+)&limit=(\d+)/g.exec(playlists.next) + const offset = regExec![1] + const limit = regExec![2] + const newPlaylists = await sp.getUserPlaylists(username, { offset, limit }) + playlists = newPlaylists.body + playlistList = playlistList.concat(playlists.items) + } + playlistList.forEach((playlist: any) => { + data.push(plugins.spotify._convertPlaylistStructure(playlist)) + }) } else { - data = { error: 'spotifyNotEnabled'} + data = { error: 'spotifyNotEnabled' } } res.send(data) } diff --git a/server/src/routes/api/get/index.ts b/server/src/routes/api/get/index.ts index f4a2f96..1ece60c 100644 --- a/server/src/routes/api/get/index.ts +++ b/server/src/routes/api/get/index.ts @@ -1,3 +1,4 @@ +import analyzeLink from './analyzeLink' import getHome from './getHome' import getCharts from './getCharts' import mainSearch from './mainSearch' @@ -16,6 +17,7 @@ import getQueue from './getQueue' export default [ albumSearch, + analyzeLink, getHome, getCharts, getChartTracks, diff --git a/server/src/routes/api/post/index.ts b/server/src/routes/api/post/index.ts index 582c621..88f5a07 100644 --- a/server/src/routes/api/post/index.ts +++ b/server/src/routes/api/post/index.ts @@ -8,12 +8,12 @@ import logout from './logout' import saveSettings from './saveSettings' export default [ - loginArl, - addToQueue, - loginWithCredentials, - cancelAllDownloads, - removeFinishedDownloads, - removeFromQueue, - logout, - saveSettings + loginArl, + addToQueue, + loginWithCredentials, + cancelAllDownloads, + removeFinishedDownloads, + removeFromQueue, + logout, + saveSettings ] diff --git a/server/src/routes/api/post/loginWithCredentials.ts b/server/src/routes/api/post/loginWithCredentials.ts index 9726baa..5841e80 100644 --- a/server/src/routes/api/post/loginWithCredentials.ts +++ b/server/src/routes/api/post/loginWithCredentials.ts @@ -7,7 +7,7 @@ const handler: ApiHandler['handler'] = async (req, res) => { const { email, password } = req.body let accessToken = req.body.accessToken - if (!accessToken){ + if (!accessToken) { accessToken = await getAccessToken(email, password) } let arl diff --git a/server/src/routes/api/post/removeFromQueue.ts b/server/src/routes/api/post/removeFromQueue.ts index 40fdb01..a46f772 100644 --- a/server/src/routes/api/post/removeFromQueue.ts +++ b/server/src/routes/api/post/removeFromQueue.ts @@ -4,13 +4,13 @@ import { cancelDownload } from '../../../main' const path = '/removeFromQueue' const handler: ApiHandler['handler'] = async (req, res) => { - const {uuid} = req.query - if (uuid){ - cancelDownload(uuid) - res.send({ result: true }) - }else{ - res.send({ result: false }) - } + const { uuid } = req.query + if (uuid) { + cancelDownload(uuid) + res.send({ result: true }) + } else { + res.send({ result: false }) + } } const apiHandler = { path, handler } diff --git a/server/src/routes/api/post/saveSettings.ts b/server/src/routes/api/post/saveSettings.ts index f1eec4e..dd3f31a 100644 --- a/server/src/routes/api/post/saveSettings.ts +++ b/server/src/routes/api/post/saveSettings.ts @@ -1,6 +1,5 @@ -import { ApiHandler } from '../../../types' +import { ApiHandler, Settings, SpotifySettings } from '../../../types' import { saveSettings, listener } from '../../../main' -import { Settings, SpotifySettings } from '../../../types' const path = '/saveSettings' @@ -10,8 +9,8 @@ export interface SaveSettingsData { } const handler: ApiHandler['handler'] = async (req, res) => { - const { settings, spotifySettings }: SaveSettingsData = req.query - saveSettings(settings, spotifySettings) + const { settings, spotifySettings }: SaveSettingsData = req.query + saveSettings(settings, spotifySettings) listener.send('updateSettings', { settings, spotifySettings }) res.send({ result: true }) } diff --git a/server/src/types.ts b/server/src/types.ts index f6e6f1d..629aefc 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -96,3 +96,138 @@ export interface Settings { // TODO export interface SpotifySettings {} + +interface BaseDeezerObject { + id: number + type: string +} + +interface NamedDeezerObject extends BaseDeezerObject { + name: string +} + +interface PicturedDeezerObject extends BaseDeezerObject { + picture: string + picture_small: string + picture_medium: string + picture_big: string + picture_xl: string +} + +interface CoveredDeezerObject extends BaseDeezerObject { + cover: string + cover_small: string + cover_medium: string + cover_big: string + cover_xl: string +} + +interface DeezerWrapper { + data: Type[] +} + +export interface DeezerContributor extends NamedDeezerObject, PicturedDeezerObject { + link: string + share: string + radio: boolean + tracklist: string + role: string +} + +export interface DeezerTrackArtist extends NamedDeezerObject, PicturedDeezerObject { + link: string + share: string + radio: boolean + tracklist: string +} + +export interface DeezerAlbumArtist extends NamedDeezerObject, PicturedDeezerObject { + tracklist: string +} + +export interface DeezerAlbum extends BaseDeezerObject, CoveredDeezerObject { + title: string + link: string + md5_image: string + release_date: string + tracklist: string +} + +export interface DeezerGenre extends NamedDeezerObject { + picture: string +} + +type DeezerGenres = DeezerWrapper + +export interface GetAlbumTrackArtist extends NamedDeezerObject { + tracklist: string +} + +export interface DeezerTrack extends BaseDeezerObject { + readable: boolean + title: string + title_short: string + title_version: string + link: string + duration: number + rank: number + explicit_lyrics: boolean + explicit_content_lyrics: number + explicit_content_cover: number + preview: string + md5_image: string + artist: GetAlbumTrackArtist +} + +type DeezerTracks = DeezerWrapper + +export interface GetTrackResponse extends BaseDeezerObject { + readable: boolean + title: string + title_short: string + title_version: string + isrc: string + link: string + share: string + duration: number + track_position: number + disk_number: number + rank: number + release_date: string + explicit_lyrics: boolean + explicit_content_lyrics: number + explicit_content_cover: number + preview: string + bpm: number + gain: number + available_countries: string[] + contributors: DeezerContributor[] + md5_image: string + artist: DeezerTrackArtist + album: DeezerAlbum +} + +export interface GetAlbumResponse extends BaseDeezerObject, CoveredDeezerObject { + title: string + upc: string + link: string + share: string + md5_image: string + genre_id: number + genres: DeezerGenres + label: string + nb_tracks: number + duration: number + fans: number + rating: number + release_date: string + record_type: string + available: boolean + tracklist: string + explicit_lyrics: boolean + explicit_content_lyrics: number + explicit_content_cover: number + contributors: DeezerContributor[] + artist: DeezerAlbumArtist + tracks: DeezerTracks +}