From 5e81e02599601dd1c45220473d3bd88f918598d7 Mon Sep 17 00:00:00 2001 From: Erin Date: Wed, 24 Jun 2020 14:16:48 -0400 Subject: [PATCH] :sparkles: Spotify Node Co-authored-by: Ricardo Espinoza --- .../SpotifyOAuth2Api.credentials.ts | 53 ++ .../nodes/Spotify/GenericFunctions.ts | 84 ++ .../nodes-base/nodes/Spotify/Spotify.node.ts | 816 ++++++++++++++++++ packages/nodes-base/nodes/Spotify/spotify.png | Bin 0 -> 6664 bytes packages/nodes-base/package.json | 2 + 5 files changed, 955 insertions(+) create mode 100644 packages/nodes-base/credentials/SpotifyOAuth2Api.credentials.ts create mode 100644 packages/nodes-base/nodes/Spotify/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Spotify/Spotify.node.ts create mode 100644 packages/nodes-base/nodes/Spotify/spotify.png diff --git a/packages/nodes-base/credentials/SpotifyOAuth2Api.credentials.ts b/packages/nodes-base/credentials/SpotifyOAuth2Api.credentials.ts new file mode 100644 index 0000000000..1dc9f1057a --- /dev/null +++ b/packages/nodes-base/credentials/SpotifyOAuth2Api.credentials.ts @@ -0,0 +1,53 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class SpotifyOAuth2Api implements ICredentialType { + name = 'spotifyOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Spotify OAuth2 API'; + properties = [ + { + displayName: 'Spotify Server', + name: 'server', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.spotify.com/', + }, + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://accounts.spotify.com/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://accounts.spotify.com/api/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: 'user-read-playback-state playlist-read-collaborative user-modify-playback-state playlist-modify-public user-read-currently-playing playlist-read-private user-read-recently-played playlist-modify-private', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + } + ]; +} diff --git a/packages/nodes-base/nodes/Spotify/GenericFunctions.ts b/packages/nodes-base/nodes/Spotify/GenericFunctions.ts new file mode 100644 index 0000000000..f4c86a9d2d --- /dev/null +++ b/packages/nodes-base/nodes/Spotify/GenericFunctions.ts @@ -0,0 +1,84 @@ +import { OptionsWithUri } from 'request'; + +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +/** + * Make an API request to Spotify + * + * @param {IHookFunctions} this + * @param {string} method + * @param {string} url + * @param {object} body + * @returns {Promise} + */ +export async function spotifyApiRequest(this: IHookFunctions | IExecuteFunctions, + method: string, endpoint: string, body: object, query?: object, uri?: string): Promise { // tslint:disable-line:no-any + + const options: OptionsWithUri = { + method, + headers: { + 'User-Agent': 'n8n', + 'Content-Type': 'text/plain', + 'Accept': ' application/json', + }, + body, + qs: query, + uri: uri || `https://api.spotify.com/v1${endpoint}`, + json: true + }; + + try { + const credentials = this.getCredentials('spotifyOAuth2Api'); + + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + if (Object.keys(body).length === 0) { + delete options.body; + } + + return await this.helpers.requestOAuth2.call(this, 'spotifyOAuth2Api', options); + } catch (error) { + if (error.statusCode === 401) { + // Return a clear error + throw new Error('The Spotify credentials are not valid!'); + } + + if (error.error && error.error.error && error.error.error.message) { + // Try to return the error prettier + throw new Error(`Spotify error response [${error.error.error.status}]: ${error.error.error.message}`); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} + +export async function spotifyApiRequestAllItems(this: IHookFunctions | IExecuteFunctions, + propertyName: string, method: string, endpoint: string, body: object, query?: object): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + let uri: string | undefined; + + do { + responseData = await spotifyApiRequest.call(this, method, endpoint, body, query, uri); + returnData.push.apply(returnData, responseData[propertyName]); + uri = responseData.next; + + } while ( + responseData['next'] !== null + ); + + return returnData; +} diff --git a/packages/nodes-base/nodes/Spotify/Spotify.node.ts b/packages/nodes-base/nodes/Spotify/Spotify.node.ts new file mode 100644 index 0000000000..b0c2df8119 --- /dev/null +++ b/packages/nodes-base/nodes/Spotify/Spotify.node.ts @@ -0,0 +1,816 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + spotifyApiRequest, + spotifyApiRequestAllItems, +} from './GenericFunctions'; + +export class Spotify implements INodeType { + description: INodeTypeDescription = { + displayName: 'Spotify', + name: 'spotify', + icon: 'file:spotify.png', + group: ['input'], + version: 1, + description: 'Access public song data via the Spotify API.', + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + defaults: { + name: 'Spotify', + color: '#1DB954', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'spotifyOAuth2Api', + required: true, + }, + ], + properties: [ + // ---------------------------------------------------------- + // Resource to Operate on + // Player, Album, Artisits, Playlists, Tracks + // ---------------------------------------------------------- + { + displayName: 'Resource', + name: 'resource', + type: 'options', + options: [ + { + name: 'Album', + value: 'album', + }, + { + name: 'Artist', + value: 'artist', + }, + { + name: 'Player', + value: 'player', + }, + { + name: 'Playlist', + value: 'playlist', + }, + { + name: 'Track', + value: 'track', + }, + ], + default: 'player', + description: 'The resource to operate on.', + }, + // -------------------------------------------------------------------------------------------------------- + // Player Operations + // Pause, Play, Get Recently Played, Get Currently Playing, Next Song, Previous Song, Add to Queue + // -------------------------------------------------------------------------------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'player', + ], + }, + }, + options: [ + { + name: 'Add Song to Queue', + value: 'addSongToQueue', + description: 'Add a song to your queue.' + }, + { + name: 'Currently Playing', + value: 'currentlyPlaying', + description: 'Get your currently playing track.' + }, + { + name: 'Next Song', + value: 'nextSong', + description: 'Skip to your next track.' + }, + { + name: 'Pause', + value: 'pause', + description: 'Pause your music.', + }, + { + name: 'Previous Song', + value: 'previousSong', + description: 'Skip to your previous song.' + }, + { + name: 'Recently Played', + value: 'recentlyPlayed', + description: 'Get your recently played tracks.' + }, + { + name: 'Start Music', + value: 'startMusic', + description: 'Start playing a playlist, artist, or album.' + }, + ], + default: 'addSongToQueue', + description: 'The operation to perform.', + }, + { + displayName: 'Resource ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'player' + ], + operation: [ + 'startMusic', + ], + }, + }, + placeholder: 'spotify:album:1YZ3k65Mqw3G8FzYlW1mmp', + description: `Enter a playlist, artist, or album URI or ID.`, + }, + { + displayName: 'Track ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'player' + ], + operation: [ + 'addSongToQueue', + ], + }, + }, + placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU', + description: `Enter a track URI or ID.`, + }, + // ----------------------------------------------- + // Album Operations + // Get an Album, Get an Album's Tracks + // ----------------------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'album', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get an album by URI or ID.', + }, + { + name: `Get Tracks`, + value: 'getTracks', + description: `Get an album's tracks by URI or ID.`, + }, + ], + default: 'get', + description: 'The operation to perform.', + }, + { + displayName: 'Album ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'album', + ], + }, + }, + placeholder: 'spotify:album:1YZ3k65Mqw3G8FzYlW1mmp', + description: `The album's Spotify URI or ID.`, + }, + // ------------------------------------------------------------------------------------------------------------- + // Artist Operations + // Get an Artist, Get an Artist's Related Artists, Get an Artist's Top Tracks, Get an Artist's Albums + // ------------------------------------------------------------------------------------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'artist', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get an artist by URI or ID.', + }, + { + name: `Get Albums`, + value: 'getAlbums', + description: `Get an artist's albums by URI or ID.`, + }, + { + name: `Get Related Artists`, + value: 'getRelatedArtists', + description: `Get an artist's related artists by URI or ID.`, + }, + { + name: `Get Top Tracks`, + value: 'getTopTracks', + description: `Get an artist's top tracks by URI or ID.`, + }, + ], + default: 'get', + description: 'The operation to perform.', + }, + { + displayName: 'Artist ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'artist', + ], + }, + }, + placeholder: 'spotify:artist:4LLpKhyESsyAXpc4laK94U', + description: `The artist's Spotify URI or ID.`, + }, + { + displayName: 'Country', + name: 'country', + type: 'string', + default: 'US', + required: true, + displayOptions: { + show: { + resource: [ + 'artist' + ], + operation: [ + 'getTopTracks', + ], + }, + }, + placeholder: 'US', + description: `Top tracks in which country? Enter the postal abbriviation.`, + }, + // ------------------------------------------------------------------------------------------------------------- + // Playlist Operations + // Get a Playlist, Get a Playlist's Tracks, Add/Remove a Song from a Playlist, Get a User's Playlists + // ------------------------------------------------------------------------------------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'playlist', + ], + }, + }, + options: [ + { + name: 'Add an Item', + value: 'add', + description: 'Add tracks from a playlist by track and playlist URI or ID.', + }, + { + name: 'Get', + value: 'get', + description: 'Get a playlist by URI or ID.', + }, + { + name: 'Get Tracks', + value: 'getTracks', + description: `Get a playlist's tracks by URI or ID.`, + }, + { + name: `Get the User's Playlists`, + value: 'getUserPlaylists', + description: `Get a user's playlists.`, + }, + { + name: 'Remove an Item', + value: 'delete', + description: 'Remove tracks from a playlist by track and playlist URI or ID.', + }, + ], + default: 'add', + description: 'The operation to perform.', + }, + { + displayName: 'Playlist ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'playlist' + ], + operation: [ + 'add', + 'delete', + 'get', + 'getTracks' + ], + }, + }, + placeholder: 'spotify:playlist:37i9dQZF1DWUhI3iC1khPH', + description: `The playlist's Spotify URI or its ID.`, + }, + { + displayName: 'Track ID', + name: 'trackID', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'playlist' + ], + operation: [ + 'add', + 'delete' + ], + }, + }, + placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU', + description: `The track's Spotify URI or its ID. The track to add/delete from the playlist.`, + }, + // ----------------------------------------------------- + // Track Operations + // Get a Track, Get a Track's Audio Features + // ----------------------------------------------------- + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'track', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a track by its URI or ID.', + }, + { + name: 'Get Audio Features', + value: 'getAudioFeatures', + description: 'Get audio features for a track by URI or ID.', + }, + ], + default: 'track', + description: 'The operation to perform.', + }, + { + displayName: 'Track ID', + name: 'id', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'track', + ], + }, + }, + placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU', + description: `The track's Spotify URI or ID.`, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + default: false, + required: true, + displayOptions: { + show: { + resource: [ + 'album', + 'artist', + 'playlist' + ], + operation: [ + 'getTracks', + 'getAlbums', + 'getUserPlaylists' + ] + }, + }, + description: `The number of items to return.`, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + required: true, + displayOptions: { + show: { + resource: [ + 'album', + 'artist', + 'playlist' + ], + operation: [ + 'getTracks', + 'getAlbums', + 'getUserPlaylists' + ], + returnAll: [ + false + ] + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + description: `The number of items to return.`, + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + default: 50, + required: true, + displayOptions: { + show: { + resource: [ + 'player', + ], + operation: [ + 'recentlyPlayed', + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 50, + }, + description: `The number of items to return.`, + }, + ] + }; + + + async execute(this: IExecuteFunctions): Promise { + // Get all of the incoming input data to loop through + const items = this.getInputData(); + const returnData: IDataObject[] = []; + + // For Post + let body: IDataObject; + // For Query string + let qs: IDataObject; + + let requestMethod: string; + let endpoint: string; + let returnAll: boolean; + let propertyName = ''; + let responseData; + + const operation = this.getNodeParameter('operation', 0) as string; + const resource = this.getNodeParameter('resource', 0) as string; + + // Set initial values + requestMethod = 'GET'; + endpoint = ''; + body = {}; + qs = {}; + returnAll = false; + + for(let i = 0; i < items.length; i++) { + // ----------------------------- + // Player Operations + // ----------------------------- + if( resource === 'player' ) { + if(operation === 'pause') { + requestMethod = 'PUT'; + + endpoint = `/me/player/pause`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + + } else if(operation === 'recentlyPlayed') { + requestMethod = 'GET'; + + endpoint = `/me/player/recently-played`; + + const limit = this.getNodeParameter('limit', i) as number; + + qs = { + 'limit': limit + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.items; + + } else if(operation === 'currentlyPlaying') { + requestMethod = 'GET'; + + endpoint = `/me/player/currently-playing`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + } else if(operation === 'nextSong') { + requestMethod = 'POST'; + + endpoint = `/me/player/next`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + + } else if(operation === 'previousSong') { + requestMethod = 'POST'; + + endpoint = `/me/player/previous`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + + } else if(operation === 'startMusic') { + requestMethod = 'PUT'; + + endpoint = `/me/player/play`; + + const id = this.getNodeParameter('id', i) as string; + + body.context_uri = id; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + + } else if(operation === 'addSongToQueue') { + requestMethod = 'POST'; + + endpoint = `/me/player/queue`; + + const id = this.getNodeParameter('id', i) as string; + + qs = { + 'uri': id + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + } + // ----------------------------- + // Album Operations + // ----------------------------- + } else if( resource === 'album') { + const uri = this.getNodeParameter('id', i) as string; + + const id = uri.replace('spotify:album:', ''); + + requestMethod = 'GET'; + + if(operation === 'get') { + endpoint = `/albums/${id}`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + } else if(operation === 'getTracks') { + endpoint = `/albums/${id}/tracks`; + + propertyName = 'tracks'; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + + propertyName = 'items'; + + if(!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + + qs = { + 'limit': limit + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.items; + } + } + // ----------------------------- + // Artist Operations + // ----------------------------- + } else if( resource === 'artist') { + const uri = this.getNodeParameter('id', i) as string; + + const id = uri.replace('spotify:artist:', ''); + + if(operation === 'getAlbums') { + + endpoint = `/artists/${id}/albums`; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + + propertyName = 'items'; + + if(!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + + qs = { + 'limit': limit + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.items; + } + } else if(operation === 'getRelatedArtists') { + + endpoint = `/artists/${id}/related-artists`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.artists; + + } else if(operation === 'getTopTracks'){ + const country = this.getNodeParameter('country', i) as string; + + qs = { + 'country': country + }; + + endpoint = `/artists/${id}/top-tracks`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.tracks; + + } else if (operation === 'get') { + + requestMethod = 'GET'; + + endpoint = `/artists/${id}`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + } + // ----------------------------- + // Playlist Operations + // ----------------------------- + } else if( resource === 'playlist') { + if(['delete', 'get', 'getTracks', 'add'].includes(operation)) { + const uri = this.getNodeParameter('id', i) as string; + + const id = uri.replace('spotify:playlist:', ''); + + if(operation === 'delete') { + requestMethod = 'DELETE'; + const trackId = this.getNodeParameter('trackID', i) as string; + + body.tracks = [ + { + "uri": `${trackId}`, + "positions": [ 0 ] + } + ]; + + endpoint = `/playlists/${id}/tracks`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + + } else if(operation === 'get') { + requestMethod = 'GET'; + + endpoint = `/playlists/${id}`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + } else if(operation === 'getTracks') { + requestMethod = 'GET'; + + endpoint = `/playlists/${id}/tracks`; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + + propertyName = 'items'; + + if(!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + + qs = { + 'limit': limit + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.items; + } + } else if(operation === 'add') { + requestMethod = 'POST'; + + const trackId = this.getNodeParameter('trackID', i) as string; + + qs = { + 'uris': trackId + }; + + endpoint = `/playlists/${id}/tracks`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + } + } else if(operation === 'getUserPlaylists') { + requestMethod = 'GET'; + + endpoint = `/me/playlists`; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + + propertyName = 'items'; + + if(!returnAll) { + const limit = this.getNodeParameter('limit', i) as number; + + qs = { + 'limit': limit + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = responseData.items; + } + } + // ----------------------------- + // Track Operations + // ----------------------------- + } else if( resource === 'track') { + const uri = this.getNodeParameter('id', i) as string; + + const id = uri.replace('spotify:track:', ''); + + requestMethod = 'GET'; + + if(operation === 'getAudioFeatures') { + endpoint = `/audio-features/${id}`; + } else if(operation === 'get') { + endpoint = `/tracks/${id}`; + } + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + } + + if(returnAll) { + responseData = await spotifyApiRequestAllItems.call(this, propertyName, requestMethod, endpoint, body, qs); + } + + if (Array.isArray(responseData)) { + returnData.push.apply(returnData, responseData as IDataObject[]); + } else { + returnData.push(responseData as IDataObject); + } + } + + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Spotify/spotify.png b/packages/nodes-base/nodes/Spotify/spotify.png new file mode 100644 index 0000000000000000000000000000000000000000..2feeb78bbf25c43b2e1ceab5ee5f382eb790b207 GIT binary patch literal 6664 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 yuDUBI#tL~Id;c)#1LY@6KIaUBA^!o!Zi|xu literal 0 HcmV?d00001 diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index a09d97d032..7782a493b1 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -119,6 +119,7 @@ "dist/credentials/StripeApi.credentials.js", "dist/credentials/SalesmateApi.credentials.js", "dist/credentials/SegmentApi.credentials.js", + "dist/credentials/SpotifyOAuth2Api.credentials.js", "dist/credentials/SurveyMonkeyApi.credentials.js", "dist/credentials/SurveyMonkeyOAuth2Api.credentials.js", "dist/credentials/TelegramApi.credentials.js", @@ -263,6 +264,7 @@ "dist/nodes/Slack/Slack.node.js", "dist/nodes/Sms77/Sms77.node.js", "dist/nodes/SplitInBatches.node.js", + "dist/nodes/Spotify/Spotify.node.js", "dist/nodes/SpreadsheetFile.node.js", "dist/nodes/SseTrigger.node.js", "dist/nodes/Start.node.js",