When I developed my portfolio, I wanted to display my Spotify listening live. And after a few hours of searching. I managed to get to the bottom of it, and here's how I did it.
Result
Here's the result you'll get at the end of this article.
Spotify
I'm busy writing down in a clear and concise way, the easiest way to get your Spotify API keys and your TOKEN. Of these, it's the TOKEN that may seem the most complex to retrieve if you're not familiar with the process. However, once this step has been completed, recovery simple and hassle-free.
Environment
Add the three variables SPOTIFY_CLIENT_ID
, SPOTIFY_CLIENT_SECRET
and SPOTIFY_REFRESH_TOKEN
to your environment file.
and SPOTIFY_REFRESH_TOKEN
.
SPOTIFY_CLIENT_ID=""
SPOTIFY_CLIENT_SECRET=""
SPOTIFY_REFRESH_TOKEN=""
Typing
We will now create three files in src/types/spotify/
named entities.d.ts
, entity.d.ts
& request.d.ts
.
& request.d.ts
import type { SpotifyEntity, Image } from './entity';
interface Album extends SpotifyEntity {
type: 'album';
popularity: number;
}
interface Artist extends SpotifyEntity {
type: 'artist';
popularity: number;
}
export interface Track extends SpotifyEntity {
type: 'track';
popularity: number;
duration_ms: number;
album: Album;
artists: Array<Artist>;
preview_url: string;
is_playable: boolean;
is_local: boolean;
}
export interface ReadableTrack {
name: string;
artist: string;
album: string;
previewUrl: string;
url: string;
image?: Image;
hdImage?: Image;
duration: number;
}
export type SpotifyEntityType = 'album' | 'artist' | 'playlist' | 'track';
export type SpotifyEntityUri = `spotify:${SpotifyEntityType}:${string}`;
export interface Image {
url: string;
height?: number | null;
width?: number | null;
}
export interface SpotifyEntity {
id: string;
name: string;
href: string;
uri: SpotifyEntityUri;
type: SpotifyEntityType;
images: Array<Image>;
external_urls: { spotify: string };
}
import type { ReadableTrack, Track } from './entities';
import type { SpotifyEntity } from './entity.d';
export interface SpotifyResponse<T extends SpotifyEntity | PlayHistoryObject> {
href: string;
next?: string | null;
previous?: string | null;
limit: number;
offset: number;
total: number;
items: Array<T>;
}
export interface ErrorResponse {
error: {
status: number;
message: string;
};
}
export interface NowPlayingResponse {
is_playing: boolean;
item: Track;
}
export interface PlayHistoryObject {
track: Track;
played_at?: string;
context?: unknown | null;
}
export type NowPlayingAPIResponse = {
track?: ReadableTrack | null;
isPlaying: boolean;
} | null;
Spotify library
We're now going to create a file in src/lib/
named spotify.ts
and put all our business logic
in it.
import type {ErrorResponse, NowPlayingResponse, PlayHistoryObject, SpotifyResponse,} from '@/types/spotify/request.d';
const serialize = (obj: Record<string | number, string | number | boolean>) => {
const str = [];
for (const p in obj) {
if (Object.prototype.hasOwnProperty.call(obj, p)) {
str.push(`${encodeURIComponent(p)}=${encodeURIComponent(obj[p])}`);
}
}
return str.join('&');
};
const buildSpotifyRequest = async <T>(
endpoint: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
body?: Record<string, unknown>,
): Promise<T | ErrorResponse> => {
const {access_token: accessToken} = await getAccessToken().catch(null);
if (!accessToken) {
return {
error: {message: 'Could not get access token', status: 401},
};
}
const response = await fetch(endpoint, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
method,
body: body && method !== 'GET' ? JSON.stringify(body) : undefined,
});
try {
const json = await response.json();
if (response.ok) return json as T;
return json as ErrorResponse;
} catch {
return {
error: {
message: response.statusText || 'Server error',
status: response.status || 500,
},
};
}
};
const clientId = process.env.SPOTIFY_CLIENT_ID || '';
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET || '';
const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN || '';
const basic = btoa(`${clientId}:${clientSecret}`);
const TOKEN_ENDPOINT = 'https://accounts.spotify.com/api/token';
const getAccessToken = async (): Promise<{ access_token?: string }> => {
try {
const response = await fetch(TOKEN_ENDPOINT, {
method: 'POST',
headers: {
Authorization: `Basic ${basic}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: serialize({
grant_type: 'refresh_token',
refresh_token: refreshToken,
}),
next: {
revalidate: 0,
},
});
return response.json();
} catch {
return {access_token: undefined};
}
};
const NOW_PLAYING_ENDPOINT = 'https://api.spotify.com/v1/me/player/currently-playing';
export const getNowPlaying = async () => buildSpotifyRequest<NowPlayingResponse>(NOW_PLAYING_ENDPOINT);
const RECENTLY_PLAYED_ENDPOINT = 'https://api.spotify.com/v1/me/player/recently-played?limit=1';
export const getRecentlyPlayed = async () => buildSpotifyRequest<SpotifyResponse<PlayHistoryObject>>(RECENTLY_PLAYED_ENDPOINT,);
Component
We're now going to create a component and set up our logic so that the Spotify API is called to update the data on the client side.
import {useRequest} from "@/hooks/use-request";
import type {NowPlayingAPIResponse} from "@/types/spotify/request";
export function CurrentlyListenSpotify() {
const {data} = useRequest<NowPlayingAPIResponse>('api/spotify/now-playing');
const {track} = data || {isPlaying: false};
return (
<div className="space-y-8">
{data?.isPlaying ? (
<a
href={track?.url}
target={'_blank'}
className="select-none"
title={`En train d'écouter ${track?.name} par ${track?.artist} sur Spotify`}
rel="noreferrer"
>
<div className="flex flex-row-reverse items-center justify-between gap-2">
<Image
src={track?.image?.url as string}
alt={`Couverture d'album : '${track?.album}' par '${track?.artist}'`}
width={56}
height={56}
quality={50}
className="size-6 rounded border"
/>
<div className="flex flex-col">
<div className="font-semibold">{track?.artist}</div>
<span className="inline-flex">—</span>
<p className="text-xs text-gray-500">{track?.name}</p>
</div>
</div>
</a>
) : (
<div className="flex flex-row-reverse items-center justify-between gap-2">
<SpotifyIcon />
<div className="flex flex-col">
<div className="font-semibold">Rien n'est écouté</div>
<span className="inline-flex">—</span>
<p className="text-xs text-muted-foreground">Spotify</p></div>
</div>
)}
</div>
)
}
Finally
All that's left is to add your component where you want it to be displayed.
import {CurrentlyListenSpotify} from "@/components/spotify";
export function App () {
return (
<div>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
<CurrentlyListenSpotify/>
</div>
)
}