I get a 400 error code when I try making an API request to add to my liked songs. I do not understand why I am getting it, as I am feeding the API request the track.id as the documentation suggests I do:
This is what I logged to the console.error:
“{“error”: {“status”: 400, “message”: “Missing required field: id”
This is my home.jsx that I have set up to make put requests to add to liked songs on a users’ spotify account once I press the heart button:
import { useDispatch, useSelector } from "react-redux";
import { logout } from "../features/userSlice";
import React, { useEffect, useState, useRef } from "react";
import { useSwipeable } from "react-swipeable";
import { IoPlay, IoPause } from "react-icons/io5";
import { GoHeartFill } from "react-icons/go";
import { IoRemoveCircleOutline } from "react-icons/io5";
import {
fetchSongRecommendations,
songsRecommended,
fetchTopArtists,
topArtistsSelector,
fetchNewReleases,
newReleases,
like,
dislike,
liked,
disliked,
} from "../features/recommendationsSlice";
import { playTrack, pauseTrack } from "../features/playbackSlice";
import { user } from "../features/userSlice";
const Home = () => {
const dispatch = useDispatch();
const topArtists = useSelector(topArtistsSelector);
const myRecommended = useSelector(songsRecommended);
const newSongs = useSelector(newReleases);
const playbackState = useSelector((state) => state.playback);
const profile = useSelector(user);
const likedTracks = useSelector(liked);
const dislikedTracks = useSelector(disliked);
// State to track the current song index
const [currentSongIndex, setCurrentSongIndex] = useState(0);
const currentTrack = myRecommended[currentSongIndex];
// Check if the current track is liked
const isTrackLiked = likedTracks.some((track) => track.id === currentTrack?.id);
const isTrackDisliked = dislikedTracks.some((track) => track.id === currentTrack?.id);
// Audio reference
const audioRef = useRef(null);
useEffect(() => {
dispatch(fetchTopArtists());
}, [dispatch]);
useEffect(() => {
if (topArtists.length > 0) {
dispatch(fetchSongRecommendations());
}
}, [dispatch, topArtists]);
useEffect(() => {
dispatch(fetchNewReleases());
}, [dispatch]);
// Swipe handlers
const swipeHandlers = useSwipeable({
onSwipedLeft: () => handleNextSong(),
onSwipedRight: () => handlePreviousSong(),
preventDefaultTouchmoveEvent: true,
trackMouse: true,
});
// Play or pause track
const handlePlayPause = () => {
if (
playbackState.isPlaying &&
playbackState.currentTrack === currentTrack?.preview_url
) {
dispatch(pauseTrack());
} else {
dispatch(playTrack(currentTrack?.preview_url));
}
};
// Handle track change
const handleNextSong = () => {
if (currentSongIndex < myRecommended.length - 1) {
setCurrentSongIndex((prevIndex) => prevIndex + 1);
}
};
const handlePreviousSong = () => {
if (currentSongIndex > 0) {
setCurrentSongIndex((prevIndex) => prevIndex - 1);
}
};
// Audio handling
useEffect(() => {
if (audioRef.current) {
audioRef.current.pause(); // Pause current audio
}
audioRef.current = new Audio(currentTrack?.preview_url);
if (playbackState.isPlaying) {
audioRef.current.play();
}
return () => {
audioRef.current.pause();
audioRef.current.src = "";
};
}, [currentTrack, playbackState.isPlaying]);
const handleLike = () => {
dispatch(like(currentTrack)); // Dispatch like action
};
const handleDislike = () => {
dispatch(dislike(currentTrack)); // Dispatch dislike action
};
return (
<div className="p-5 grid gap-10">
<h1 className="text-center text-xl">
<span className="font-bold">Welcome</span> {profile?.display_name} to Jammin, your AI-enhanced music assistant!
</h1>
<div className="grid grid-cols-2">
<h2>Song recommendations</h2>
<button className="justify-self-end border rounded-lg p-1">Enhance</button>
</div>
<div
className="border rounded-lg overflow-hidden w-full grid grid-rows-[90%_10%] h-[400px]"
{...swipeHandlers}
>
<div key={currentSongIndex} className="grid justify-items-center">
<img
src={currentTrack?.album.images[1]?.url}
alt={currentTrack?.name}
/>
<p>{currentTrack?.name}</p>
<p className="opacity-50 text-[12px]">
{currentTrack?.artists.map((artist) => artist?.name).join(", ")}
</p>
</div>
<div className="grid grid-cols-3 justify-items-center border-t">
<button onClick={handleDislike}>
<IoRemoveCircleOutline className={`h-full ${isTrackDisliked ? "text-red-500" : ""}`} size={28} />
</button>
<button onClick={handlePlayPause}>
{playbackState.isPlaying && playbackState.currentTrack === currentTrack?.preview_url ? (
<IoPause className="h-full" size={28} />
) : (
<IoPlay className="h-full" size={28} />
)}
</button>
<button onClick={handleLike}>
<GoHeartFill className={`h-full ${isTrackLiked ? "text-green" : ""}`} size={28} />
</button>
</div>
</div>
<div className="grid gap-2">
<h2>New Releases:</h2>
{newSongs?.albums?.items.map((song) => (
<div key={song.id} className="border rounded-lg overflow-hidden grid grid-cols-[20%_80%] gap-2">
<img src={song.images[1].url} alt={song.name} />
<div className="grid content-center">
<p>{song.artists.map((artist) => artist.name).join(", ")}</p>
<p className="opacity-50 text-[12px]">{song.name} - {song.type}</p>
</div>
</div>
))}
</div>
<button onClick={() => dispatch(logout())} className="bg-red-500 text-white px-4 py-2 rounded">
Logout
</button>
</div>
);
};
export default Home;
Below is the recommendationsSlice.jsx where the actual API call has been made. I do not understand why it keeps throwing the 400 error since everything seems in order. Is there something I’m missing?
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import SpotifyWebApi from 'spotify-web-api-js';
const spotifyApi = new SpotifyWebApi();
const initialState = {
tracks: [],
likedTracks: JSON.parse(localStorage.getItem('liked_tracks')) || [],
dislikedTracks: JSON.parse(localStorage.getItem('disliked_tracks')) || [],
topArtists: [],
newReleases: [],
status: 'idle',
error: null,
};
// Fetch song recommendations
export const fetchSongRecommendations = createAsyncThunk(
'recommendations/fetchSongRecommendations',
async (_, { rejectWithValue, getState }) => {
const accessToken = localStorage.getItem('access_token');
if (!accessToken) {
return rejectWithValue('No access token found.');
}
const state = getState();
const topArtists = state.recommendations.topArtists;
if (!topArtists || topArtists.length === 0) {
return rejectWithValue('No top artists available for recommendations.');
}
try {
spotifyApi.setAccessToken(accessToken);
const artistSeeds = topArtists.map((artist) => artist.id).slice(0, 5);
const response = await spotifyApi.getRecommendations({
seed_artists: artistSeeds,
limit: 40,
});
if (!response || !response.tracks) {
return rejectWithValue('Invalid response from Spotify API.');
}
const tracksWithPreviews = response.tracks.filter((track) => track.preview_url);
const limitedTracks = tracksWithPreviews.slice(0, 20);
return limitedTracks;
} catch (error) {
if (error.status === 429) {
const retryAfter = error.headers['retry-after'] || 1;
return rejectWithValue(`Rate limit hit. Retry after ${retryAfter} seconds.`);
}
return rejectWithValue(error.message);
}
}
);
// Fetch top artists
export const fetchTopArtists = createAsyncThunk(
'recommendations/fetchTopArtists',
async (_, { rejectWithValue }) => {
const accessToken = localStorage.getItem('access_token');
if (!accessToken) {
return rejectWithValue('No access token found.');
}
try {
spotifyApi.setAccessToken(accessToken);
const response = await spotifyApi.getMyTopArtists();
const topArtists = response.items.slice(0, 5).map((artist) => ({
id: artist.id,
name: artist.name,
}));
return topArtists;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// Fetch new releases
export const fetchNewReleases = createAsyncThunk(
'recommendations/fetchNewReleases',
async (_, { rejectWithValue }) => {
const accessToken = localStorage.getItem('access_token');
if (!accessToken) {
return rejectWithValue('No access token found.');
}
try {
spotifyApi.setAccessToken(accessToken);
const response = await spotifyApi.getNewReleases({ limit: 10 });
return response;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// Like a track
export const like = createAsyncThunk(
'recommendations/like',
async (track, { rejectWithValue }) => {
const accessToken = localStorage.getItem('access_token');
if (!accessToken) {
return rejectWithValue('No access token found.');
}
try {
spotifyApi.setAccessToken(accessToken);
let likedTracks = JSON.parse(localStorage.getItem('liked_tracks')) || [];
const trackIndex = likedTracks.findIndex((likedTrack) => likedTrack.id === track.id);
if (trackIndex > -1) {
await spotifyApi.removeFromMySavedTracks([track.id]);
const updatedLikedTracks = likedTracks.filter((likedTrack) => likedTrack.id !== track.id);
localStorage.setItem('liked_tracks', JSON.stringify(updatedLikedTracks));
return updatedLikedTracks;
} else {
await spotifyApi.addToMySavedTracks([track.id]);
const updatedLikedTracks = [...likedTracks, track];
localStorage.setItem('liked_tracks', JSON.stringify(updatedLikedTracks));
return updatedLikedTracks;
}
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// Dislike a track
export const dislike = createAsyncThunk(
'recommendations/dislike',
async (track, { rejectWithValue }) => {
const accessToken = localStorage.getItem('access_token');
if (!accessToken) {
return rejectWithValue('No access token found.');
}
spotifyApi.setAccessToken(accessToken);
let dislikedTracks = JSON.parse(localStorage.getItem('disliked_tracks')) || [];
let likedTracks = JSON.parse(localStorage.getItem('liked_tracks')) || [];
const trackIndex = dislikedTracks.findIndex((dislikedTrack) => dislikedTrack.id === track.id);
const likedTrackIndex = likedTracks.findIndex((likedTrack) => likedTrack.id === track.id);
try {
if (likedTrackIndex > -1) {
likedTracks = likedTracks.filter((likedTrack) => likedTrack.id !== track.id);
await spotifyApi.removeFromMySavedTracks([track.id]);
localStorage.setItem('liked_tracks', JSON.stringify(likedTracks));
}
if (trackIndex > -1) {
dislikedTracks = dislikedTracks.filter((dislikedTrack) => dislikedTrack.id !== track.id);
} else {
dislikedTracks = [...dislikedTracks, track];
}
localStorage.setItem('disliked_tracks', JSON.stringify(dislikedTracks));
return dislikedTracks;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
const recommendationsSlice = createSlice({
name: 'recommendations',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchSongRecommendations.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchSongRecommendations.fulfilled, (state, action) => {
state.status = 'succeeded';
state.tracks = action.payload;
})
.addCase(fetchSongRecommendations.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
})
.addCase(fetchTopArtists.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchTopArtists.fulfilled, (state, action) => {
state.topArtists = action.payload;
state.status = 'succeeded';
})
.addCase(fetchTopArtists.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
})
.addCase(fetchNewReleases.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchNewReleases.fulfilled, (state, action) => {
state.newReleases = action.payload;
state.status = 'succeeded';
})
.addCase(fetchNewReleases.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload;
})
.addCase(like.fulfilled, (state, action) => {
state.likedTracks = action.payload;
})
.addCase(dislike.fulfilled, (state, action) => {
state.dislikedTracks = action.payload;
});
},
});
export default recommendationsSlice.reducer;
// Selectors
export const fetchStatus = (state) => state.recommendations.status;
export const songsRecommended = (state) => state.recommendations.tracks;
export const topArtistsSelector = (state) => state.recommendations.topArtists;
export const newReleasesSelector = (state) => state.recommendations.newReleases;
export const likedTracksSelector = (state) => state.recommendations.likedTracks;
export const dislikedTracksSelector = (state) => state.recommendations.dislikedTracks;
The error is suggesting that the track id is missing and I do not see how that is as I make the request with it.
5