Jammming Project - Error When Saving Playlist

I am close to finishing the Jammming project and most everything seems to be working as expected. However, when I save a playlist, it doesn’t save to my Spotify account. When I look in the console, I see the below:

Here is my Spotify.js code:

const clientId = '[Redacted]';
const redirectUri = 'http://localhost:3000/';

let accessToken;

const Spotify = {
  getAccessToken() {
    if (accessToken) {
      return accessToken;
    }

    // check for access token match
    const accessTokenMatch = window.location.href.match(/access_token=([^&]*)/);
    const expiresInMatch = window.location.href.match(/expires_in=([^&]*)/);

    if (accessTokenMatch && expiresInMatch) {
      accessToken = accessTokenMatch[1];
      const expiresIn = Number(expiresInMatch[1]);
      //This clears the parameters, allowing us to grab a new access token when it expires
      window.setTimeout(() => accessToken = '', expiresIn * 1000);
      window.history.pushState('Access Token', null, '/');
      return accessToken;
    } else {
      const accessUrl = `https://accounts.spotify.com/authorize?client_id=${clientId}&response_type=token&scope=playlist-modify-public&redirect_uri=${redirectUri}`;
      window.location = accessUrl;
    }
  },

  search(term) {
    const accessToken = Spotify.getAccessToken();
    return fetch(`https://api.spotify.com/v1/search?type=track&q=${term}`, {
      headers: {Authorization: `Bearer ${accessToken}`}
    }).then(response => {
      return response.json();
    }).then(jsonResponse => {
      if (!jsonResponse.tracks) {
        return [];
      }
      return jsonResponse.tracks.items.map(track => ({
        id: track.id,
        name: track.name,
        artist: track.artists[0].name,
        album: track.album.name,
        uri: track.uri
      }));
    });
  },

  savePlaylist(name, trackUris) {
    if (!name || !trackUris.length) {
      return;
    }

    const accessToken = Spotify.getAccessToken();
    const headers = {Authorization: `Bearer ${accessToken}`};
    let userId;

    return fetch('https://api.spotify.com/v1/me', {headers: headers}
    ).then(response => response.json()
    ).then(jsonResponse => {
      userId = jsonResponse.id;
      return fetch(`https://cors-anywhere.herokuapp.com/http://api.spotify.com/v1/users/${userId}/playlists`,
        {
          headers: headers,
          method: 'POST',
          body: JSON.stringify({name: name})
      }).then(response => response.json()
      ).then(jsonResponse => {
        const playlistId = jsonResponse.id;
        return fetch(`https://api.spotify.com/v1/playlists/${playlistId}/tracks`, {
          headers: headers,
          method: 'POST',
          body: JSON.stringify({uris: trackUris})
      })
    })
  });
  }
}

export default Spotify;

Note that I included https://cors-anywhere.herokuapp.com/ in the second fetch() request because I was getting a CORS error in addition to the 404 error. Don’t think that is what the issue is. Also note that I saw here that the project uses outdated POST formatting so I tried the abbreviated address mentioned in that post, but it didn’t help.

It seems like the issue is that the playlist ID is not being saved to the playlistId variable because in the error it has “undefined” where the ID should be, but I’m not sure why that is happening or how to fix it. Anyone else experienced this/know why it is happening?

What do you see if you console log playlistId just after this line?

const playlistId = jsonResponse.id;

Undefined. It seems like maybe there is some issue with the jsonResponse not containing a playlist id, but I’m not sure why that would be happening. I tried going through the walkthrough video and copying the code step by step but it didn’t seem to help.

console log the values above as well: response, response.json(), and jsonResponse
See what comes back. You should be able to look through the jsonResponse and see the properties

Thanks for the responses. When I logged response.json(), I got an error that said “body stream is locked”. According to this, that happens when you use response.json() more than once. However, the solutions suggested in that article didn’t fix the problem. I got the below when I logged jsonResponse (sorry for the black bars, trying to avoid posting my name on the forum):

This is what I see when I expand one of the items:

Is the jsonResponse.tracks working for you in the ‘search’ function just above this savePlayList function?

From the object you posted, is there another property showing a User ID? I see the IDs connected to each song but I can’t see past those. I’m curious to know if that property name has changed from ‘id’ to ‘userID’ for example and that’s what is throwing the code off.

jsonResponse.tracks seems to be working from what I can tell. When I log it to the console it seems fine. When I look through the jsonResponse object I posted, the only ID property I see is for each individual song like you noted.

Hi

Upping this thread since I seem to be running through the exact same issue, and my playlistID also comes back as undefined…
As extra info about the behavior I can add that:
1/I’ve tried to substitute my Spotify.savePlaylist method with the one provided in the solution code, but still got the same issue.
2/I’ve tried to substitute my whole Spotify.js with the one provided in the solution code but had an issue saying I’m missing client_id on clicking on search (Error I didn’t have in my code…).
3/When I use my code, I can look for song, add them to the right column with the “+” button, change the name of the playlist, but as soon as I hit the Save Playlist button, it wipes the tracks in the playlist column, and:
a/Doesn’t reset the playlist name as expected
b/Doesn’t create or save anything to my Spotify account

which is very puzzling, since it seems the function is called but somehow doesn’t execute as expected…

Here is my code for app.js and Spotify.js

app.js :

import React from 'react';
import './App.css';
import SearchBar from '../SearchBar/SearchBar';
import SearchResults from '../SearchResults/SearchResults';
import Playlist from '../Playlist/Playlist';
import Spotify from '../../util/Spotify';

class App extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      searchResults:[],
      playlistName: 'New Playlist',
      playlistTracks: []
    };
    this.addTrack = this.addTrack.bind(this);
    this.removeTrack = this.removeTrack.bind(this);
    this.updatePlaylistName = this.updatePlaylistName.bind(this);
    this.savePlaylist = this.savePlaylist.bind(this);
    this.search = this.search.bind(this);
  }
  search(term){
    Spotify.search(term).then(searchResult=>this.setState({searchResults: searchResult})
  )};

  savePlaylist(){
  let trackURIs=this.state.playlistTracks.map(x=>x.uri);
Spotify.savePlaylist(this.state.playlistName,trackURIs).then(()=>
{this.setState({
  playlistName: 'New Playlist',
  playlistTracks: []}
  )}
  );
  };

  addTrack(track){
    if(this.state.playlistTracks.find(savedTrack => savedTrack.id === track.id)){
        return;
      }else{this.state.playlistTracks.push(track);
        this.setState({playlistTracks: this.state.playlistTracks})};
      };
  
  removeTrack(track){
    let i=0;
    for(i=0;i<this.state.playlistTracks.length;i++){    
    if(this.state.playlistTracks[i].id !== track.id){
      return
    }else{
      let newPlaylist = this.state.playlistTracks.filter(bob => bob.id !== track.id);
      this.setState({playlistTracks: newPlaylist});
          }
        }
      };

  updatePlaylistName(name){
    this.setState({playlistName: name})
              };

  render(){
    return(<div>
    <h1>Ja<span className="highlight">mmm</span>ing</h1>
    <div className="App">
      <SearchBar onSearch={this.search}/>
      <div className="App-playlist">
        <SearchResults searchResults={this.state.searchResults} onAdd={this.addTrack}/>
        <Playlist onSave={this.savePlaylist} onNameChange={this.updatePlaylistName} playlistName={this.state.playlistName} playlistTracks={this.state.playlistTracks} onRemove={this.removeTrack} />
      </div>
    </div>
  </div>);
  };
}

export default App;

And for Spotify.js


const clientID = *placeholder*;
const queryUrl = 'https://accounts.spotify.com/authorize';
const redirectUri = 'http://localhost:3000/';
let accessToken;
const finalQ = `${queryUrl}?client_id=${clientID}&redirect_uri=${redirectUri}&response_type=token`;

const Spotify = {
  getAccessToken(){
      if(accessToken){return accessToken}else{
    const accesto = window.location.href.match(/access_token=([^&]*)/);
    const expin = window.location.href.match(/expires_in=([^&]*)/);
    if(accesto && expin){
        accessToken = accesto[1];
        const expiresIn = Number(expin[1]);
        window.setTimeout(() => accessToken = '', expiresIn * 1000);
        window.history.pushState('Access Token', null, '/');
return accessToken;
    }else{
    window.location = finalQ;
    };
};

},

search(term){
   let acctok = this.getAccessToken();
   return fetch('https://api.spotify.com/v1/search?type=track&q='+term, {headers: {Authorization: `Bearer ${acctok}`}}).then(
           Response => Response.json()).then(
           jsonResponse => {return jsonResponse.tracks.items.map(track =>(
        {
        id: track.id,
        name: track.name,
        artist: track.artists[0].name,
        album: track.album.name,
        uri: track.uri
        }
        )
        );
    });
},

savePlaylist(playName,trackUri){
const accessToken= Spotify.getAccessToken();
if (!playName || !trackUri.length) {
    return;
  }
    let header = {Authorization: `Bearer ${accessToken}`};
    let userID = '';
    return fetch('https://api.spotify.com/v1/me',{headers: header}).then(
    Response => Response.json()).then(jsonResponse => {
    userID = jsonResponse.id;
    console.log(userID);
    return fetch('https://api.spotify.com/v1/users/'+userID+'/playlists',{
        headers: header,
        method:'POST',
        body: JSON.stringify({name: playName})
    }).then(postPl => postPl.json()).then(playlistres =>
        {
        let playlistID = playlistres.id;
        console.log(playlistID);
        fetch('https://api.spotify.com/v1/users/'+userID+'playlists/'+playlistID+'/tracks',{
        headers: header,
        method:'POST',
        body: JSON.stringify({uris: trackUri})
    });
})})
    
}
};



export default Spotify;

Would really appreciate any help.

What do the errors in the console say?

I keep thinking to wrap this function in a try/catch block so you can see what the error is. It may be an authentication error but would be good to confirm that.

I got a 404 error, which I guess makes sense given that it seems like the problem is that the playlist id is undefined in the jsonResponse object.

I did this course a couple of years ago. I got it working and the playlists would save in my Spotify account. here’s my code so you can see if it helps. Also clientId should be initialized to an empty string rather than placeholder.

const Spotify = {

  // Retrieves an access token from Spotify API to authenticate requests and retrieve data
  getAccessToken: function() {
    if (userAccessToken) {
      return userAccessToken;
    }

      const accessTokenValue = window.location.href.match(/access_token=([^&]*)/);
      const expireTimeValue = window.location.href.match(/expires_in=([^&]*)/);

        if (accessTokenValue && expireTimeValue) {
          userAccessToken = accessTokenValue[1];
          const expiresIn = Number(expireTimeValue[1]);
          window.setTimeout(() => userAccessToken = '', expiresIn * 1000);
          window.history.pushState('Access Token', null, '/');
        } else {
          window.location = `${url}?client_id=${clientId}&response_type=token&scope=playlist-modify-public&redirect_uri=${redirectURI}`;
        }
  },

  search: function(term) {
    const accessToken = Spotify.getAccessToken();
      return fetch(`https://api.spotify.com/v1/search?type=track&q=${term}`, {
        headers: {
            Authorization: `Bearer ${accessToken}`
        }
      }).then(response => {
        return response.json();
      }).then(jsonResponse => {
        if (!jsonResponse.tracks) {
        return [];
      }
      console.log(jsonResponse.tracks);
      return jsonResponse.tracks.items.map(track => ({
        id: track.id,
        name: track.name,
        artist: track.artists[0].name,
        album: track.album.name,
        image: track.album.images[2].url,
        preview: track.preview_url,
        uri: track.uri
      }));
     });
   },

   savePlaylist: function(listName, trackUris) {
     if ( !listName || !trackUris.length ) {
        return;
       }

       const accessToken = Spotify.getAccessToken();
       const headers = { Authorization: `Bearer ${accessToken}`}
       let userID;

       return fetch('https://api.spotify.com/v1/me', {headers: headers}
       ).then(response => response.json()
       ).then(jsonResponse => {
         userID = jsonResponse.id;
         return fetch(`https://api.spotify.com/v1/users/${userID}/playlists`,{
         method: 'POST',
         headers: headers,
         body: JSON.stringify({ name: listName })
         }).then(response =>  response.json()
         ).then(jsonResponse => {
           const playlistID = jsonResponse.id;
            return fetch(`https://api.spotify.com/v1/users/${userID}/playlists/${playlistID}/tracks`,{
           method: 'POST',
           headers: headers,
           body: JSON.stringify({ uris: trackUris })
         });
       });
     });
   } // end of savePlaylist method
} // end of Spotify object


export default Spotify;
1 Like

Thanks for taking the time to help!
Just to clarify, the “placeholder” in my code is well replaced with my client ID. Same as the code I pasted below, the clientID in the URL stands for my actual client ID, just didnt want to paste it here.

I’ll see what I can do with your code.
Meanwhile, here are the 2 errors I’m getting:

this one appearing at line 54 of my Spotify.js

POST https://api.spotify.com/v1/users/clientID/playlists 403
(anonymous) @ Spotify.js:54
Promise.then (async)
savePlaylist @ Spotify.js:50
savePlaylist @ App.js:28
callCallback @ react-dom.development.js:188
invokeGuardedCallbackDev @ react-dom.development.js:237
invokeGuardedCallback @ react-dom.development.js:292
invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:306
executeDispatch @ react-dom.development.js:389
executeDispatchesInOrder @ react-dom.development.js:414
executeDispatchesAndRelease @ react-dom.development.js:3278
executeDispatchesAndReleaseTopLevel @ react-dom.development.js:3287
forEachAccumulated @ react-dom.development.js:3259
runEventsInBatch @ react-dom.development.js:3304
runExtractedPluginEventsInBatch @ react-dom.development.js:3514
handleTopLevel @ react-dom.development.js:3558
batchedEventUpdates$1 @ react-dom.development.js:21871
batchedEventUpdates @ react-dom.development.js:795
dispatchEventForLegacyPluginEventSystem @ react-dom.development.js:3568
attemptToDispatchEvent @ react-dom.development.js:4267
dispatchEvent @ react-dom.development.js:4189
unstable_runWithPriority @ scheduler.development.js:653
runWithPriority$1 @ react-dom.development.js:11039
discreteUpdates$1 @ react-dom.development.js:21887
discreteUpdates @ react-dom.development.js:806
dispatchDiscreteEvent @ react-dom.development.js:4168

this one occurring at line 62 of the same file.

POST https://api.spotify.com/v1/users/clientID/playlists/undefined/tracks 404
(anonymous) @ Spotify.js:62
Promise.then (async)
(anonymous) @ Spotify.js:54
Promise.then (async)
savePlaylist @ Spotify.js:50
savePlaylist @ App.js:28
callCallback @ react-dom.development.js:188
invokeGuardedCallbackDev @ react-dom.development.js:237
invokeGuardedCallback @ react-dom.development.js:292
invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:306
executeDispatch @ react-dom.development.js:389
executeDispatchesInOrder @ react-dom.development.js:414
executeDispatchesAndRelease @ react-dom.development.js:3278
executeDispatchesAndReleaseTopLevel @ react-dom.development.js:3287
forEachAccumulated @ react-dom.development.js:3259
runEventsInBatch @ react-dom.development.js:3304
runExtractedPluginEventsInBatch @ react-dom.development.js:3514
handleTopLevel @ react-dom.development.js:3558
batchedEventUpdates$1 @ react-dom.development.js:21871
batchedEventUpdates @ react-dom.development.js:795
dispatchEventForLegacyPluginEventSystem @ react-dom.development.js:3568
attemptToDispatchEvent @ react-dom.development.js:4267
dispatchEvent @ react-dom.development.js:4189
unstable_runWithPriority @ scheduler.development.js:653
runWithPriority$1 @ react-dom.development.js:11039
discreteUpdates$1 @ react-dom.development.js:21887
discreteUpdates @ react-dom.development.js:806
dispatchDiscreteEvent @ react-dom.development.js:4168

Interesting update:

I’ve replaced my Spotify Object with yours, and it worked!!!..Partially :slight_smile:

The playlist is saved into my spotify account and the tracks are there, BUT, the playlist name is not reset to ‘New Playlist’ (Also the tracks still get wiped from the playlist column).

I’ll look further into our code differences and try to spot what caused this.

EDIT:

After looking further and doing a few edits, it seems the main cause of the error were the following for me:
1/My accessUrl was wrong:
``{queryUrl}?client_id={clientID}&redirect_uri=${redirectUri}&response_type=token;

instead of

``{url}?client_id={clientId}&response_type=token&scope=playlist-modify-public&redirect_uri=${redirectURI};

But that is weird, cause even with that, I managed to get results from the search function before I update to the right URL… I guess this worked somehow for GET request but not POST??

However now I get an even weirder behavior:
If I look for artists, songs etc…I have searched in the past using this app, proper search results come out, and then I’m also able to look for never-searched-before artists/songs. BUT! If I refresh the page and try to look for a new song/artist with a keyword I’ve never used before, I get a “Unhandled Rejection (TypeError): Cannot read property ‘items’ of undefined” caused by my search function, and when logging the result of the fetch() in the search function, I get:

  1. [[PromiseStatus]]: “resolved”
  2. [[PromiseValue]]: Object
  3. error:
    1. status: 401
    2. message: “Invalid access token”

So it seems there’s a problem with how the access token is being handled, and maybe not reset/created properly in some patterns? I find it weird that I can look for past looked for artist, and that when I do so, it kinda open the door for me to look for never-looked-before artists without creating an error.

Here’s my edited Spotify Object for reference:

const clientID = placeholder;
const queryUrl = 'https://accounts.spotify.com/authorize';
const redirectUri = 'http://localhost:3000/';
let accessToken;
const finalQ = `${queryUrl}?client_id=${clientID}&response_type=token&scope=playlist-modify-public&redirect_uri=${redirectUri}`;


const Spotify = {
  getAccessToken: function(){
      if(accessToken){return accessToken;}
    const accesto = window.location.href.match(/access_token=([^&]*)/);
    const expin = window.location.href.match(/expires_in=([^&]*)/);
    if(accesto && expin){
        accessToken = accesto[1];
        const expiresIn = Number(expin[1]);
        window.setTimeout(() => accessToken = '', expiresIn * 1000);
        window.history.pushState('Access Token', null, '/');} else {window.location = finalQ;}
},

search: function(term){
   let userAccessToken = Spotify.getAccessToken();
   return fetch(`https://api.spotify.com/v1/search?type=track&q=${term}`, {headers: {Authorization: `Bearer ${userAccessToken}`}}).then(
           Response => {
               let searchid= Response.json();
               console.log (searchid);
               return searchid;}).then(
           jsonResponse => {return jsonResponse.tracks.items.map(track =>(
        {
        id: track.id,
        name: track.name,
        artist: track.artists[0].name,
        album: track.album.name,
        uri: track.uri
        }
        )
        );
    });
},

savePlaylist: function(playName,trackUri){
const userAccessToken= Spotify.getAccessToken();
if (!playName || !trackUri.length) {
    return;
  }
    let headers = {Authorization: `Bearer ${userAccessToken}`};
    let userID = '';
    return fetch('https://api.spotify.com/v1/me',{headers: headers}).then(
    Response => Response.json()).then(jsonResponse => {
    userID = jsonResponse.id;
    console.log(userID);
    return fetch('https://api.spotify.com/v1/users/'+userID+'/playlists',{
        method:'POST',
        headers: headers,
        body: JSON.stringify({name: playName})
    }).then(postPl => postPl.json()).then(playlistres =>
        {
        const playlistID = playlistres.id;
        console.log(playlistID);
        return fetch('https://api.spotify.com/v1/users/'+userID+'/playlists/'+playlistID+'/tracks',{
        method:'POST',
        headers: headers,
        body: JSON.stringify({uris: trackUri})
    });
})})
    
}
};



export default Spotify;

That is really strange! I went back and looked at my code in App.js to if it may have to do with setting State but it’s really straight-forward.

What does this line look like in the console? How long do they give you before it expires?

const expiresIn = Number(expin[1]);

Thanks for the reply!

It gives me 3600, as expected I guess.

EDIT: it’s getting weirder and weirder.
I’ve tried to paste back my original code (so that I could try to re-edit it again step by step and try to isolate the part that doesn’t work properly), but by doing so, my past code who would not let me save a playlist, now does,BUT! The playlist in my spotify is empty (even though the name is correct).
It really feels like the access token is having some weird behavior…It feels like once it worked once, it must have saved something somewhere, allowing to save playlist now… My search function now works differently as well. I’m not getting the spotify login page anymore, but instead I get the access token instantly through the updated url. And when I try to hit search with a blank field after obtaining the token, I get those 2 errors:

GET https://api.spotify.com/v1/search?type=track&q= 400
search @ Spotify.js:28
search @ App.js:23
search @ SearchBar.js:14
callCallback @ react-dom.development.js:188
invokeGuardedCallbackDev @ react-dom.development.js:237
invokeGuardedCallback @ react-dom.development.js:292
invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:306
executeDispatch @ react-dom.development.js:389
executeDispatchesInOrder @ react-dom.development.js:414
executeDispatchesAndRelease @ react-dom.development.js:3278
executeDispatchesAndReleaseTopLevel @ react-dom.development.js:3287
forEachAccumulated @ react-dom.development.js:3259
runEventsInBatch @ react-dom.development.js:3304
runExtractedPluginEventsInBatch @ react-dom.development.js:3514
handleTopLevel @ react-dom.development.js:3558
batchedEventUpdates$1 @ react-dom.development.js:21871
batchedEventUpdates @ react-dom.development.js:795
dispatchEventForLegacyPluginEventSystem @ react-dom.development.js:3568
attemptToDispatchEvent @ react-dom.development.js:4267
dispatchEvent @ react-dom.development.js:4189
unstable_runWithPriority @ scheduler.development.js:653
runWithPriority$1 @ react-dom.development.js:11039
discreteUpdates$1 @ react-dom.development.js:21887
discreteUpdates @ react-dom.development.js:806
dispatchDiscreteEvent @ react-dom.development.js:4168
Uncaught (in promise) TypeError: Cannot read property 'items' of undefined
    at Spotify.js:30
(anonymous) @ Spotify.js:30
Promise.then (async)
search @ App.js:23
search @ SearchBar.js:14
callCallback @ react-dom.development.js:188
invokeGuardedCallbackDev @ react-dom.development.js:237
invokeGuardedCallback @ react-dom.development.js:292
invokeGuardedCallbackAndCatchFirstError @ react-dom.development.js:306
executeDispatch @ react-dom.development.js:389
executeDispatchesInOrder @ react-dom.development.js:414
executeDispatchesAndRelease @ react-dom.development.js:3278
executeDispatchesAndReleaseTopLevel @ react-dom.development.js:3287
forEachAccumulated @ react-dom.development.js:3259
runEventsInBatch @ react-dom.development.js:3304
runExtractedPluginEventsInBatch @ react-dom.development.js:3514
handleTopLevel @ react-dom.development.js:3558
batchedEventUpdates$1 @ react-dom.development.js:21871
batchedEventUpdates @ react-dom.development.js:795
dispatchEventForLegacyPluginEventSystem @ react-dom.development.js:3568
attemptToDispatchEvent @ react-dom.development.js:4267
dispatchEvent @ react-dom.development.js:4189
unstable_runWithPriority @ scheduler.development.js:653
runWithPriority$1 @ react-dom.development.js:11039
discreteUpdates$1 @ react-dom.development.js:21887
discreteUpdates @ react-dom.development.js:806
dispatchDiscreteEvent @ react-dom.development.js:4168

This worked for me as well. I’m not 100% sure why, but it does seem like it may have something to do with the access URL. I didn’t originally include a response type or a scope, so maybe that was the source of my problem. Really appreciate all the help!

Hi there,
Did you manage to solve the issues you were having with the Jammming project?
I seem to be nearly there - I have got it to the stage that playlists are saving - but when saved the playlist name doesn’t change back to New Playlist. I think that was one of the issues you were having - did you get that bit working properly?
I had to have a break from it for a week or so as I had been boggling at it for too long!!