Ok, here are three quick and dirty nodes that will fetch and playback the first playlist.
First we have to get our auth and refresh tokens. This first flow handles that. It’s important to note that this flow expects global variables. You can set those variables with the code below. The node-red IP does not have to be externally routable. Mine is 192.168.3.5.
global.set("spotify_client_id", "<client id>");
global.set("spotify_client_secret", "<client secret>");
global.set("spotify_redirect_uri", "http://<node-red IP>:1880/spotify/auth/callback");
The next flow is the meat of the authentication. You will need to call this before every API call. Conversely you could do something with error handling and call as needed but it’s probably easiest to call it every time. Basically it checks for the auth token. If it’s already present it returns it, if it doesn’t exist and there’s no refresh token then it initiates a call to get auth and refresh tokens. If there’s a refresh token then it uses that to get the auth token. As I think about it, there’s a problem here in that it doesn’t know when the auth token expires. I used to store the tokens in cookies with expiry dates but that’s not really best practice. I should probably keep a context-less cookie with an expiry that I could check and then know my access code is no longer valid. Something for another day.
You should make this a subflow so it can be reused as needed.
[{"id":"bd53672a.f0dfc8","type":"subflow","name":"Spotify - Authorization Code","info":"","in":[{"x":60,"y":220,"wires":[{"id":"94e54c31.8dbf2"}]}],"out":[{"x":1116,"y":220,"wires":[{"id":"94e54c31.8dbf2","port":1}]}]},{"id":"94e54c31.8dbf2","type":"function","z":"bd53672a.f0dfc8","name":"isAuthenticated?","func":"node.log(\"beginning spotify authentication\");\n\nnode.log(\"getting access_token\");\nvar access_token = global.get(\"spotify_access_token\")\nnode.log(\"received access token\");\nnode.log(\"spotify_access_token: \" + access_token);\nvar refresh_token = global.get(\"spotify_refresh_token\");\nnode.log(\"is this a refresh attempt: \" + msg.refresh_attempt !== undefined ? 'yes' : 'no');\n\nif (flow.get('refresh') && access_token === undefined) {\n throw 'refresh attempt without access token';\n}\n\nif (msg.refresh_attempt) {\n node.log(\"payload token value: \" + msg.payload.access_token);\n access_token = msg.payload.access_token;\n}\n\nif (access_token !== undefined) {\n node.log(\"Expected flow\");\n // This is the normal flow\n // msg.url need to be constructed in the \n // previous calling node.\n msg.headers = { 'Authorization': 'Bearer ' + access_token };\n msg.json = true;\n return [null, msg, null];\n} else if (refresh_token !== undefined) {\n node.log(\"Refresh flow\");\n flow.set('refresh', true);\n // Token has expired. Use refresh token\n // to generate a new access token.\n msg.req.query.refresh_token = refresh_token;\n msg.refresh_attempt = true;\n return [null, null, msg];\n} else {\n node.log(\"No Tokens flow\");\n // No refresh or access token. Need to login.\n msg.url = '/spotify/login';\n return [msg, null, null];\n}\n","outputs":3,"noerr":0,"x":210,"y":220,"wires":[["1ba8d1cd.36e23e","1cd125d2.a770ba"],[],["529bda7c.88cc54","7d8c818a.aadcb"]]},{"id":"2d2d6e4f.3e54c2","type":"comment","z":"bd53672a.f0dfc8","name":"Authorization Code Authentication","info":"This is the main authorization flow for logging in,\ngetting access tokens, and refreshing tokens. This \nflow allows for scopes to be set. Currently, all \nscopes are wired in.\n","x":160,"y":40,"wires":[]},{"id":"529bda7c.88cc54","type":"function","z":"bd53672a.f0dfc8","name":"Spotify Authorization","func":"var querystring = global.get('querystring');\n\nvar client_id = global.get(\"spotify_client_id\");\nvar client_secret = global.get(\"spotify_client_secret\");\n\nvar refresh_token = msg.req.query.refresh_token;\nglobal.set(\"spotify_refresh_token\", refresh_token);\nmsg.payload = {\n grant_type: 'refresh_token',\n refresh_token: refresh_token,\n refresh_attempt: true\n};\n\nmsg.headers = {\n 'Content-Type' : 'application/x-www-form-urlencoded', \n 'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64'))\n};\n \nmsg.json = true;\n\nreturn msg;","outputs":1,"noerr":0,"x":500,"y":320,"wires":[["cbbcd6d4.70f548"]]},{"id":"cbbcd6d4.70f548","type":"http request","z":"bd53672a.f0dfc8","name":"","method":"POST","ret":"obj","url":"https://accounts.spotify.com/api/token","tls":"","x":690,"y":320,"wires":[["f9c9d72d.fbf4d8"]]},{"id":"f9c9d72d.fbf4d8","type":"function","z":"bd53672a.f0dfc8","name":"Set access_token variables","func":"if ( msg.statusCode === 200) {\nnode.log(\"Status code is 200\");\n var access_token = msg.payload.access_token,\n scope = msg.payload.scope,\n expires_in = msg.payload.expires_in;\n \n global.set(\"spotify_access_token\", access_token);\n global.set(\"spotify_scope\", scope);\n \n return [msg, null];\n} else {\n node.log(\"Status code is \" + msg.statusCode);\n msg.url = \"error\";\n msg.payload = msg.statusCode;\n return [null, msg];\n}\n","outputs":2,"noerr":0,"x":900,"y":320,"wires":[["94e54c31.8dbf2"],["9e1c64ee.1146e8"]]},{"id":"9e1c64ee.1146e8","type":"http response","z":"bd53672a.f0dfc8","name":"Error","statusCode":"","headers":{},"x":1090,"y":380,"wires":[]},{"id":"b6661045.4cbf7","type":"function","z":"bd53672a.f0dfc8","name":"Spotify Authorization","func":"node.log(\"Spotify building request to callback\");\n\nvar querystring = global.get('querystring');\n\nvar client_id = global.get(\"spotify_client_id\");\nvar client_secret = global.get(\"spotify_client_secret\");\nvar redirect_uri = global.get(\"redirect_uri\");\n\n// your application requests authorization\nvar scope = 'playlist-modify-private playlist-read-private playlist-modify-public playlist-read-collaborative user-follow-read user-follow-modify user-read-private user-read-email user-read-birthdate user-top-read user-read-recently-played user-library-modify user-library-read user-read-playback-state user-read-currently-playing user-modify-playback-state streaming';\nconsole.log(scope);\nmsg.res.redirect('https://accounts.spotify.com/authorize?' +\n querystring.stringify({\n response_type: 'code',\n client_id: client_id,\n scope: scope,\n redirect_uri: redirect_uri,\n state: msg.state\n }));\n\nnode.log(\"Spotify redirecting to callback\");\n\n","outputs":1,"noerr":0,"x":740,"y":140,"wires":[["c13a40ee.f3ba3"]]},{"id":"1ba8d1cd.36e23e","type":"function","z":"bd53672a.f0dfc8","name":"Generate state value","func":"/**\n * Generates a random string containing numbers and letters\n * @param {number} length The length of the string\n * @return {string} The generated string\n */\nvar generateRandomString = function(length) {\n var text = '';\n var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n\n for (var i = 0; i < length; i++) {\n text += possible.charAt(Math.floor(Math.random() * possible.length));\n }\n return text;\n};\n\nvar state = generateRandomString(16);\nvar stateKey = 'spotify_auth_state';\nmsg.cookies = { };\nmsg.cookies[stateKey] = state;\n\nmsg.state = state;\nnode.log(\"spotify state: \" + state);\nreturn msg;","outputs":1,"noerr":0,"x":500,"y":140,"wires":[["b6661045.4cbf7","85528716.162578"]]},{"id":"68560cd9.08f144","type":"comment","z":"bd53672a.f0dfc8","name":"State","info":"Per the Spotify Documentation\nOptional, but strongly recommended. \nThe state can be useful for correlating requests \nand responses. Because your redirect_uri can be \nguessed, using a state value can increase your \nassurance that an incoming connection is the result \nof an authentication request. If you generate a \nrandom string, or encode the hash of some client \nstate, such as a cookie, in this state variable, \nyou can validate the response to additionally \nensure that both the request and response \noriginated in the same browser. This provides \nprotection against attacks such as cross-site \nrequest forgery. See RFC-6749.","x":450,"y":100,"wires":[]},{"id":"b2708426.f088b8","type":"comment","z":"bd53672a.f0dfc8","name":"Spotify Auth Redirect","info":"Need to pass:\n * client_id\n * client_secret\n * redirect_uri\n","x":740,"y":100,"wires":[]},{"id":"8c497eb1.bd6ed","type":"comment","z":"bd53672a.f0dfc8","name":"No Auth Token Flow","info":"If no access token or refresh token are present\nthen treat it as an initial login. This should\nhit the callback.","x":1250,"y":140,"wires":[]},{"id":"9dbbc7e1.088cd8","type":"comment","z":"bd53672a.f0dfc8","name":"Refresh Token Flow","info":"Refresh token is present, generate a new \naccess token.","x":1250,"y":320,"wires":[]},{"id":"f355a985.8df9d8","type":"comment","z":"bd53672a.f0dfc8","name":"Happy path","info":"Access token is present","x":1230,"y":220,"wires":[]},{"id":"7d8c818a.aadcb","type":"debug","z":"bd53672a.f0dfc8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":290,"y":340,"wires":[]},{"id":"c13a40ee.f3ba3","type":"debug","z":"bd53672a.f0dfc8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":930,"y":140,"wires":[]},{"id":"85528716.162578","type":"debug","z":"bd53672a.f0dfc8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":690,"y":180,"wires":[]},{"id":"1cd125d2.a770ba","type":"debug","z":"bd53672a.f0dfc8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":290,"y":140,"wires":[]},{"id":"6b7c0514.91f0ac","type":"catch","z":"bd53672a.f0dfc8","name":"","scope":null,"x":940,"y":380,"wires":[["9e1c64ee.1146e8"]]}]
The next flow is the callback. This listens on the node-red IP path from the global above. You can put this flow anywhere.
[{"id":"ebb9aeff.06ecf","type":"debug","z":"4f7b479d.f40f38","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":310,"y":140,"wires":[]},{"id":"2c974944.d97516","type":"function","z":"4f7b479d.f40f38","name":"Callback","func":"node.log(\"reached callback\");\nvar querystring = global.get('querystring');\n\nvar client_id = global.get(\"spotify_client_id\");\nvar client_secret = global.get(\"spotify_client_secret\");\nvar redirect_uri = global.get(\"redirect_uri\");\n\n// your application requests refresh and access tokens\n// after checking the state parameter\nvar stateKey = 'spotify_auth_state';\n\nvar code = msg.req.query.code || null;\nnode.log(\"Code: \" + code);\nvar state = msg.req.query.state || null;\nnode.log(\"State: \" + state);\nvar storedState = msg.req.cookies ? msg.req.cookies[stateKey] : null;\nnode.log(\"StoredState: \" + storedState);\n\n// I don't really care about state.\n//if (state === null || state !== storedState) {\n// msg.res.redirect('/error' +\n// querystring.stringify({\n// error: 'state_mismatch'\n// }));\n// return;\n//}\n\nmsg.url = 'https://accounts.spotify.com/api/token';\n\nmsg.payload = {\n \"code\": code,\n \"redirect_uri\": redirect_uri,\n \"grant_type\": 'authorization_code'\n};\n\nmsg.headers = {\n 'Content-Type' : 'application/x-www-form-urlencoded', \n 'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64'))\n};\n\nmsg.json = true;\n\nreturn msg;","outputs":1,"noerr":0,"x":324.2143096923828,"y":178.1827745437622,"wires":[["c3498d4c.4601c","6b3c44a7.af9cfc"]]},{"id":"940b279e.fe2528","type":"http response","z":"4f7b479d.f40f38","name":"","statusCode":"","headers":{},"x":877.2143096923828,"y":178.1827745437622,"wires":[]},{"id":"c3498d4c.4601c","type":"http request","z":"4f7b479d.f40f38","name":"","method":"POST","ret":"obj","url":"https://accounts.spotify.com/api/token","tls":"","x":483.2143096923828,"y":178.1827745437622,"wires":[["b39e38fa.74dba8","24682428.7ae98c"]]},{"id":"b39e38fa.74dba8","type":"function","z":"4f7b479d.f40f38","name":"Set access_token variable","func":"if ( msg.statusCode === 200) {\n var access_token = msg.payload.access_token,\n refresh_token = msg.payload.refresh_token,\n scope = msg.payload.scope,\n expires_in = msg.payload.expires_in;\n \n global.set(\"spotify_access_token\", access_token);\n global.set(\"spotify_refresh_token\", refresh_token);\n global.set(\"spotify_scope\", scope);\n}\n\n// Could use some error handling if the response code is not 200\nreturn msg;\n","outputs":1,"noerr":0,"x":693.2143096923828,"y":178.1827745437622,"wires":[["940b279e.fe2528","319702f0.1d1f2e"]]},{"id":"ee942049.7307a","type":"http in","z":"4f7b479d.f40f38","name":"","url":"/spotify/auth/callback","method":"get","upload":false,"swaggerDoc":"","x":122.21430969238281,"y":178.1827745437622,"wires":[["2c974944.d97516","ebb9aeff.06ecf"]]},{"id":"f27bfc68.78c13","type":"comment","z":"4f7b479d.f40f38","name":"Spotify Callback","info":"The URI to redirect to after the user grants or\ndenies permission. This URI needs to have been \nentered in the Redirect URI whitelist that you \nspecified when you registered your application. \nThe value of redirect_uri here must exactly \nmatch one of the values you entered when you \nregistered your application, including upper or \nlowercase, terminating slashes, and such.","x":94.5000228881836,"y":131.03991603851318,"wires":[]},{"id":"6b3c44a7.af9cfc","type":"debug","z":"4f7b479d.f40f38","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":463.7856903076172,"y":139.6113977432251,"wires":[]},{"id":"24682428.7ae98c","type":"debug","z":"4f7b479d.f40f38","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":630.9285774230957,"y":142.46849822998047,"wires":[]},{"id":"319702f0.1d1f2e","type":"debug","z":"4f7b479d.f40f38","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":875.2143421173096,"y":142.4684705734253,"wires":[]}]
Ok, it’s taken to this point just to get our access token but now that we have it, we can do whatever we want. Here’s a simple flow to get your first playlist and play it on the active device.
[{"id":"bd53672a.f0dfc8","type":"subflow","name":"Spotify - Authorization Code","info":"","in":[{"x":60,"y":220,"wires":[{"id":"94e54c31.8dbf2"}]}],"out":[{"x":1116,"y":220,"wires":[{"id":"94e54c31.8dbf2","port":1}]}]},{"id":"94e54c31.8dbf2","type":"function","z":"bd53672a.f0dfc8","name":"isAuthenticated?","func":"node.log(\"beginning spotify authentication\");\n\nnode.log(\"getting access_token\");\nvar access_token = global.get(\"spotify_access_token\")\nnode.log(\"received access token\");\nnode.log(\"spotify_access_token: \" + access_token);\nvar refresh_token = global.get(\"spotify_refresh_token\");\nnode.log(\"is this a refresh attempt: \" + msg.refresh_attempt !== undefined ? 'yes' : 'no');\n\nif (flow.get('refresh') && access_token === undefined) {\n throw 'refresh attempt without access token';\n}\n\nif (msg.refresh_attempt) {\n node.log(\"payload token value: \" + msg.payload.access_token);\n access_token = msg.payload.access_token;\n}\n\nif (access_token !== undefined) {\n node.log(\"Expected flow\");\n // This is the normal flow\n // msg.url need to be constructed in the \n // previous calling node.\n msg.headers = { 'Authorization': 'Bearer ' + access_token };\n msg.json = true;\n return [null, msg, null];\n} else if (refresh_token !== undefined) {\n node.log(\"Refresh flow\");\n flow.set('refresh', true);\n // Token has expired. Use refresh token\n // to generate a new access token.\n msg.req.query.refresh_token = refresh_token;\n msg.refresh_attempt = true;\n return [null, null, msg];\n} else {\n node.log(\"No Tokens flow\");\n // No refresh or access token. Need to login.\n msg.url = '/spotify/login';\n return [msg, null, null];\n}\n","outputs":3,"noerr":0,"x":210,"y":220,"wires":[["1ba8d1cd.36e23e","1cd125d2.a770ba"],[],["529bda7c.88cc54","7d8c818a.aadcb"]]},{"id":"2d2d6e4f.3e54c2","type":"comment","z":"bd53672a.f0dfc8","name":"Authorization Code Authentication","info":"This is the main authorization flow for logging in,\ngetting access tokens, and refreshing tokens. This \nflow allows for scopes to be set. Currently, all \nscopes are wired in.\n","x":160,"y":40,"wires":[]},{"id":"529bda7c.88cc54","type":"function","z":"bd53672a.f0dfc8","name":"Spotify Authorization","func":"var querystring = global.get('querystring');\n\nvar client_id = global.get(\"spotify_client_id\");\nvar client_secret = global.get(\"spotify_client_secret\");\n\nvar refresh_token = msg.req.query.refresh_token;\nglobal.set(\"spotify_refresh_token\", refresh_token);\nmsg.payload = {\n grant_type: 'refresh_token',\n refresh_token: refresh_token,\n refresh_attempt: true\n};\n\nmsg.headers = {\n 'Content-Type' : 'application/x-www-form-urlencoded', \n 'Authorization': 'Basic ' + (new Buffer(client_id + ':' + client_secret).toString('base64'))\n};\n \nmsg.json = true;\n\nreturn msg;","outputs":1,"noerr":0,"x":500,"y":320,"wires":[["cbbcd6d4.70f548"]]},{"id":"cbbcd6d4.70f548","type":"http request","z":"bd53672a.f0dfc8","name":"","method":"POST","ret":"obj","url":"https://accounts.spotify.com/api/token","tls":"","x":690,"y":320,"wires":[["f9c9d72d.fbf4d8"]]},{"id":"f9c9d72d.fbf4d8","type":"function","z":"bd53672a.f0dfc8","name":"Set access_token variables","func":"if ( msg.statusCode === 200) {\nnode.log(\"Status code is 200\");\n var access_token = msg.payload.access_token,\n scope = msg.payload.scope,\n expires_in = msg.payload.expires_in;\n \n global.set(\"spotify_access_token\", access_token);\n global.set(\"spotify_scope\", scope);\n \n return [msg, null];\n} else {\n node.log(\"Status code is \" + msg.statusCode);\n msg.url = \"error\";\n msg.payload = msg.statusCode;\n return [null, msg];\n}\n","outputs":2,"noerr":0,"x":900,"y":320,"wires":[["94e54c31.8dbf2"],["9e1c64ee.1146e8"]]},{"id":"9e1c64ee.1146e8","type":"http response","z":"bd53672a.f0dfc8","name":"Error","statusCode":"","headers":{},"x":1090,"y":380,"wires":[]},{"id":"b6661045.4cbf7","type":"function","z":"bd53672a.f0dfc8","name":"Spotify Authorization","func":"node.log(\"Spotify building request to callback\");\n\nvar querystring = global.get('querystring');\n\nvar client_id = global.get(\"spotify_client_id\");\nvar client_secret = global.get(\"spotify_client_secret\");\nvar redirect_uri = global.get(\"redirect_uri\");\n\n// your application requests authorization\nvar scope = 'playlist-modify-private playlist-read-private playlist-modify-public playlist-read-collaborative user-follow-read user-follow-modify user-read-private user-read-email user-read-birthdate user-top-read user-read-recently-played user-library-modify user-library-read user-read-playback-state user-read-currently-playing user-modify-playback-state streaming';\nconsole.log(scope);\nmsg.res.redirect('https://accounts.spotify.com/authorize?' +\n querystring.stringify({\n response_type: 'code',\n client_id: client_id,\n scope: scope,\n redirect_uri: redirect_uri,\n state: msg.state\n }));\n\nnode.log(\"Spotify redirecting to callback\");\n\n","outputs":1,"noerr":0,"x":740,"y":140,"wires":[["c13a40ee.f3ba3"]]},{"id":"1ba8d1cd.36e23e","type":"function","z":"bd53672a.f0dfc8","name":"Generate state value","func":"/**\n * Generates a random string containing numbers and letters\n * @param {number} length The length of the string\n * @return {string} The generated string\n */\nvar generateRandomString = function(length) {\n var text = '';\n var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n\n for (var i = 0; i < length; i++) {\n text += possible.charAt(Math.floor(Math.random() * possible.length));\n }\n return text;\n};\n\nvar state = generateRandomString(16);\nvar stateKey = 'spotify_auth_state';\nmsg.cookies = { };\nmsg.cookies[stateKey] = state;\n\nmsg.state = state;\nnode.log(\"spotify state: \" + state);\nreturn msg;","outputs":1,"noerr":0,"x":500,"y":140,"wires":[["b6661045.4cbf7","85528716.162578"]]},{"id":"68560cd9.08f144","type":"comment","z":"bd53672a.f0dfc8","name":"State","info":"Per the Spotify Documentation\nOptional, but strongly recommended. \nThe state can be useful for correlating requests \nand responses. Because your redirect_uri can be \nguessed, using a state value can increase your \nassurance that an incoming connection is the result \nof an authentication request. If you generate a \nrandom string, or encode the hash of some client \nstate, such as a cookie, in this state variable, \nyou can validate the response to additionally \nensure that both the request and response \noriginated in the same browser. This provides \nprotection against attacks such as cross-site \nrequest forgery. See RFC-6749.","x":450,"y":100,"wires":[]},{"id":"b2708426.f088b8","type":"comment","z":"bd53672a.f0dfc8","name":"Spotify Auth Redirect","info":"Need to pass:\n * client_id\n * client_secret\n * redirect_uri\n","x":740,"y":100,"wires":[]},{"id":"8c497eb1.bd6ed","type":"comment","z":"bd53672a.f0dfc8","name":"No Auth Token Flow","info":"If no access token or refresh token are present\nthen treat it as an initial login. This should\nhit the callback.","x":1250,"y":140,"wires":[]},{"id":"9dbbc7e1.088cd8","type":"comment","z":"bd53672a.f0dfc8","name":"Refresh Token Flow","info":"Refresh token is present, generate a new \naccess token.","x":1250,"y":320,"wires":[]},{"id":"f355a985.8df9d8","type":"comment","z":"bd53672a.f0dfc8","name":"Happy path","info":"Access token is present","x":1230,"y":220,"wires":[]},{"id":"7d8c818a.aadcb","type":"debug","z":"bd53672a.f0dfc8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":290,"y":340,"wires":[]},{"id":"c13a40ee.f3ba3","type":"debug","z":"bd53672a.f0dfc8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":930,"y":140,"wires":[]},{"id":"85528716.162578","type":"debug","z":"bd53672a.f0dfc8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":690,"y":180,"wires":[]},{"id":"1cd125d2.a770ba","type":"debug","z":"bd53672a.f0dfc8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":290,"y":140,"wires":[]},{"id":"6b7c0514.91f0ac","type":"catch","z":"bd53672a.f0dfc8","name":"","scope":null,"x":940,"y":380,"wires":[["9e1c64ee.1146e8"]]},{"id":"b9adfaca.a59688","type":"inject","z":"4f7b479d.f40f38","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":1160,"y":1400,"wires":[["9302d18a.7f1a4"]]},{"id":"9302d18a.7f1a4","type":"subflow:bd53672a.f0dfc8","z":"4f7b479d.f40f38","name":"Spotify Authorization","x":1200,"y":1440,"wires":[["9fa4b1b6.95aef"]]},{"id":"946bd874.2c7bd8","type":"http request","z":"4f7b479d.f40f38","name":"Get Playlists from Spotify","method":"GET","ret":"obj","url":"https://api.spotify.com/v1/me/playlists","tls":"","x":1210,"y":1520,"wires":[["e8747d2d.f7dd5"]]},{"id":"21cd1eaf.a4dc92","type":"http request","z":"4f7b479d.f40f38","name":"Play Playlist","method":"PUT","ret":"obj","url":"https://api.spotify.com/v1/me/player/play","tls":"","x":1170,"y":1600,"wires":[[]]},{"id":"e8747d2d.f7dd5","type":"function","z":"4f7b479d.f40f38","name":"Build response with first playlist","func":"msg.payload = {\n \"context_uri\":\"spotify:playlist:\" + msg.payload.items[0].id \n};\n// The http nodes overwrite the header so we need to set it again with our auth token\nmsg.headers = msg.auth;\nreturn msg;","outputs":1,"noerr":0,"x":1230,"y":1560,"wires":[["21cd1eaf.a4dc92"]]},{"id":"9fa4b1b6.95aef","type":"function","z":"4f7b479d.f40f38","name":"Set aside the auth header for later use","func":"msg.auth = msg.headers;\nreturn msg;","outputs":1,"noerr":0,"x":1250,"y":1480,"wires":[["946bd874.2c7bd8"]]}]
There are a lot of debuggers and node.log statements in the code to help you see what it’s doing. Dump these into node-red and let me know what it does. As you’re building functionality, the Spotify console is your friend https://developer.spotify.com/console/.