为什么应用程序的主要功能代码在授权工作流之前开始运行?

huangapple go评论106阅读模式
英文:

Why does the main function code for the app start running before the authorisation workflow?

问题

I was building an app that helps me move tracks from my library to another playlist which requires me to go through the Spotify Authorisation workflow. I'm fairly certain the scopes are correct and I've managed to return the correct access and refresh tokens, but I cannot figure out how to only get the tracks from the user's library after the user has logged in and authorised their account for access.

I tried passing the authorisation flow into a function only to be called before the app gets the tracks but that didn't seem to work.

  1. const __dirname = dirname(fileURLToPath(import.meta.url));
  2. const app = Express();
  3. const port = 3030;
  4. // CLIENT_SECRET stored in Config Vars
  5. // const apiUrl = "https://accounts.spotify.com/api/token"; // Spotify Web API URL
  6. const client_id = '467fab359c114e719ecefafd6af299e5'; // Client id
  7. const client_secret = 'your_client_secret' // temp client secret
  8. // const client_secret = process.env.CLIENT_SECRET;
  9. const redirect_uri = 'http://localhost:3030/callback/'; // Callback URL
  10. let AT, RT; // Stores access and refresh tokens
  11. const scope = [
  12. 'user-read-private',
  13. 'user-read-email',
  14. 'user-library-read',
  15. 'playlist-read-private',
  16. 'playlist-modify-public',
  17. 'playlist-modify-private'
  18. ];
  19. /**
  20. * Generates a random string containing numbers and letters
  21. * @param {number} length The length of the string
  22. * @return {string} The generated string
  23. */
  24. let generateRandomString = function (length) {
  25. let text = '';
  26. let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  27. for (let i = 0; i < length; i++) {
  28. text += possible.charAt(Math.floor(Math.random() * possible.length));
  29. }
  30. return text;
  31. };
  32. let stateKey = 'spotify_auth_state';
  33. const authorizeSpotify = () => {
  34. return new Promise((resolve, reject) => {
  35. app.get('/', function (req, res) {
  36. res.sendFile(__dirname + "/index.html");
  37. res.redirect('/login');
  38. });
  39. app.use(Express.static(__dirname + '/index.html'))
  40. .use(cors())
  41. .use(cookieParser());
  42. app.get('/login', function (req, res) {
  43. let state = generateRandomString(16);
  44. res.cookie(stateKey, state);
  45. // app requests authorization
  46. res.redirect('https://accounts.spotify.com/authorize?' +
  47. querystring.stringify({
  48. response_type: 'code',
  49. client_id: client_id,
  50. scope: scope,
  51. redirect_uri: redirect_uri,
  52. state: state
  53. }));
  54. });
  55. app.get('/callback', function (req, res) {
  56. // app requests refresh and access tokens
  57. // after checking the state parameter
  58. let code = req.query.code || null;
  59. let state = req.query.state || null;
  60. let storedState = req.cookies ? req.cookies[stateKey] : null;
  61. console.log(state);
  62. console.log(storedState);
  63. if (state === null || state !== storedState) {
  64. res.redirect('/#' +
  65. querystring.stringify({
  66. error: 'state_mismatch'
  67. }));
  68. } else {
  69. res.clearCookie(stateKey);
  70. let authOptions = {
  71. url: 'https://accounts.spotify.com/api/token',
  72. form: {
  73. code: code,
  74. redirect_uri: redirect_uri,
  75. grant_type: 'authorization_code'
  76. },
  77. headers: {
  78. 'Authorization': 'Basic ' + (Buffer.from(client_id + ':' + client_secret).toString('base64'))
  79. },
  80. json: true
  81. };
  82. request.post(authOptions, function (error, response, body) {
  83. if (!error && response.statusCode === 200) {
  84. console.log(body);
  85. AT = body.access_token;
  86. RT = body.refresh_token;
  87. let options = {
  88. url: 'https://api.spotify.com/v1/me',
  89. headers: { 'Authorization': 'Bearer ' + AT },
  90. json: true
  91. };
  92. interval = setInterval(requestToken, body.expires_in * 1000 * 0.70);
  93. previousExpires = body.expires_in;
  94. res.send("Logged in!");
  95. }
  96. });
  97. }
  98. });
  99. let interval;
  100. let previousExpires = 0;
  101. const requestToken = () => {
  102. const authOptions = {
  103. url: 'https://accounts.spotify.com/api/token',
  104. headers: { 'Authorization': 'Basic ' + (Buffer.from(client_id + ':' + client_secret).toString('base64')) },
  105. form: {
  106. grant_type: 'refresh_token',
  107. refresh_token: RT
  108. },
  109. json: true
  110. };
  111. request.post(authOptions, function (error, response, body) {
  112. if (error || response.statusCode !== 200) {
  113. console.error(error);
  114. return;
  115. }
  116. AT = body.access_token;
  117. if (body.refresh_token) {
  118. RT = body.refresh_token;
  119. }
  120. console.log("Access Token refreshed!");
  121. if (previousExpires != body.expires_in) {
  122. clearInterval(interval);
  123. interval = setInterval(requestToken, body.expires_in * 1000 * 0.70);
  124. previousExpires = body.expires_in;
  125. }
  126. });
  127. }
  128. resolve({AT, RT});
  129. });
  130. };
  131. // Write code for app here
  132. // Function to get the user's library tracks
  133. const getUserLibraryTracks = (AT) => {
  134. return new Promise((resolve, reject) => {
  135. const options = {
  136. url: 'https://api.spotify.com/v1/me/tracks',
  137. headers: { 'Authorization': 'Bearer ' + AT },
  138. json: true
  139. };
  140. request.get(options, (error, response, body) => {
  141. if (error || response.statusCode !== 200) {
  142. console.log('Response:', body);
  143. reject(error || new Error('Failed to get user library tracks'));
  144. } else {
  145. resolve(body.items.map(item => item.track));
  146. }
  147. });
  148. });
  149. };
  150. // Function to get the tracks in a playlist
  151. const getPlaylistTracks = (AT, playlistId) => {
  152. return new Promise((resolve, reject) => {
  153. const options = {
  154. url: `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
  155. headers: { 'Authorization': 'Bearer ' + AT },
  156. json: true
  157. };
  158. request.get(options, (error, response, body) => {
  159. if (error || response.statusCode !== 200) {
  160. reject(error || new Error('Failed to get playlist tracks'));
  161. } else {
  162. resolve(body.items.map(item => item.track));
  163. }
  164. });
  165. });
  166. };
  167. // Function to add tracks to a playlist
  168. const addTracksToPlaylist = (AT, playlistId, trackIds) => {
  169. return new Promise((resolve, reject) => {
  170. const options = {
  171. url: `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
  172. headers: { 'Authorization': 'Bearer ' + AT },
  173. json: true,
  174. body: { uris: trackIds }
  175. };
  176. request.post(options, (error, response, body) => {
  177. if (error || response.statusCode
  178. <details>
  179. <summary>英文:</summary>
  180. I was building an app that helps me move tracks from my library to another playlist which requires me to go through the Spotify Authorisation workflow. I&#39;m fairly certain the scopes are correct and I&#39;ve managed to return the correct access and refresh tokens, but I cannot figure out how to only get the tracks from the user&#39;s library _after_ the user has logged in and authorised their account for access.
  181. I tried passing the authorisation flow into a function only to be called before the app gets the tracks but that didn&#39;t seem to work.
  182. ```js
  183. const __dirname = dirname(fileURLToPath(import.meta.url));
  184. const app = Express();
  185. const port = 3030;
  186. // CLIENT_SECRET stored in Config Vars
  187. // const apiUrl = &quot;https://accounts.spotify.com/api/token&quot;; // Spotify Web API URL
  188. const client_id = &#39;467fab359c114e719ecefafd6af299e5&#39;; // Client id
  189. const client_secret = &#39;your_client_secret&#39; // temp client secret
  190. // const client_secret = process.env.CLIENT_SECRET;
  191. const redirect_uri = &#39;http://localhost:3030/callback/&#39;; // Callback URL
  192. let AT, RT; // Stores access and refresh tokens
  193. const scope = [
  194. &#39;user-read-private&#39;,
  195. &#39;user-read-email&#39;,
  196. &#39;user-library-read&#39;,
  197. &#39;playlist-read-private&#39;,
  198. &#39;playlist-modify-public&#39;,
  199. &#39;playlist-modify-private&#39;
  200. ];
  201. /**
  202. * Generates a random string containing numbers and letters
  203. * @param {number} length The length of the string
  204. * @return {string} The generated string
  205. */
  206. let generateRandomString = function (length) {
  207. let text = &#39;&#39;;
  208. let possible = &#39;ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789&#39;;
  209. for (let i = 0; i &lt; length; i++) {
  210. text += possible.charAt(Math.floor(Math.random() * possible.length));
  211. }
  212. return text;
  213. };
  214. let stateKey = &#39;spotify_auth_state&#39;;
  215. const authorizeSpotify = () =&gt; {
  216. return new Promise((resolve, reject) =&gt; {
  217. app.get(&#39;/&#39;, function (req, res) {
  218. res.sendFile(__dirname + &quot;/index.html&quot;);
  219. res.redirect(&#39;/login&#39;);
  220. });
  221. app.use(Express.static(__dirname + &#39;/index.html&#39;))
  222. .use(cors())
  223. .use(cookieParser());
  224. app.get(&#39;/login&#39;, function (req, res) {
  225. let state = generateRandomString(16);
  226. res.cookie(stateKey, state);
  227. // app requests authorization
  228. res.redirect(&#39;https://accounts.spotify.com/authorize?&#39; +
  229. querystring.stringify({
  230. response_type: &#39;code&#39;,
  231. client_id: client_id,
  232. scope: scope,
  233. redirect_uri: redirect_uri,
  234. state: state
  235. }));
  236. });
  237. app.get(&#39;/callback&#39;, function (req, res) {
  238. // app requests refresh and access tokens
  239. // after checking the state parameter
  240. let code = req.query.code || null;
  241. let state = req.query.state || null;
  242. let storedState = req.cookies ? req.cookies[stateKey] : null;
  243. console.log(state);
  244. console.log(storedState);
  245. if (state === null || state !== storedState) {
  246. res.redirect(&#39;/#&#39; +
  247. querystring.stringify({
  248. error: &#39;state_mismatch&#39;
  249. }));
  250. } else {
  251. res.clearCookie(stateKey);
  252. let authOptions = {
  253. url: &#39;https://accounts.spotify.com/api/token&#39;,
  254. form: {
  255. code: code,
  256. redirect_uri: redirect_uri,
  257. grant_type: &#39;authorization_code&#39;
  258. },
  259. headers: {
  260. &#39;Authorization&#39;: &#39;Basic &#39; + (Buffer.from(client_id + &#39;:&#39; + client_secret).toString(&#39;base64&#39;))
  261. },
  262. json: true
  263. };
  264. request.post(authOptions, function (error, response, body) {
  265. if (!error &amp;&amp; response.statusCode === 200) {
  266. console.log(body);
  267. AT = body.access_token;
  268. RT = body.refresh_token;
  269. let options = {
  270. url: &#39;https://api.spotify.com/v1/me&#39;,
  271. headers: { &#39;Authorization&#39;: &#39;Bearer &#39; + AT },
  272. json: true
  273. };
  274. interval = setInterval(requestToken, body.expires_in * 1000 * 0.70);
  275. previousExpires = body.expires_in;
  276. res.send(&quot;Logged in!&quot;);
  277. }
  278. });
  279. }
  280. });
  281. let interval;
  282. let previousExpires = 0;
  283. const requestToken = () =&gt; {
  284. const authOptions = {
  285. url: &#39;https://accounts.spotify.com/api/token&#39;,
  286. headers: { &#39;Authorization&#39;: &#39;Basic &#39; + (Buffer.from(client_id + &#39;:&#39; + client_secret).toString(&#39;base64&#39;)) },
  287. form: {
  288. grant_type: &#39;refresh_token&#39;,
  289. refresh_token: RT
  290. },
  291. json: true
  292. };
  293. request.post(authOptions, function (error, response, body) {
  294. if (error || response.statusCode !== 200) {
  295. console.error(error);
  296. return;
  297. }
  298. AT = body.access_token;
  299. if (body.refresh_token) {
  300. RT = body.refresh_token;
  301. }
  302. console.log(&quot;Access Token refreshed!&quot;);
  303. if (previousExpires != body.expires_in) {
  304. clearInterval(interval);
  305. interval = setInterval(requestToken, body.expires_in * 1000 * 0.70);
  306. previousExpires = body.expires_in;
  307. }
  308. });
  309. }
  310. resolve({AT, RT});
  311. });
  312. };
  313. // Write code for app here
  314. // Function to get the user&#39;s library tracks
  315. const getUserLibraryTracks = (AT) =&gt; {
  316. return new Promise((resolve, reject) =&gt; {
  317. const options = {
  318. url: &#39;https://api.spotify.com/v1/me/tracks&#39;,
  319. headers: { &#39;Authorization&#39;: &#39;Bearer &#39; + AT },
  320. json: true
  321. };
  322. request.get(options, (error, response, body) =&gt; {
  323. if (error || response.statusCode !== 200) {
  324. console.log(&#39;Response:&#39;, body);
  325. reject(error || new Error(&#39;Failed to get user library tracks&#39;));
  326. } else {
  327. resolve(body.items.map(item =&gt; item.track));
  328. }
  329. });
  330. });
  331. };
  332. // Function to get the tracks in a playlist
  333. const getPlaylistTracks = (AT, playlistId) =&gt; {
  334. return new Promise((resolve, reject) =&gt; {
  335. const options = {
  336. url: `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
  337. headers: { &#39;Authorization&#39;: &#39;Bearer &#39; + AT },
  338. json: true
  339. };
  340. request.get(options, (error, response, body) =&gt; {
  341. if (error || response.statusCode !== 200) {
  342. reject(error || new Error(&#39;Failed to get playlist tracks&#39;));
  343. } else {
  344. resolve(body.items.map(item =&gt; item.track));
  345. }
  346. });
  347. });
  348. };
  349. // Function to add tracks to a playlist
  350. const addTracksToPlaylist = (AT, playlistId, trackIds) =&gt; {
  351. return new Promise((resolve, reject) =&gt; {
  352. const options = {
  353. url: `https://api.spotify.com/v1/playlists/${playlistId}/tracks`,
  354. headers: { &#39;Authorization&#39;: &#39;Bearer &#39; + AT },
  355. json: true,
  356. body: { uris: trackIds }
  357. };
  358. request.post(options, (error, response, body) =&gt; {
  359. if (error || response.statusCode !== 201) {
  360. reject(error || new Error(&#39;Failed to add tracks to playlist&#39;));
  361. } else {
  362. resolve();
  363. }
  364. });
  365. });
  366. };
  367. // Function to update the playlist with new tracks
  368. const updatePlaylist = async (playlistId) =&gt; {
  369. try {
  370. const {AT, RT } = await authorizeSpotify();
  371. const libraryTracks = await getUserLibraryTracks(AT);
  372. const playlistTracks = await getPlaylistTracks(AT, playlistId);
  373. const trackIdsToAdd = libraryTracks
  374. .filter(track =&gt; !playlistTracks.some(playlistTrack =&gt; playlistTrack.id === track.id))
  375. .map(track =&gt; track.uri);
  376. await addTracksToPlaylist(AT, playlistId, trackIdsToAdd);
  377. console.log(&#39;Playlist updated successfully&#39;);
  378. } catch (error) {
  379. console.error(&#39;Failed to update playlist:&#39;, error);
  380. }
  381. };
  382. // Call the updatePlaylist function to update the playlist
  383. updatePlaylist(&#39;your_playlist_id&#39;);
  384. app.listen(port, () =&gt; console.log(`Listening on port: ${port}`));

Running the code, I keep receiving this message:

  1. [nodemon] starting `node autoadd.js`
  2. Listening on port: 3030
  3. Response: { error: { status: 401, message: &#39;Invalid access token&#39; } }
  4. Failed to update playlist: Error: Failed to get user library tracks
  5. at Request._callback (file:///home/nero/Projects/Autoadd/autoadd.js:195:25)
  6. at Request.self.callback (/home/nero/Projects/Autoadd/node_modules/request/request.js:185:22)
  7. at Request.emit (events.js:314:20)
  8. at Request.&lt;anonymous&gt; (/home/nero/Projects/Autoadd/node_modules/request/request.js:1154:10)
  9. at Request.emit (events.js:314:20)
  10. at IncomingMessage.&lt;anonymous&gt; (/home/nero/Projects/Autoadd/node_modules/request/request.js:1076:12)
  11. at Object.onceWrapper (events.js:420:28)
  12. at IncomingMessage.emit (events.js:326:22)
  13. at endReadableNT (_stream_readable.js:1241:12)
  14. at processTicksAndRejections (internal/process/task_queues.js:84:21)

Of course, after running the code, I browse to the localhost:3030 address and it logs in successfully with my account. The console logging this:

  1. {
  2. access_token: &#39;BQBC1CAN2Wv3PIR1XdwTuQwgrHjQ1eCgQJqAZ0PWBNAiHGk6OKqsJFeafJEqBXBWfg1qpOvVxfEJ4SF77OHgxn9OvxS8Lg9Na0NSFlz1iWR26xztSJEq4Or-hwUKB2yE_Y-X6yPvzaScar7HDFADSQtVMxOx1Z8wq3hbi498i0bGTTnYccFTijopoSxbwfKvbfMTRxNrdUJt0z8u_w&#39;,
  3. token_type: &#39;Bearer&#39;,
  4. expires_in: 3600,
  5. refresh_token: &#39;AQC3bMXEM23qjQqOOXrC5Tcsvt6ijfp2umMyz466u1DCi9nNN2J9jsU0Q4ilYq2cu19xA80fhrljQSutWrFGyBzOUV3i1mytO4UBEjbbKOHuKXFXwEYV83Rxzo-7ic_-YFA&#39;,
  6. scope: &#39;playlist-modify-private&#39;
  7. }

答案1

得分: 1

Flow Overview
为什么应用程序的主要功能代码在授权工作流之前开始运行?

使用express作为REST服务器,axios用于Spotify REST POST调用。

使用授权码流获取访问令牌

Spotify API调用

获取播放列表项

  1. GET /playlists/{playlist_id}/tracks

为什么应用程序的主要功能代码在授权工作流之前开始运行?

获取用户保存的曲目

  1. GET /me/tracks

为什么应用程序的主要功能代码在授权工作流之前开始运行?

将项目添加到播放列表

  1. POST /playlists/{playlist_id}/tracks

演示代码

保存为update_songs.js

  1. const express = require(&quot;express&quot;)
  2. const axios = require(&#39;axios&#39;)
  3. const cors = require(&quot;cors&quot;);
  4. const app = express()
  5. app.use(cors())
  6. CLIENT_ID = &quot;&lt;your client ID&gt;&quot;
  7. CLIENT_SECRET = &quot;&lt;your client secret&gt;&quot;
  8. PORT = 3030 // it is located in Spotify dashboard&#39;s Redirect URIs, my port is 3000
  9. REDIRECT_URI = `http://localhost:${PORT}/callback` // my case is &#39;http://localhost:3000/callback&#39;
  10. PLAYLIST_ID = &#39;your playlist ID&#39;
  11. SCOPE = [
  12. &#39;user-read-private&#39;,
  13. &#39;user-read-email&#39;,
  14. &#39;user-library-read&#39;,
  15. &#39;playlist-read-private&#39;,
  16. &#39;playlist-modify-public&#39;,
  17. &#39;playlist-modify-private&#39;
  18. ]
  19. const getToken = async (code) =&gt; {
  20. try {
  21. const resp = await axios.post(
  22. url = &#39;https://accounts.spotify.com/api/token&#39;,
  23. data = new URLSearchParams({
  24. &#39;grant_type&#39;: &#39;authorization_code&#39;,
  25. &#39;redirect_uri&#39;: REDIRECT_URI,
  26. &#39;code&#39;: code
  27. }),
  28. config = {
  29. headers: {
  30. &#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded&#39;
  31. },
  32. auth: {
  33. username: CLIENT_ID,
  34. password: CLIENT_SECRET
  35. }
  36. })
  37. return Promise.resolve(resp.data.access_token);
  38. } catch (err) {
  39. console.error(err)
  40. return Promise.reject(err)
  41. }
  42. }
  43. const addSongs = async (playlist_id, tracks, token) =&gt; {
  44. try {
  45. const uris = []
  46. for(const track of tracks) {
  47. if (track.new) {
  48. uris.push(track.uri)
  49. }
  50. }
  51. const chunkSize = 100;
  52. for (let i = 0; i &lt; uris.length; i += chunkSize) {
  53. const sub_uris = uris.slice(i, i + chunkSize);
  54. const resp = await axios.post(
  55. url = `https://api.spotify.com/v1/playlists/${playlist_id}/tracks`,
  56. data = {
  57. &#39;uris&#39;: sub_uris
  58. },
  59. config = {
  60. headers: {
  61. &#39;Content-Type&#39;: &#39;application/json&#39;,
  62. &#39;Authorization&#39;: `Bearer ${token}`,
  63. }
  64. })
  65. }
  66. return Promise.resolve(&#39;OK&#39;);
  67. } catch (err) {
  68. console.error(err)
  69. return Promise.reject(err)
  70. }
  71. }
  72. const getPlaylistTracks = async (playlist, token) =&gt; {
  73. try {
  74. let next = 1
  75. const tracks = []
  76. url = `https://api.spotify.com/v1/playlists/${playlist}`
  77. while (next != null) {
  78. const resp = await axios.get(
  79. url,
  80. config = {
  81. headers: {
  82. &#39;Accept-Encoding&#39;: &#39;application/json&#39;,
  83. &#39;Authorization&#39;: `Bearer ${token}`,
  84. }
  85. }
  86. )
  87. items = []
  88. if (resp.data.items) {
  89. items = resp.data.items
  90. } else if (resp.data.tracks.items) {
  91. items = resp.data.tracks.items
  92. }
  93. for(const item of items) {
  94. if (item.track?.name != null) {
  95. tracks.push({
  96. name: item.track.name,
  97. external_urls: item.track.external_urls.spotify,
  98. uri: item.track.uri,
  99. new: false
  100. })
  101. }
  102. }
  103. if (resp.data.items) {
  104. url = resp.data.next
  105. } else if (resp.data.tracks.items) {
  106. url = resp.data.tracks.next
  107. } else {
  108. break
  109. }
  110. next = url
  111. }
  112. return Promise.resolve(tracks)
  113. } catch (err) {
  114. console.error(err)
  115. return Promise.reject(err)
  116. }
  117. }
  118. const update_track = (arr, track) =&gt; {
  119. const { length } = arr;
  120. const id = length + 1;
  121. const found = arr.some(el =&gt; el.external_urls === track.external_urls);
  122. if (!found) {
  123. arr.push({ name : track.name, external_urls: track.external_urls, uri: track.uri, new: true })
  124. };
  125. return arr;
  126. }
  127. const updatePlaylistTracks = async (my_tracks, previous_tracks, token) =&gt; {
  128. try {
  129. new_tracks = previous_tracks.map(a =&gt; Object.assign({}, a));
  130. // update new playlist with my_tracks and previous_tracks
  131. for(const track of my_tracks) {
  132. new_tracks = update_track(new_tracks, track)
  133. }
  134. return Promise.resolve(new_tracks)
  135. } catch (err) {
  136. console.error(err)
  137. return Promise.reject(err)
  138. }
  139. }
  140. const getMyTracks = async (token) =&gt; {
  141. try {
  142. let offset = 0
  143. let next = 1
  144. const limit = 50;
  145. const tracks = [];
  146. while (next != null) {
  147. const resp = await axios.get(
  148. url = `https://api.spotify.com/v1/me/tracks/?limit=${limit}&amp;offset=${offset}`,
  149. config = {
  150. headers: {
  151. &#39;Accept-Encoding&#39;: &#39;application/json&#39;,
  152. &#39;Authorization&#39;: `Bearer ${token}`,
  153. }
  154. }
  155. );
  156. for(const item of resp.data.items) {
  157. if(item.track?.name != null) {
  158. tracks.push({
  159. name: item.track.name,
  160. external_urls: item.track.external_urls.spotify,
  161. uri: item.track.uri,
  162. new: false,
  163. added_at: item.added_at
  164. })
  165. }
  166. }
  167. offset = offset + limit
  168. <details>
  169. <summary>英文:</summary>
  170. My understanding is you want to get my tracks and adding into the playlist except for duplicated(already existing) songs.
  171. Flow Overview
  172. [![enter image description here][1]][1]
  173. Using [`express`](https://expressjs.com/) for REST server, [`axios`](https://axios-http.com/docs/intro) for Spotify REST POST call.
  174. Using [`Authorization Code Flow`](https://developer.spotify.com/documentation/web-api/tutorials/code-flow) for getting `access token`
  175. #### Spotify API calls
  176. [Get Playlist Items](https://developer.spotify.com/documentation/web-api/reference/get-playlists-tracks)

GET /playlists/{playlist_id}/tracks

  1. [![enter image description here][2]][2]
  2. [Get User&#39;s Saved Tracks](https://developer.spotify.com/documentation/web-api/reference/get-users-saved-tracks)

GET /me/tracks

  1. [![enter image description here][3]][3]
  2. [Add Items to Playlist](https://developer.spotify.com/documentation/web-api/reference/add-tracks-to-playlist)

POST /playlists/{playlist_id}/tracks

  1. ### Demo code
  2. Save as `update_songs.js`
  3. ```node.js
  4. const express = require(&quot;express&quot;)
  5. const axios = require(&#39;axios&#39;)
  6. const cors = require(&quot;cors&quot;);
  7. const app = express()
  8. app.use(cors())
  9. CLIENT_ID = &quot;&lt;your client ID&gt;&quot;
  10. CLIENT_SECRET = &quot;&lt;your client secret&gt;&quot;
  11. PORT = 3030 // it is located in Spotify dashboard&#39;s Redirect URIs, my port is 3000
  12. REDIRECT_URI = `http://localhost:${PORT}/callback` // my case is &#39;http://localhost:3000/callback&#39;
  13. PLAYLIST_ID = &#39;your playlist ID&#39;
  14. SCOPE = [
  15. &#39;user-read-private&#39;,
  16. &#39;user-read-email&#39;,
  17. &#39;user-library-read&#39;,
  18. &#39;playlist-read-private&#39;,
  19. &#39;playlist-modify-public&#39;,
  20. &#39;playlist-modify-private&#39;
  21. ]
  22. const getToken = async (code) =&gt; {
  23. try {
  24. const resp = await axios.post(
  25. url = &#39;https://accounts.spotify.com/api/token&#39;,
  26. data = new URLSearchParams({
  27. &#39;grant_type&#39;: &#39;authorization_code&#39;,
  28. &#39;redirect_uri&#39;: REDIRECT_URI,
  29. &#39;code&#39;: code
  30. }),
  31. config = {
  32. headers: {
  33. &#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded&#39;
  34. },
  35. auth: {
  36. username: CLIENT_ID,
  37. password: CLIENT_SECRET
  38. }
  39. })
  40. return Promise.resolve(resp.data.access_token);
  41. } catch (err) {
  42. console.error(err)
  43. return Promise.reject(err)
  44. }
  45. }
  46. const addSongs = async (playlist_id, tracks, token) =&gt; {
  47. try {
  48. const uris = []
  49. for(const track of tracks) {
  50. if (track.new) {
  51. uris.push(track.uri)
  52. }
  53. }
  54. const chunkSize = 100;
  55. for (let i = 0; i &lt; uris.length; i += chunkSize) {
  56. const sub_uris = uris.slice(i, i + chunkSize);
  57. const resp = await axios.post(
  58. url = `https://api.spotify.com/v1/playlists/${playlist_id}/tracks`,
  59. data = {
  60. &#39;uris&#39;: sub_uris
  61. },
  62. config = {
  63. headers: {
  64. &#39;Content-Type&#39;: &#39;application/json&#39;,
  65. &#39;Authorization&#39;: `Bearer ${token}`,
  66. }
  67. })
  68. }
  69. return Promise.resolve(&#39;OK&#39;);
  70. } catch (err) {
  71. console.error(err)
  72. return Promise.reject(err)
  73. }
  74. }
  75. const getPlaylistTracks = async (playlist, token) =&gt; {
  76. try {
  77. let next = 1
  78. const tracks = []
  79. url = `https://api.spotify.com/v1/playlists/${playlist}`
  80. while (next != null) {
  81. const resp = await axios.get(
  82. url,
  83. config = {
  84. headers: {
  85. &#39;Accept-Encoding&#39;: &#39;application/json&#39;,
  86. &#39;Authorization&#39;: `Bearer ${token}`,
  87. }
  88. }
  89. )
  90. items = []
  91. if (resp.data.items) {
  92. items = resp.data.items
  93. } else if (resp.data.tracks.items) {
  94. items = resp.data.tracks.items
  95. }
  96. for(const item of items) {
  97. if (item.track?.name != null) {
  98. tracks.push({
  99. name: item.track.name,
  100. external_urls: item.track.external_urls.spotify,
  101. uri: item.track.uri,
  102. new: false
  103. })
  104. }
  105. }
  106. if (resp.data.items) {
  107. url = resp.data.next
  108. } else if (resp.data.tracks.items) {
  109. url = resp.data.tracks.next
  110. } else {
  111. break
  112. }
  113. next = url
  114. }
  115. return Promise.resolve(tracks)
  116. } catch (err) {
  117. console.error(err)
  118. return Promise.reject(err)
  119. }
  120. }
  121. const update_track = (arr, track) =&gt; {
  122. const { length } = arr;
  123. const id = length + 1;
  124. const found = arr.some(el =&gt; el.external_urls === track.external_urls);
  125. if (!found) {
  126. arr.push({ name : track.name, external_urls: track.external_urls, uri: track.uri, new: true })
  127. };
  128. return arr;
  129. }
  130. const updatePlaylistTracks = async (my_tracks, previous_tracks, token) =&gt; {
  131. try {
  132. new_tracks = previous_tracks.map(a =&gt; Object.assign({}, a));
  133. // update new playlist with my_tracks and previous_tracks
  134. for(const track of my_tracks) {
  135. new_tracks = update_track(new_tracks, track)
  136. }
  137. return Promise.resolve(new_tracks)
  138. } catch (err) {
  139. console.error(err)
  140. return Promise.reject(err)
  141. }
  142. }
  143. const getMyTracks = async (token) =&gt; {
  144. try {
  145. let offset = 0
  146. let next = 1
  147. const limit = 50;
  148. const tracks = [];
  149. while (next != null) {
  150. const resp = await axios.get(
  151. url = `https://api.spotify.com/v1/me/tracks/?limit=${limit}&amp;offset=${offset}`,
  152. config = {
  153. headers: {
  154. &#39;Accept-Encoding&#39;: &#39;application/json&#39;,
  155. &#39;Authorization&#39;: `Bearer ${token}`,
  156. }
  157. }
  158. );
  159. for(const item of resp.data.items) {
  160. if(item.track?.name != null) {
  161. tracks.push({
  162. name: item.track.name,
  163. external_urls: item.track.external_urls.spotify,
  164. uri: item.track.uri,
  165. new: false,
  166. added_at: item.added_at
  167. })
  168. }
  169. }
  170. offset = offset + limit
  171. next = resp.data.next
  172. }
  173. return Promise.resolve(tracks)
  174. } catch (err) {
  175. console.error(err)
  176. return Promise.reject(err)
  177. }
  178. }
  179. app.get(&quot;/login&quot;, (request, response) =&gt; {
  180. const redirect_url = `https://accounts.spotify.com/authorize?response_type=code&amp;client_id=${CLIENT_ID}&amp;scope=${SCOPE}&amp;state=123456&amp;redirect_uri=${REDIRECT_URI}&amp;prompt=consent`
  181. response.redirect(redirect_url);
  182. })
  183. app.get(&quot;/callback&quot;, async (request, response) =&gt; {
  184. const code = request.query[&quot;code&quot;]
  185. getToken(code)
  186. .then(access_token =&gt; {
  187. getMyTracks(access_token)
  188. .then(my_tracks =&gt; {
  189. getPlaylistTracks(PLAY_LIST_ID, access_token)
  190. .then(previous_tracks =&gt; {
  191. updatePlaylistTracks(my_tracks, previous_tracks, access_token)
  192. .then(new_tracks =&gt; {
  193. addSongs(PLAY_LIST_ID, new_tracks, access_token)
  194. .then(OK =&gt; {
  195. return response.send({
  196. &#39;my tracks Total:&#39;: my_tracks.length,
  197. &#39;my tracks&#39;: my_tracks,
  198. &#39;previous playlist Total:&#39;: previous_tracks.length,
  199. &#39;previous playlist&#39;: previous_tracks,
  200. &#39;new playlist Total:&#39;: new_tracks.length,
  201. &#39;new playlist&#39;: new_tracks,
  202. &#39;add song result&#39;: OK });
  203. })
  204. })
  205. })
  206. })
  207. })
  208. .catch(error =&gt; {
  209. console.log(error.message);
  210. })
  211. })
  212. app.listen(PORT, () =&gt; {
  213. console.log(`Listening on :${PORT}`)
  214. })

Install dependencies

  1. npm install express axios cors

Run it

From terminal

  1. node update_songs.js

By browser

  1. http://locahost:3030/login

Result

From Browser

My library list songs: total 837 songs

为什么应用程序的主要功能代码在授权工作流之前开始运行?

Previous Playlist songs: total 771 songs

Before adding songs (previous playlist songs)

为什么应用程序的主要功能代码在授权工作流之前开始运行?

为什么应用程序的主要功能代码在授权工作流之前开始运行?
After adding songs: total 1,591 songs

为什么应用程序的主要功能代码在授权工作流之前开始运行?

为什么应用程序的主要功能代码在授权工作流之前开始运行?

Update for checking new songs

For checking if any new songs are in my library, can see when it added_at

You need to add one line of code into getMyTracks()

  1. tracks.push({
  2. name: item.track.name,
  3. external_urls: item.track.external_urls.spotify,
  4. uri: item.track.uri,
  5. new: false,
  6. added_at: item.added_at
  7. })

Update V3 for my tracks only

Step By Step, this code can get my library songs.

  1. const express = require(&quot;express&quot;)
  2. const axios = require(&#39;axios&#39;)
  3. const cors = require(&quot;cors&quot;);
  4. const app = express()
  5. app.use(cors())
  6. CLIENT_ID = &quot;&lt;your client ID&gt;&quot;
  7. CLIENT_SECRET = &quot;&lt;your client secret&gt;&quot;
  8. PORT = 3030 // it is located in Spotify dashboard&#39;s Redirect URIs
  9. REDIRECT_URI = `http://localhost:${PORT}/callback` // my case is &#39;http://localhost:3000/callback&#39;
  10. SCOPE = [
  11. &#39;user-read-private&#39;,
  12. &#39;user-read-email&#39;,
  13. &#39;user-library-read&#39;,
  14. &#39;playlist-read-private&#39;,
  15. &#39;playlist-modify-public&#39;,
  16. &#39;playlist-modify-private&#39;
  17. ]
  18. app.get(&quot;/login&quot;, (request, response) =&gt; {
  19. const redirect_url = `https://accounts.spotify.com/authorize?response_type=code&amp;client_id=${CLIENT_ID}&amp;scope=${SCOPE}&amp;state=123456&amp;redirect_uri=${REDIRECT_URI}&amp;prompt=consent`
  20. response.redirect(redirect_url);
  21. })
  22. const getToken = async (code) =&gt; {
  23. try {
  24. const resp = await axios.post(
  25. &#39;https://accounts.spotify.com/api/token&#39;,
  26. new URLSearchParams({
  27. &#39;grant_type&#39;: &#39;authorization_code&#39;,
  28. &#39;redirect_uri&#39;: REDIRECT_URI,
  29. &#39;code&#39;: code
  30. }),
  31. {
  32. headers: {
  33. &#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded&#39;
  34. },
  35. auth: {
  36. username: CLIENT_ID,
  37. password: CLIENT_SECRET
  38. }
  39. })
  40. return Promise.resolve(resp.data.access_token);
  41. } catch (err) {
  42. console.error(err)
  43. return Promise.reject(err)
  44. }
  45. }
  46. const getMyTracks = async (token) =&gt; {
  47. try {
  48. let offset = 0
  49. let next = 1
  50. const limit = 50;
  51. const tracks = [];
  52. while (next != null) {
  53. const resp = await axios.get(
  54. url = `https://api.spotify.com/v1/me/tracks/?limit=${limit}&amp;offset=${offset}`,
  55. config = {
  56. headers: {
  57. &#39;Accept-Encoding&#39;: &#39;application/json&#39;,
  58. &#39;Authorization&#39;: `Bearer ${token}`,
  59. }
  60. }
  61. );
  62. for(const item of resp.data.items) {
  63. if(item.track?.name != null) {
  64. tracks.push({
  65. name: item.track.name,
  66. external_urls: item.track.external_urls.spotify,
  67. uri: item.track.uri,
  68. new: false,
  69. added_at: item.added_at
  70. })
  71. }
  72. }
  73. offset = offset + limit
  74. next = resp.data.next
  75. }
  76. return Promise.resolve(tracks)
  77. } catch (err) {
  78. console.error(err)
  79. return Promise.reject(err)
  80. }
  81. };
  82. app.get(&quot;/callback&quot;, async (request, response) =&gt; {
  83. const code = request.query[&quot;code&quot;]
  84. getToken(code)
  85. .then(access_token =&gt; {
  86. getMyTracks(access_token)
  87. .then(my_tracks =&gt; {
  88. return response.send({
  89. &#39;total:&#39; : my_tracks.length,
  90. &#39;my tracks&#39;: my_tracks
  91. });
  92. })
  93. })
  94. .catch(error =&gt; {
  95. console.log(error.message);
  96. })
  97. })
  98. app.listen(PORT, () =&gt; {
  99. console.log(`Listening on :${PORT}`)
  100. })

Update V3 for plylist only

Step By Step, this code can get playlist songs.

  1. const express = require(&quot;express&quot;)
  2. const axios = require(&#39;axios&#39;)
  3. const cors = require(&quot;cors&quot;);
  4. const app = express()
  5. app.use(cors())
  6. CLIENT_ID = &quot;&lt;your client ID&gt;&quot;
  7. CLIENT_SECRET = &quot;&lt;your client secret&gt;&quot;
  8. PORT = 3030 // it is located in Spotify dashboard&#39;s Redirect URIs
  9. REDIRECT_URI = `http://localhost:${PORT}/callback` // my case is &#39;http://localhost:3000/callback&#39;
  10. PLAYLIST_ID = &#39;your playlist ID&#39;
  11. SCOPE = [
  12. &#39;user-read-private&#39;,
  13. &#39;user-read-email&#39;,
  14. &#39;user-library-read&#39;,
  15. &#39;playlist-read-private&#39;,
  16. &#39;playlist-modify-public&#39;,
  17. &#39;playlist-modify-private&#39;
  18. ]
  19. app.get(&quot;/login&quot;, (request, response) =&gt; {
  20. const redirect_url = `https://accounts.spotify.com/authorize?response_type=code&amp;client_id=${CLIENT_ID}&amp;scope=${SCOPE}&amp;state=123456&amp;redirect_uri=${REDIRECT_URI}&amp;prompt=consent`
  21. response.redirect(redirect_url);
  22. })
  23. const getToken = async (code) =&gt; {
  24. try {
  25. const resp = await axios.post(
  26. &#39;https://accounts.spotify.com/api/token&#39;,
  27. new URLSearchParams({
  28. &#39;grant_type&#39;: &#39;authorization_code&#39;,
  29. &#39;redirect_uri&#39;: REDIRECT_URI,
  30. &#39;code&#39;: code
  31. }),
  32. {
  33. headers: {
  34. &#39;Content-Type&#39;: &#39;application/x-www-form-urlencoded&#39;
  35. },
  36. auth: {
  37. username: CLIENT_ID,
  38. password: CLIENT_SECRET
  39. }
  40. })
  41. return Promise.resolve(resp.data.access_token);
  42. } catch (err) {
  43. console.error(err)
  44. return Promise.reject(err)
  45. }
  46. }
  47. const getPlaylistTracks = async (playlist, token) =&gt; {
  48. try {
  49. let next = 1
  50. const tracks = []
  51. url = `https://api.spotify.com/v1/playlists/${playlist}`
  52. while (next != null) {
  53. const resp = await axios.get(
  54. url,
  55. config = {
  56. headers: {
  57. &#39;Accept-Encoding&#39;: &#39;application/json&#39;,
  58. &#39;Authorization&#39;: `Bearer ${token}`,
  59. }
  60. }
  61. )
  62. items = []
  63. if (resp.data.items) {
  64. items = resp.data.items
  65. } else if (resp.data.tracks.items) {
  66. items = resp.data.tracks.items
  67. }
  68. for(const item of items) {
  69. if (item.track?.name != null) {
  70. tracks.push({
  71. name: item.track.name,
  72. external_urls: item.track.external_urls.spotify,
  73. uri: item.track.uri,
  74. new: false
  75. })
  76. }
  77. }
  78. if (resp.data.items) {
  79. url = resp.data.next
  80. } else if (resp.data.tracks.items) {
  81. url = resp.data.tracks.next
  82. } else {
  83. break
  84. }
  85. next = url
  86. }
  87. return Promise.resolve(tracks)
  88. } catch (err) {
  89. console.error(err)
  90. return Promise.reject(err)
  91. }
  92. }
  93. app.get(&quot;/callback&quot;, async (request, response) =&gt; {
  94. const code = request.query[&quot;code&quot;]
  95. getToken(code)
  96. .then(access_token =&gt; {
  97. getPlaylistTracks(PLAYLIST_ID, access_token)
  98. .then(tracks =&gt; {
  99. return response.send({
  100. &#39;total:&#39; : tracks.length,
  101. &#39;playlist tracks&#39;: tracks
  102. });
  103. })
  104. })
  105. .catch(error =&gt; {
  106. console.log(error.message);
  107. })
  108. })
  109. app.listen(PORT, () =&gt; {
  110. console.log(`Listening on :${PORT}`)
  111. })

huangapple
  • 本文由 发表于 2023年7月4日 20:25:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/76612617.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定