-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(spotify): integrate Spotify API for show recently played tracks
- Loading branch information
Showing
13 changed files
with
261 additions
and
6 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
SPOTIFY_CLIENT_ID= | ||
SPOTIFY_CLIENT_SECRET= | ||
SPOTIFY_REDIRECT_URL= | ||
SPOTIFY_REFRESH_TOKEN= |
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { NextRequest, NextResponse } from 'next/server'; | ||
import btoa from 'btoa'; | ||
import type { SpotifyTokenResponse } from '@/types'; | ||
|
||
export async function GET(request: NextRequest) { | ||
if (process.env.NODE_ENV !== 'development') return NextResponse.json({ error: 'not_implemented' }, { status: 501 }); | ||
|
||
const url = new URL(request.url); | ||
|
||
const code = url.searchParams.get('code'); | ||
if (!code) return NextResponse.json({ error: 'missing_params' }, { status: 400 }); | ||
|
||
const response = await fetch('https://accounts.spotify.com/api/token', { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded', | ||
Authorization: `Basic ${btoa(`${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}`)}` | ||
}, | ||
body: new URLSearchParams({ | ||
grant_type: 'authorization_code', | ||
code, | ||
redirect_uri: process.env.SPOTIFY_REDIRECT_URL | ||
}) | ||
}); | ||
|
||
if (!response.ok) return NextResponse.json({ error: 'failed_to_get_token' }, { status: 400 }); | ||
|
||
const data: SpotifyTokenResponse = await response.json(); | ||
|
||
return NextResponse.json(data.access_token); | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { NextResponse } from 'next/server'; | ||
|
||
const CLIENT_ID = process.env.SPOTIFY_CLIENT_ID; | ||
const REDIRECT_URI = process.env.SPOTIFY_REDIRECT_URL; | ||
|
||
export async function GET(): Promise<Response> { | ||
if (process.env.NODE_ENV !== 'development') return NextResponse.json({ error: 'not_implemented' }, { status: 501 }); | ||
|
||
const scope = 'user-read-recently-played'; | ||
|
||
const url = new URL('https://accounts.spotify.com/authorize'); | ||
|
||
url.searchParams.set('response_type', 'code'); | ||
url.searchParams.set('client_id', CLIENT_ID); | ||
url.searchParams.set('scope', scope); | ||
url.searchParams.set('redirect_uri', REDIRECT_URI); | ||
|
||
return NextResponse.redirect(url.toString()); | ||
} |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import getNewAccessToken from '@/utils/getSpotifyAccessToken'; | ||
import Image from 'next/image'; | ||
import Link from 'next/link'; | ||
|
||
export default async function Songs() { | ||
if (!process.env.SPOTIFY_CLIENT_ID || !process.env.SPOTIFY_CLIENT_SECRET || !process.env.SPOTIFY_REFRESH_TOKEN) { | ||
console.warn('Environment variables SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, and SPOTIFY_REFRESH_TOKEN are required to render the recently played songs component'); | ||
|
||
return ( | ||
<span className='text-tertiary'> | ||
No data available | ||
</span> | ||
); | ||
} | ||
|
||
const accessToken = await getNewAccessToken(); | ||
|
||
const response = await fetch('https://api.spotify.com/v1/me/player/recently-played?limit=5', { | ||
headers: { | ||
Authorization: `Bearer ${accessToken}` | ||
}, | ||
next: { | ||
revalidate: 3600 | ||
} | ||
}); | ||
|
||
if (!response.ok) throw new Error('Failed to get recently played tracks'); | ||
|
||
const tracks: SpotifyApi.UsersRecentlyPlayedTracksResponse = await response.json(); | ||
|
||
if (!tracks.items.length) return ( | ||
<span className='text-tertiary'> | ||
No data available | ||
</span> | ||
); | ||
|
||
return tracks.items | ||
.sort((a, b) => new Date(b.played_at).getTime() - new Date(a.played_at).getTime()) | ||
.map(({ played_at, track }) => ( | ||
<Link | ||
href={track.album.external_urls.spotify} | ||
key={track.id} | ||
className='group relative z-10 flex w-full items-center gap-x-4' | ||
> | ||
<div className='absolute left-0 top-0 -z-10 h-[130%] w-[105%] translate-x-[-2.5%] translate-y-[-12%] rounded-xl bg-secondary opacity-0 transition-opacity duration-75 group-hover:opacity-100' /> | ||
|
||
<Image | ||
src={track.album.images[0].url} | ||
height={48} | ||
width={48} | ||
alt={track.name} | ||
className='rounded-lg' | ||
/> | ||
|
||
<div className='flex flex-col'> | ||
<h2 className='font-medium text-primary'> | ||
{track.name} | ||
</h2> | ||
|
||
<div className='flex items-center text-sm text-tertiary'> | ||
{track.artists.map(({ name }) => name).join(', ')} | ||
|
||
<span className='mx-1'>•</span> | ||
|
||
{new Date(played_at).toLocaleTimeString('en-US', { | ||
hour: '2-digit', | ||
minute: '2-digit' | ||
})} | ||
</div> | ||
</div> | ||
</Link> | ||
)); | ||
} |
This file contains 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
This file contains 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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
declare global { | ||
namespace NodeJS { | ||
interface ProcessEnv { | ||
NODE_ENV: 'development' | 'production'; | ||
SPOTIFY_CLIENT_ID: string; | ||
SPOTIFY_CLIENT_SECRET: string; | ||
SPOTIFY_REDIRECT_URL: string; | ||
SPOTIFY_REFRESH_TOKEN: string; | ||
} | ||
} | ||
} | ||
|
||
export {}; |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
/** | ||
* Retrieves a new Spotify access token using the refresh token. | ||
* | ||
* @returns {Promise<string>} A promise that resolves to the new access token. | ||
* @throws {Error} If any of the required environment variables are missing or if the request fails. | ||
* | ||
* Environment Variables: | ||
* - `SPOTIFY_CLIENT_ID`: The Spotify client ID. | ||
* - `SPOTIFY_CLIENT_SECRET`: The Spotify client secret. | ||
* - `SPOTIFY_REFRESH_TOKEN`: The Spotify refresh token. | ||
*/ | ||
export default async function getNewAccessToken(): Promise<string> { | ||
if (!process.env.SPOTIFY_CLIENT_ID || !process.env.SPOTIFY_CLIENT_SECRET || !process.env.SPOTIFY_REFRESH_TOKEN) { | ||
throw new Error('Missing environment variables.'); | ||
} | ||
|
||
const response = await fetch('https://accounts.spotify.com/api/token', { | ||
method: 'POST', | ||
headers: { | ||
'Content-Type': 'application/x-www-form-urlencoded', | ||
Authorization: `Basic ${btoa(`${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}`)}` | ||
}, | ||
body: new URLSearchParams({ | ||
grant_type: 'refresh_token', | ||
refresh_token: process.env.SPOTIFY_REFRESH_TOKEN | ||
}), | ||
next: { | ||
revalidate: 3500 | ||
} | ||
}); | ||
|
||
if (!response.ok) throw new Error('Failed to get new access token.'); | ||
|
||
const data = await response.json(); | ||
|
||
return data.access_token; | ||
} |