Search code examples
javascriptnode.jscallbackspotifyglitch-framework

Spotify Web API: Receiving TypeError: callback is not a function


I'm creating a node.js web app that uses the Spotify Web API and the OpenAI API. The server side code basically takes a prompt from user, processes it with OpenAI language model, then fetches tracks from Spotify and builds a playlist. The app successfully takes the prompt, processes, and fetches the track (I can see the tracks being fetched in my console), but after that I am getting a TypeError: callback is not a function. It appears that the error is being thrown by the http-manager.js file inside the Spotify Web API.

I don't think I even referenced a callback in my code, could this be an issue with Spotify's API/the way I'm trying to interact with it?

This is the exact error in my console when testing the app:

TypeError: callback is not a function
    at /rbd/pnpm-volume/bfe9bf90-d68c-4e41-8b2a-95cefb846cfe/node_modules/spotify-web-api-node/src/http-manager.js:71:16
    at Request.callback (/rbd/pnpm-volume/bfe9bf90-d68c-4e41-8b2a-95cefb846cfe/node_modules/spotify-web-api-node/node_modules/superagent/lib/node/index.js:905:3)
    at /rbd/pnpm-volume/bfe9bf90-d68c-4e41-8b2a-95cefb846cfe/node_modules/spotify-web-api-node/node_modules/superagent/lib/node/index.js:1127:20
    at IncomingMessage.<anonymous> (/rbd/pnpm-volume/bfe9bf90-d68c-4e41-8b2a-95cefb846cfe/node_modules/spotify-web-api-node/node_modules/superagent/lib/node/parsers/json.js:22:7)
    at Stream.emit (events.js:400:28)
    at Unzip.<anonymous> (/rbd/pnpm-volume/bfe9bf90-d68c-4e41-8b2a-95cefb846cfe/node_modules/spotify-web-api-node/node_modules/superagent/lib/node/unzip.js:53:12)
    at Unzip.emit (events.js:400:28)
    at endReadableNT (internal/streams/readable.js:1334:12)
    at processTicksAndRejections (internal/process/task_queues.js:82:21)

server side code:

//variables and imports

//openai api config code....

//fastify configuration code....

// Spotify configuration
const SPOTIFY_CLIENT_ID = process.env.SPOTIFY_CLIENT_ID;
const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET;
const SPOTIFY_REDIRECT_URI = process.env.SPOTIFY_REDIRECT_URI;
const SPOTIFY_AUTH_SCOPES =
  'user-read-private user-read-email playlist-modify-public playlist-modify-private';

const SpotifyWebApi = require('spotify-web-api-node');

const spotifyApi = new SpotifyWebApi({
  clientId: SPOTIFY_CLIENT_ID,
  clientSecret: SPOTIFY_CLIENT_SECRET,
  redirectUri: SPOTIFY_REDIRECT_URI,
});

//search and extract songs from Spotify code...


// Utility function to generate a random string
function generateRandomString(length) {
  const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  let text = "";

  for (let i = 0; i < length; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }

  return text;
}

//search keyword refining code...

//create playlist and add tracks to it
async function createPlaylistAndAddTracks(userId, playlistName, tracks, accessToken) {
  spotifyApi.setAccessToken(accessToken);

  try {
    const playlist = await new Promise((resolve, reject) => {
      spotifyApi.createPlaylist(userId, playlistName, { public: true }, (err, data) => {
        if (err) reject(err);
        resolve(data);
      });
    });

    const playlistId = playlist.body.id;

    const trackUris = tracks.map((track) => track.uri);

    await new Promise((resolve, reject) => {
      spotifyApi.addTracksToPlaylist(playlistId, trackUris, (err, data) => {
        if (err) reject(err);
        resolve(data);
      });
    });

    return playlistId;
  } catch (error) {
    console.error("Error creating playlist and adding tracks:", error);
    throw error;
  }
}

// Routes
// "/" get route

// "/" post route

fastify.get('/login', (req, reply) => {
  const state = generateRandomString(16);
  reply.setCookie("spotify_auth_state", state, {
    path: "/",
    maxAge: 3600, // 1 hour
    httpOnly: true,
  });

  const authUrl =
    'https://accounts.spotify.com/authorize' +
    '?response_type=code' +
    '&client_id=' + encodeURIComponent(SPOTIFY_CLIENT_ID) +
    '&scope=' + encodeURIComponent(SPOTIFY_AUTH_SCOPES) +
    '&redirect_uri=' + encodeURIComponent(SPOTIFY_REDIRECT_URI) +
    '&state=' + state;
  reply.redirect(authUrl);
});

// "user" get route code...

//"jockey" route for processing prompts and interacting with Spotify API
fastify.get('/jockey', function (request, reply) {
  return reply.view('/src/pages/jockey.hbs');
});

//taking user input and generating keywords for use in SpotifyAPI
fastify.post("/jockey", async function (request, reply) {
  const prompt = request.body.prompt;
  const promptWithInstruction = `We have a user who wants to listen to music related to the theme: "${prompt}". Can you provide a comma-separated list of keywords or phrases that are relevant to this theme and could be used to search for music on Spotify?`;


  try {
    const result = await openai.createCompletion({
      model: "text-davinci-003",
      prompt: promptWithInstruction,
      max_tokens: 2048,
      temperature: 0.8,
    });

    const generatedText = result.data.choices[0].text.trim();
    const keywords = extractKeywords(generatedText);
    console.log("Generated Keywords:", keywords); //MILKSTEAK
    
    const tracks = await searchAndExtractTracks(keywords, request.cookies.access_token);
    console.log("Extracted tracks:", tracks);
    
    // Get the user's ID
    const userResponse = await spotifyApi.getMe();
    const userId = userResponse.body.id;


    // Create a new playlist and add the fetched tracks
    const playlistId = await createPlaylistAndAddTracks(userId, request.cookies.access_token, tracks);


    // Redirect to the /jockey page after processing the input
    return reply.redirect("/jockey");

  } catch (error) {
    console.error(error);
    return reply.code(500).send("Error generating response from OpenAI API");
  }
});



fastify.get('/callback', async (req, reply) => {
  const code = req.query.code;
  const state = req.query.state;
  const storedState = req.cookies.spotify_auth_state;

  if (state === null || state !== storedState) {
    reply.code(400).send('State mismatch');
  } else {
    reply.clearCookie("spotify_auth_state");

    const tokenUrl = 'https://accounts.spotify.com/api/token';

    try {
      const response = await request.post(tokenUrl, {
        form: {
          code: code,
          redirect_uri: SPOTIFY_REDIRECT_URI,
          grant_type: 'authorization_code',
        },
        headers: {
          Authorization:
            'Basic ' +
            Buffer.from(SPOTIFY_CLIENT_ID + ':' + SPOTIFY_CLIENT_SECRET).toString('base64'),
        },
        json: true,
      });

      // Save the access_token and refresh_token in cookies
      const accessToken = response.access_token;
      const refreshToken = response.refresh_token;

      reply.setCookie("access_token", accessToken, {
        path: "/",
        maxAge: 3600, // 1 hour
        httpOnly: true,
      });

      reply.setCookie("refresh_token", refreshToken, {
        path: "/",
        maxAge: 30 * 24 * 60 * 60, // 30 days
        httpOnly: true,
      });

      reply.redirect('/jockey');

    } catch (error) {
      reply.code(400).send('Error: ' + error.message);
    }
  }
});

// let user logout and clear cookies/session "/logout" route


//fastify.listen code...

Solution

  • Try calling createPlaylist without the userId argument. Explanation below.
    In the changelog of the version 5.0.0 it is said

    Create Playlist (createPlaylist) method no longer accepts a userId string as its first argument.

    The new definition for that function is createPlaylist(name, options, callback) and you you are calling it with the userId param, createPlaylist(userId, playlistName, { public: true }, (err, data)), which means that the function is interpreting your userId argument as name, playlistName as options, {public: true} as the callback, and your (err,data)=>{} callback will of course be ignored, so when that callback will be needed, it will try to execute your {public: true}() argument thinking that its the callback which of course raises the error stating that callback is not a function because {public: true} is indeed not a function.