From 8c693ba6e3eec0d79db9702c53118bfefe4a98d0 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 18 Jun 2021 17:41:57 -0400 Subject: [PATCH] :sparkles: Spotify improvements (#1884) * Add search resource * Add resume, volume functions to player resource * :zap: Improvements to #1870 * :zap: Improvements * :zap: Minor improvements Co-authored-by: smamudhan Co-authored-by: Jan Oberhauser --- .../nodes/Spotify/GenericFunctions.ts | 18 +- .../nodes-base/nodes/Spotify/Spotify.node.ts | 303 +++++++++++++++++- 2 files changed, 310 insertions(+), 11 deletions(-) diff --git a/packages/nodes-base/nodes/Spotify/GenericFunctions.ts b/packages/nodes-base/nodes/Spotify/GenericFunctions.ts index bce394c38a..e07a6d71df 100644 --- a/packages/nodes-base/nodes/Spotify/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Spotify/GenericFunctions.ts @@ -13,6 +13,10 @@ import { NodeOperationError, } from 'n8n-workflow'; +import { + get, +} from 'lodash'; + /** * Make an API request to Spotify * @@ -40,7 +44,6 @@ export async function spotifyApiRequest(this: IHookFunctions | IExecuteFunctions if (Object.keys(body).length > 0) { options.body = body; } - try { return await this.helpers.requestOAuth2.call(this, 'spotifyOAuth2Api', options); } catch (error) { @@ -59,11 +62,16 @@ export async function spotifyApiRequestAllItems(this: IHookFunctions | IExecuteF do { responseData = await spotifyApiRequest.call(this, method, endpoint, body, query, uri); - returnData.push.apply(returnData, responseData[propertyName]); - uri = responseData.next; - + returnData.push.apply(returnData, get(responseData, propertyName)); + uri = responseData.next || responseData[propertyName.split('.')[0]].next; + //remove the query as the query parameters are already included in the next, else api throws error. + query = {}; + if (uri?.includes('offset=1000')) { + return returnData; + } } while ( - responseData['next'] !== null + (responseData['next'] !== null && responseData['next'] !== undefined) || + responseData[propertyName.split('.')[0]].next !== null ); return returnData; diff --git a/packages/nodes-base/nodes/Spotify/Spotify.node.ts b/packages/nodes-base/nodes/Spotify/Spotify.node.ts index 972c321b49..1b3e4b48dd 100644 --- a/packages/nodes-base/nodes/Spotify/Spotify.node.ts +++ b/packages/nodes-base/nodes/Spotify/Spotify.node.ts @@ -84,7 +84,8 @@ export class Spotify implements INodeType { // -------------------------------------------------------------------------------------------------------- // Player Operations - // Pause, Play, Get Recently Played, Get Currently Playing, Next Song, Previous Song, Add to Queue + // Pause, Play, Resume, Get Recently Played, Get Currently Playing, Next Song, Previous Song, + // Add to Queue, Set Volume // -------------------------------------------------------------------------------------------------------- { displayName: 'Operation', @@ -128,6 +129,16 @@ export class Spotify implements INodeType { value: 'recentlyPlayed', description: 'Get your recently played tracks.', }, + { + name: 'Resume', + value: 'resume', + description: 'Resume playback on the current active device.', + }, + { + name: 'Set Volume', + value: 'volume', + description: 'Set volume on the current active device.', + }, { name: 'Start Music', value: 'startMusic', @@ -207,6 +218,11 @@ export class Spotify implements INodeType { value: 'getTracks', description: `Get an album's tracks by URI or ID.`, }, + { + name: `Search`, + value: 'search', + description: `Search albums by keyword.`, + }, ], default: 'get', description: 'The operation to perform.', @@ -227,10 +243,33 @@ export class Spotify implements INodeType { 'getTracks', ], }, + hide: { + operation: [ + 'search', + ], + }, }, placeholder: 'spotify:album:1YZ3k65Mqw3G8FzYlW1mmp', description: `The album's Spotify URI or ID.`, }, + { + displayName: 'Search Keyword', + name: 'query', + type: 'string', + required: true, + default: '', + description: 'The keyword term to search for.', + displayOptions: { + show: { + resource: [ + 'album', + ], + operation: [ + 'search', + ], + }, + }, + }, // ------------------------------------------------------------------------------------------------------------- // Artist Operations @@ -268,6 +307,11 @@ export class Spotify implements INodeType { value: 'getTopTracks', description: `Get an artist's top tracks by URI or ID.`, }, + { + name: `Search`, + value: 'search', + description: `Search artists by keyword.`, + }, ], default: 'get', description: 'The operation to perform.', @@ -284,6 +328,11 @@ export class Spotify implements INodeType { 'artist', ], }, + hide: { + operation: [ + 'search', + ], + }, }, placeholder: 'spotify:artist:4LLpKhyESsyAXpc4laK94U', description: `The artist's Spotify URI or ID.`, @@ -308,6 +357,25 @@ export class Spotify implements INodeType { description: `Top tracks in which country? Enter the postal abbriviation.`, }, + { + displayName: 'Search Keyword', + name: 'query', + type: 'string', + required: true, + default: '', + description: 'The keyword term to search for.', + displayOptions: { + show: { + resource: [ + 'artist', + ], + operation: [ + 'search', + ], + }, + }, + }, + // ------------------------------------------------------------------------------------------------------------- // Playlist Operations // Get a Playlist, Get a Playlist's Tracks, Add/Remove a Song from a Playlist, Get a User's Playlists @@ -354,6 +422,11 @@ export class Spotify implements INodeType { value: 'delete', description: 'Remove tracks from a playlist by track and playlist URI or ID.', }, + { + name: `Search`, + value: 'search', + description: `Search playlists by keyword.`, + }, ], default: 'add', description: 'The operation to perform.', @@ -483,6 +556,24 @@ export class Spotify implements INodeType { }, ], }, + { + displayName: 'Search Keyword', + name: 'query', + type: 'string', + required: true, + default: '', + description: 'The keyword term to search for.', + displayOptions: { + show: { + resource: [ + 'playlist', + ], + operation: [ + 'search', + ], + }, + }, + }, // ----------------------------------------------------- // Track Operations @@ -510,6 +601,11 @@ export class Spotify implements INodeType { value: 'getAudioFeatures', description: 'Get audio features for a track by URI or ID.', }, + { + name: 'Search', + value: 'search', + description: `Search tracks by keyword.`, + }, ], default: 'track', description: 'The operation to perform.', @@ -526,10 +622,33 @@ export class Spotify implements INodeType { 'track', ], }, + hide: { + operation: [ + 'search', + ], + }, }, placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU', description: `The track's Spotify URI or ID.`, }, + { + displayName: 'Search Keyword', + name: 'query', + type: 'string', + required: true, + default: '', + description: 'The keyword term to search for.', + displayOptions: { + show: { + resource: [ + 'track', + ], + operation: [ + 'search', + ], + }, + }, + }, // ----------------------------------------------------- // Library Operations @@ -595,6 +714,7 @@ export class Spotify implements INodeType { 'library', 'myData', 'playlist', + 'track', ], operation: [ 'getTracks', @@ -603,6 +723,7 @@ export class Spotify implements INodeType { 'getNewReleases', 'getLikedTracks', 'getFollowingArtists', + 'search', ], }, }, @@ -621,6 +742,7 @@ export class Spotify implements INodeType { 'artist', 'library', 'playlist', + 'track', ], operation: [ 'getTracks', @@ -628,6 +750,7 @@ export class Spotify implements INodeType { 'getUserPlaylists', 'getNewReleases', 'getLikedTracks', + 'search', ], returnAll: [ false, @@ -664,6 +787,28 @@ export class Spotify implements INodeType { }, description: `The number of items to return.`, }, + { + displayName: 'Volume', + name: 'volumePercent', + type: 'number', + default: 50, + required: true, + displayOptions: { + show: { + resource: [ + 'player', + ], + operation: [ + 'volume', + ], + }, + }, + typeOptions: { + minValue: 0, + maxValue: 100, + }, + description: `The volume percentage to set.`, + }, { displayName: 'Filters', name: 'filters', @@ -691,10 +836,39 @@ export class Spotify implements INodeType { }, ], }, + { + displayName: 'Filters', + name: 'filters', + type: 'collection', + placeholder: 'Add Filter', + default: {}, + displayOptions: { + show: { + resource: [ + 'playlist', + 'artist', + 'track', + 'album', + ], + operation: [ + 'search', + ], + }, + }, + options: [ + { + displayName: 'Country', + name: 'market', + type: 'options', + options: isoCountryCodes.map(({ name, alpha2 }) => ({ name, value: alpha2 })), + default: '', + description: `If a country code is specified, only content that is playable in that market is returned.`, + }, + ], + }, ], }; - async execute(this: IExecuteFunctions): Promise { // Get all of the incoming input data to loop through const items = this.getInputData(); @@ -803,6 +977,28 @@ export class Spotify implements INodeType { responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + responseData = { success: true }; + } else if (operation === 'resume') { + requestMethod = 'PUT'; + + endpoint = `/me/player/play`; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + + responseData = { success: true }; + } else if (operation === 'volume') { + requestMethod = 'PUT'; + + endpoint = `/me/player/volume`; + + const volumePercent = this.getNodeParameter('volumePercent', i) as number; + + qs = { + volume_percent: volumePercent, + }; + + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + responseData = { success: true }; } @@ -868,6 +1064,29 @@ export class Spotify implements INodeType { responseData = responseData.items; } + } else if (operation === 'search') { + requestMethod = 'GET'; + + endpoint = '/search'; + + propertyName = 'albums.items'; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + const q = this.getNodeParameter('query', i) as string; + const filters = this.getNodeParameter('filters', i) as IDataObject; + + qs = { + q, + type: 'album', + ...filters, + }; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + qs.limit = limit; + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + responseData = responseData.albums.items; + } } } else if (resource === 'artist') { @@ -876,7 +1095,7 @@ export class Spotify implements INodeType { // Artist Operations // ----------------------------- - const uri = this.getNodeParameter('id', i) as string; + const uri = this.getNodeParameter('id', i, '') as string; const id = uri.replace('spotify:artist:', ''); @@ -928,6 +1147,30 @@ export class Spotify implements INodeType { endpoint = `/artists/${id}`; responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + } else if (operation === 'search') { + requestMethod = 'GET'; + + endpoint = '/search'; + + propertyName = 'artists.items'; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + const q = this.getNodeParameter('query', i) as string; + const filters = this.getNodeParameter('filters', i) as IDataObject; + + qs = { + q, + limit: 50, + type: 'artist', + ...filters, + }; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + qs.limit = limit; + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + responseData = responseData.artists.items; + } } } else if (resource === 'playlist') { @@ -1036,6 +1279,30 @@ export class Spotify implements INodeType { } responseData = await spotifyApiRequest.call(this, 'POST', '/me/playlists', body, qs); + } else if (operation === 'search') { + requestMethod = 'GET'; + + endpoint = '/search'; + + propertyName = 'playlists.items'; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + const q = this.getNodeParameter('query', i) as string; + const filters = this.getNodeParameter('filters', i) as IDataObject; + + qs = { + q, + type: 'playlist', + limit: 50, + ...filters, + }; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + qs.limit = limit; + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + responseData = responseData.playlists.items; + } } } else if (resource === 'track') { @@ -1044,7 +1311,7 @@ export class Spotify implements INodeType { // Track Operations // ----------------------------- - const uri = this.getNodeParameter('id', i) as string; + const uri = this.getNodeParameter('id', i, '') as string; const id = uri.replace('spotify:track:', ''); @@ -1052,11 +1319,35 @@ export class Spotify implements INodeType { if (operation === 'getAudioFeatures') { endpoint = `/audio-features/${id}`; + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); } else if (operation === 'get') { endpoint = `/tracks/${id}`; - } + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + } else if (operation === 'search') { + requestMethod = 'GET'; - responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + endpoint = '/search'; + + propertyName = 'tracks.items'; + + returnAll = this.getNodeParameter('returnAll', i) as boolean; + const q = this.getNodeParameter('query', i) as string; + const filters = this.getNodeParameter('filters', i) as IDataObject; + + qs = { + q, + type: 'track', + limit: 50, + ...filters, + }; + + if (returnAll === false) { + const limit = this.getNodeParameter('limit', i) as number; + qs.limit = limit; + responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); + responseData = responseData.tracks.items; + } + } } else if (resource === 'library') {