Created
December 14, 2023 05:27
-
-
Save zpuckeridge/bee0956a6aee68bfaada5019a1d4d95f to your computer and use it in GitHub Desktop.
Spotify Now Playing Component + API Route + Token Generation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "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> | |
| )} | |
| </> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | |
| }; |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
/components/now-playing.tsx/api/now-playing/route.ts/lib/spotify.ts