Search code examples
javascriptnode.jsaxiosspotify

Chaining mapped axios/API calls


For the API I'm using, I have to bounce off 3 separate endpoints to get the data I need. I'm stuck on the very last endpoint and it's driving me crazy. Here's the gist of what I'm currently doing (or attempting to do).

  1. Make direct call to ENDPOINT 1. Process data through map (to return data I need specifically) then push to ARRAY 1.
  2. Once ARRAY 1 is done processing, I map ARRAY 1's data to make an API call for each of it's IDs to ENDPOINT 2, then push this to ARRAY 2.
  3. Once ARRAY 2 is done processing, I map ARRAY 2's data to make an API call for each of it's IDs to ENDPOINT 3, then push this to ARRAY 3.
  4. All of these steps are wrapped in a promise that resolves with the 3 completed arrays.

Steps 1 and 2 get done just fine, but I have tried a million different things for step 3 and it keeps returning . What would be the best way for this to be handled? Any help would be MUCH appreciated!

router.get("/", (req, res) => {
  let artists = [];
  let albums = [];
  let tracks = [];

  const options = {
    headers: {
      'Authorization': `Bearer ${token}`,
    },
  };

  function getArtists(url) {
    return new Promise(resolve => {
      axios.get(url, options).then(response => {
        artists.push(...response.data.artists.items.map(artist => ({
          url: artist.external_urls.spotify,
          name: artist.name,
          images: artist.images,
          id: artist.id,
          genres: artist.genres,
        })));
        let next = response.data.artists.next;
        if (next !== null) {
          getArtists(next);
        } else {
          resolve(getAlbums().then(() => getTracks().then(() => res.send({artists, albums, tracks}))));
        };
      });
    });
  };

  let getAlbums = () => {
    return new Promise(resolve => {
      const requests = artists.map(item => {
        return axios.get(`https://api.spotify.com/v1/artists/${item.id}/albums?market=us&include_groups=single,appears_on`, options).then(response => {
          albums.push(...response.data.items);
        });
      });
      Promise.all(requests).then(() => {
        const filtered = albums.filter((curr, index, self) => self.findIndex(t => t.id === curr.id) === index);
        const sorted = filtered.sort((a, b) => (b.release_date > a.release_date) ? 1 : -1); // change data to filtered to filter duplicates
        const sliced = sorted.slice(0, 50);
        albums = sliced;
        // res.send({artists, albums});
        resolve();
      });
    });
  };

  let getTracks = () => {
    albums.map(item => {
      return axios.get(`https://api.spotify.com/v1/albums/${item.id}/tracks`, options).then(response => {
        tracks.push(...response.data.items);
      });
    });
  };

  if (token) {
    const url = 'https://api.spotify.com/v1/me/following?type=artist&limit=50';
    getArtists(url);
  } else {
    res.send({
      message: 'Please login to retrieve data',
    });
  };
});

Solution

  • I think you might be better to follow a simpler approach, using async/await. This allows us to structure the code in an easier to follow way. If we have loads of .then() and new Promises etc, it gets very confusing, very quickly!

    I've restructured like so, I think this is easier to follow and hopefully to debug!

    We can loop over each item in the getXXX functions, or we can use Promise.all, either approach works, though the latter may be more performant.

    I've used this in getTracks()

    router.get("/", async (req, res) => {
        let options = {
            headers: {
            'Authorization': `Bearer ${token}`,
            }
        }
    
        if (!token) {
            res.send({
                message: 'Please login to retrieve data',
            });
            return;
        }
    
        const url = 'https://api.spotify.com/v1/me/following?type=artist&limit=50';
        let artists = await getArtists(url, options);
        console.log ("Artists (length):", artists.length );
        let albums = await getAlbums(artists, options);
        console.log ("Albums (length):", albums.length );
        let tracks = await getTracks(albums, options);
        console.log("Tracks (length):", tracks.length);
        res.send( { albums, artists, tracks } );   
    }
    
    async function getArtists(url, options, maxLoopCount = 100) {
    
        let artists = [];
        let count = 0;
        do {
            console.log(`getArtists: Page #${++count}...`);
            let artistResp = await getArtistsPage(url, options);
            artists.push(...artistResp.artistList);
            url = artistResp.next;
        } while (url && count < maxLoopCount) ;
        return artists;
    }
    
    async function getArtistsPage(url, options) {
    
        let response = await axios.get(url, options);
    
        let artistList = response.data.artists.items.map(artist => ({
            url: artist.external_urls.spotify,
            name: artist.name,
            images: artist.images,
            id: artist.id,
            genres: artist.genres,
        }));
    
        let next = response.data.artists.next;
        return { artistList, next}
    };
    
    async function getAlbums(artists, options, sliceCount = 50) {
        let albums = [];
    
        for(let artist of artists) {
            let response = await axios.get(`https://api.spotify.com/v1/artists/${artist.id}/albums?market=us&include_groups=single,appears_on`, options);
            albums.push(...response.data.items);
        }
    
        const filtered = albums.filter((curr, index, self) => self.findIndex(t => t.id === curr.id) === index);
        const sorted = filtered.sort((a, b) => (b.release_date > a.release_date) ? 1 : -1); // change data to filtered to filter duplicates
        const sliced = sorted.slice(0, sliceCount);
        return sliced;
    }
    
    async function getTracks(albums, options) {
        let promises = albums.map(album => axios.get(`https://api.spotify.com/v1/albums/${album.id}/tracks`, options));
        let responseList = await Promise.all(promises);
        return responseList.map(response => response.data.items).flat();
    }