Spotify improvements (#1884)

* Add search resource

* Add resume, volume functions to player resource

*  Improvements to #1870

*  Improvements

*  Minor improvements

Co-authored-by: smamudhan <sm.amudhan@live.com>
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Ricardo Espinoza
2021-06-18 17:41:57 -04:00
committed by GitHub
parent a49624a17c
commit 8c693ba6e3
2 changed files with 310 additions and 11 deletions

View File

@@ -13,6 +13,10 @@ import {
NodeOperationError, NodeOperationError,
} from 'n8n-workflow'; } from 'n8n-workflow';
import {
get,
} from 'lodash';
/** /**
* Make an API request to Spotify * Make an API request to Spotify
* *
@@ -40,7 +44,6 @@ export async function spotifyApiRequest(this: IHookFunctions | IExecuteFunctions
if (Object.keys(body).length > 0) { if (Object.keys(body).length > 0) {
options.body = body; options.body = body;
} }
try { try {
return await this.helpers.requestOAuth2.call(this, 'spotifyOAuth2Api', options); return await this.helpers.requestOAuth2.call(this, 'spotifyOAuth2Api', options);
} catch (error) { } catch (error) {
@@ -59,11 +62,16 @@ export async function spotifyApiRequestAllItems(this: IHookFunctions | IExecuteF
do { do {
responseData = await spotifyApiRequest.call(this, method, endpoint, body, query, uri); responseData = await spotifyApiRequest.call(this, method, endpoint, body, query, uri);
returnData.push.apply(returnData, responseData[propertyName]); returnData.push.apply(returnData, get(responseData, propertyName));
uri = responseData.next; 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 ( } while (
responseData['next'] !== null (responseData['next'] !== null && responseData['next'] !== undefined) ||
responseData[propertyName.split('.')[0]].next !== null
); );
return returnData; return returnData;

View File

@@ -84,7 +84,8 @@ export class Spotify implements INodeType {
// -------------------------------------------------------------------------------------------------------- // --------------------------------------------------------------------------------------------------------
// Player Operations // 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', displayName: 'Operation',
@@ -128,6 +129,16 @@ export class Spotify implements INodeType {
value: 'recentlyPlayed', value: 'recentlyPlayed',
description: 'Get your recently played tracks.', 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', name: 'Start Music',
value: 'startMusic', value: 'startMusic',
@@ -207,6 +218,11 @@ export class Spotify implements INodeType {
value: 'getTracks', value: 'getTracks',
description: `Get an album's tracks by URI or ID.`, description: `Get an album's tracks by URI or ID.`,
}, },
{
name: `Search`,
value: 'search',
description: `Search albums by keyword.`,
},
], ],
default: 'get', default: 'get',
description: 'The operation to perform.', description: 'The operation to perform.',
@@ -227,10 +243,33 @@ export class Spotify implements INodeType {
'getTracks', 'getTracks',
], ],
}, },
hide: {
operation: [
'search',
],
},
}, },
placeholder: 'spotify:album:1YZ3k65Mqw3G8FzYlW1mmp', placeholder: 'spotify:album:1YZ3k65Mqw3G8FzYlW1mmp',
description: `The album's Spotify URI or ID.`, 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 // Artist Operations
@@ -268,6 +307,11 @@ export class Spotify implements INodeType {
value: 'getTopTracks', value: 'getTopTracks',
description: `Get an artist's top tracks by URI or ID.`, description: `Get an artist's top tracks by URI or ID.`,
}, },
{
name: `Search`,
value: 'search',
description: `Search artists by keyword.`,
},
], ],
default: 'get', default: 'get',
description: 'The operation to perform.', description: 'The operation to perform.',
@@ -284,6 +328,11 @@ export class Spotify implements INodeType {
'artist', 'artist',
], ],
}, },
hide: {
operation: [
'search',
],
},
}, },
placeholder: 'spotify:artist:4LLpKhyESsyAXpc4laK94U', placeholder: 'spotify:artist:4LLpKhyESsyAXpc4laK94U',
description: `The artist's Spotify URI or ID.`, 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.`, 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 // Playlist Operations
// Get a Playlist, Get a Playlist's Tracks, Add/Remove a Song from a Playlist, Get a User's Playlists // 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', value: 'delete',
description: 'Remove tracks from a playlist by track and playlist URI or ID.', description: 'Remove tracks from a playlist by track and playlist URI or ID.',
}, },
{
name: `Search`,
value: 'search',
description: `Search playlists by keyword.`,
},
], ],
default: 'add', default: 'add',
description: 'The operation to perform.', 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 // Track Operations
@@ -510,6 +601,11 @@ export class Spotify implements INodeType {
value: 'getAudioFeatures', value: 'getAudioFeatures',
description: 'Get audio features for a track by URI or ID.', description: 'Get audio features for a track by URI or ID.',
}, },
{
name: 'Search',
value: 'search',
description: `Search tracks by keyword.`,
},
], ],
default: 'track', default: 'track',
description: 'The operation to perform.', description: 'The operation to perform.',
@@ -526,10 +622,33 @@ export class Spotify implements INodeType {
'track', 'track',
], ],
}, },
hide: {
operation: [
'search',
],
},
}, },
placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU', placeholder: 'spotify:track:0xE4LEFzSNGsz1F6kvXsHU',
description: `The track's Spotify URI or ID.`, 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 // Library Operations
@@ -595,6 +714,7 @@ export class Spotify implements INodeType {
'library', 'library',
'myData', 'myData',
'playlist', 'playlist',
'track',
], ],
operation: [ operation: [
'getTracks', 'getTracks',
@@ -603,6 +723,7 @@ export class Spotify implements INodeType {
'getNewReleases', 'getNewReleases',
'getLikedTracks', 'getLikedTracks',
'getFollowingArtists', 'getFollowingArtists',
'search',
], ],
}, },
}, },
@@ -621,6 +742,7 @@ export class Spotify implements INodeType {
'artist', 'artist',
'library', 'library',
'playlist', 'playlist',
'track',
], ],
operation: [ operation: [
'getTracks', 'getTracks',
@@ -628,6 +750,7 @@ export class Spotify implements INodeType {
'getUserPlaylists', 'getUserPlaylists',
'getNewReleases', 'getNewReleases',
'getLikedTracks', 'getLikedTracks',
'search',
], ],
returnAll: [ returnAll: [
false, false,
@@ -664,6 +787,28 @@ export class Spotify implements INodeType {
}, },
description: `The number of items to return.`, 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', displayName: 'Filters',
name: '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<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
// Get all of the incoming input data to loop through // Get all of the incoming input data to loop through
const items = this.getInputData(); const items = this.getInputData();
@@ -803,6 +977,28 @@ export class Spotify implements INodeType {
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); 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 }; responseData = { success: true };
} }
@@ -868,6 +1064,29 @@ export class Spotify implements INodeType {
responseData = responseData.items; 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') { } else if (resource === 'artist') {
@@ -876,7 +1095,7 @@ export class Spotify implements INodeType {
// Artist Operations // Artist Operations
// ----------------------------- // -----------------------------
const uri = this.getNodeParameter('id', i) as string; const uri = this.getNodeParameter('id', i, '') as string;
const id = uri.replace('spotify:artist:', ''); const id = uri.replace('spotify:artist:', '');
@@ -928,6 +1147,30 @@ export class Spotify implements INodeType {
endpoint = `/artists/${id}`; endpoint = `/artists/${id}`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); 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') { } else if (resource === 'playlist') {
@@ -1036,6 +1279,30 @@ export class Spotify implements INodeType {
} }
responseData = await spotifyApiRequest.call(this, 'POST', '/me/playlists', body, qs); 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') { } else if (resource === 'track') {
@@ -1044,7 +1311,7 @@ export class Spotify implements INodeType {
// Track Operations // Track Operations
// ----------------------------- // -----------------------------
const uri = this.getNodeParameter('id', i) as string; const uri = this.getNodeParameter('id', i, '') as string;
const id = uri.replace('spotify:track:', ''); const id = uri.replace('spotify:track:', '');
@@ -1052,11 +1319,35 @@ export class Spotify implements INodeType {
if (operation === 'getAudioFeatures') { if (operation === 'getAudioFeatures') {
endpoint = `/audio-features/${id}`; endpoint = `/audio-features/${id}`;
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
} else if (operation === 'get') { } else if (operation === 'get') {
endpoint = `/tracks/${id}`; endpoint = `/tracks/${id}`;
}
responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs); responseData = await spotifyApiRequest.call(this, requestMethod, endpoint, body, qs);
} else if (operation === 'search') {
requestMethod = 'GET';
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') { } else if (resource === 'library') {