Skip to content

Instantly share code, notes, and snippets.

@zpuckeridge
Created December 14, 2023 05:27
Show Gist options
  • Select an option

  • Save zpuckeridge/bee0956a6aee68bfaada5019a1d4d95f to your computer and use it in GitHub Desktop.

Select an option

Save zpuckeridge/bee0956a6aee68bfaada5019a1d4d95f to your computer and use it in GitHub Desktop.
Spotify Now Playing Component + API Route + Token Generation
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
import { FaSpotify } from "react-icons/fa6";
interface SongData {
progress: number;
duration: number;
title: string;
album_art: string;
url: string;
}
interface NowPlayingData {
is_playing: boolean;
song: SongData;
}
const getNowPlaying = async (): Promise<NowPlayingData | null> => {
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_VERCEL_URL}/api/now-playing`,
);
return res.json();
} catch (error) {
console.error("Error fetching now playing data:", error);
return null;
}
};
export default function NowPlaying() {
const [nowPlayingData, setNowPlayingData] = useState<NowPlayingData | null>(
null,
);
const [isLoading, setIsLoading] = useState<boolean>(true);
useEffect(() => {
const interval = setInterval(() => {
getNowPlaying()
.then((data) => {
setNowPlayingData(data);
setIsLoading(false); // Data is available, no longer loading
})
.catch(() => {
setIsLoading(false); // Failed to fetch data, no longer loading
});
}, 1000);
return () => clearInterval(interval);
}, []);
if (isLoading) {
return (
<a
href="https://open.spotify.com/user/oid25p8bf0jm4zfezkf765o03?si=f67b4f43e7fa4620"
target="_blank"
className="flex gap-2 font-mono uppercase tracking-tight leading-snug hover:text-muted-foreground transition-all duration-200"
>
<FaSpotify className="h-10 w-10 border-2 border-muted my-auto" />
<div className="my-auto">
<p className="text-sm">Not Playing</p>
</div>
</a>
);
}
if (!nowPlayingData || !nowPlayingData.song) {
return (
<a
href="https://open.spotify.com/user/oid25p8bf0jm4zfezkf765o03?si=f67b4f43e7fa4620"
target="_blank"
className="flex gap-2 font-mono uppercase tracking-tight leading-snug hover:text-muted-foreground transition-all duration-200"
>
<FaSpotify className="h-10 w-10 border-2 border-muted my-auto" />
<div className="my-auto">
<p className="text-sm">Not Playing</p>
</div>
</a>
);
}
const { is_playing, song } = nowPlayingData;
const { progress, duration, title, album_art, url } = song;
const minutesProgress = Math.floor(progress / 1000 / 60);
const secondsProgress = Math.floor((progress / 1000) % 60);
const minutesDuration = Math.floor(duration / 1000 / 60);
const secondsDuration = Math.floor((duration / 1000) % 60);
return (
<>
{is_playing ? (
<a
href={url}
target="_blank"
className="flex gap-2 font-mono uppercase tracking-tight leading-snug hover:text-muted-foreground transition-all duration-200"
>
<Image
src={album_art}
alt={title}
width={300}
height={300}
className="h-10 w-10 rounded-lg border-2 border-muted my-auto"
/>
<div className="my-auto">
<p className="text-sm">{title}</p>
<p className="text-xs text-muted-foreground">
{minutesProgress}:
{secondsProgress < 10 ? `0${secondsProgress}` : secondsProgress} /{" "}
{minutesDuration}:
{secondsDuration < 10 ? `0${secondsDuration}` : secondsDuration}
</p>
</div>
</a>
) : (
<a
href="https://open.spotify.com/user/oid25p8bf0jm4zfezkf765o03?si=f67b4f43e7fa4620"
target="_blank"
className="flex gap-2 font-mono uppercase tracking-tight leading-snug hover:text-muted-foreground transition-all duration-200"
>
<FaSpotify className="h-10 w-10 border-2 rounded-full border-muted my-auto" />
<div className="my-auto">
<p className="text-sm ">Not Playing</p>
</div>
</a>
)}
</>
);
}
import { getAccessToken } from "@/lib/spotify";
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export async function GET() {
try {
const { access_token } = await getAccessToken();
const response = await fetch(
"https://api.spotify.com/v1/me/player/currently-playing",
{
headers: {
Authorization: `Bearer ${access_token}`,
},
},
);
if (response.status === 204) {
return NextResponse.json({ is_playing: false });
}
if (!response.ok) {
if (response.status === 401) {
console.error("Unauthorized access. Please check your access token.");
} else {
console.error(
"Error fetching now playing data. Status:",
response.status,
);
}
return NextResponse.json({ is_playing: false });
}
const text = await response.text();
let data = null;
try {
data = JSON.parse(text);
} catch (error) {
console.error("Error parsing JSON:", error);
console.error("Received text:", text);
return NextResponse.json({ is_playing: false });
}
if (!data || !data.item) {
return NextResponse.json({ is_playing: false });
}
return NextResponse.json({
is_playing: data.is_playing,
song: {
url: data.item.external_urls.spotify,
title: data.item.name,
album_art: data.item.album.images[0].url,
progress: data.progress_ms,
duration: data.item.duration_ms,
},
});
} catch (error) {
console.error("Error fetching now playing data:", error);
return NextResponse.json({ is_playing: false });
}
}
import querystring from "querystring";
const client_id = process.env.SPOTIFY_CLIENT_ID;
const client_secret = process.env.SPOTIFY_CLIENT_SECRET;
const refresh_token = process.env.SPOTIFY_REFRESH_TOKEN;
const basic = Buffer.from(`${client_id}:${client_secret}`).toString("base64");
const TOKEN_ENDPOINT = `https://accounts.spotify.com/api/token`;
export const getAccessToken = async () => {
const response = await fetch(TOKEN_ENDPOINT, {
method: "POST",
headers: {
Authorization: `Basic ${basic}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: querystring.stringify({
grant_type: "refresh_token",
refresh_token,
}),
});
return response.json();
};
@zpuckeridge
Copy link
Author

/components/now-playing.tsx
/api/now-playing/route.ts
/lib/spotify.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment