Drop spotify integration
All checks were successful
Build And Publish / BuildAndPublish (push) Successful in 3m10s

This commit is contained in:
Ryan Freeman 2025-04-29 21:14:06 +01:00
parent d420655464
commit b7fa177d94
8 changed files with 1 additions and 298 deletions

View File

@ -1,6 +1,3 @@
SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET=
SPOTIFY_REFRESH_TOKEN=
NEXT_PUBLIC_SITE_URL=
GITHUB_ACCESS_TOKEN=
GITHUB_USER_ID=

BIN
.env.gpg

Binary file not shown.

View File

@ -10,7 +10,6 @@ and skills, as well as provide information about me and my interests.
- Database: [Supabase](https://supabase.com/)
- Deployment: [Self-Hosted on Raspberry Pi 5](https://ryanfreeman.dev/writing/migrating-from-vercel-to-raspberry-pi-5)
- Styling: [Tailwind CSS](https://tailwindcss.com/)
- Integrations: [Spotify](https://spotify.com/)
## Project structure

View File

@ -1,54 +0,0 @@
import {NextResponse} from 'next/server'
import {getCurrentlyPlaying} from '@/lib/spotify'
type Song = {
item: {
album: {
name: string
images: [
{
url: string
}
]
}
artists: [
{
name: string
}
]
external_urls: {
spotify: string
}
name: string
}
is_playing: boolean
}
export async function GET(request: Request) {
const response = await getCurrentlyPlaying()
if (response.status === 204 || response.status > 400) {
return new Response(JSON.stringify({isPlaying: false}), {
status: 200
})
}
const song = await response.json() as Song
const {item} = song
const artist = item.artists.map(artist => artist.name).join(', ')
const title = item.name;
const songUrl = item.external_urls.spotify
const album = item.album.name
const albumImageUrl = item.album.images[0].url
const isPlaying = song.is_playing;
return NextResponse.json({
artist,
title,
songUrl,
album,
albumImageUrl,
isPlaying
})
}

View File

@ -1,56 +0,0 @@
import {NextResponse} from 'next/server'
import {getRecentlyPlayed} from '@/lib/spotify'
type Tracks = {
items: [
{
track: {
name: string
artists: [
{
name: string
}
]
external_urls: {
spotify: string
}
album: {
name: string
images: [
{
url: string
}
]
}
}
played_at: string
}
]
}
export async function GET(request: Request) {
const response = await getRecentlyPlayed()
if (response.status > 400) {
return new Response(JSON.stringify({status: response.statusText}), {
status: response.status
})
}
const tracks = await response.json() as Tracks
const {track} = tracks.items.reduce((r, a) => r.played_at > a.played_at ? r : a)
const title = track.name;
const artist = track.artists.map(artist => artist.name).join(', ')
const songUrl = track.external_urls.spotify
const album = track.album.name
const albumImageUrl = track.album.images[0].url
return NextResponse.json({
artist,
title,
songUrl,
album,
albumImageUrl
})
}

View File

@ -1,7 +1,6 @@
import React from 'react'
import {InnerContainer, OuterContainer} from './Container'
import {NavLink} from '@/components/ui/Navigation'
import {SpotifyPlayer} from '@/components/ui/SpotifyPlayer'
import {SocialLink} from '@/components/ui/SocialLink'
import {GitHubIcon, LinkedInIcon} from '@/components/icons/SocialIcons'
@ -10,11 +9,8 @@ export function Footer() {
return (
<footer className="mt-32">
<OuterContainer>
<div className="border-t border-zinc-100 pt-10 pb-16 dark:border-zinc-700/40">
<div className="border-t border-zinc-100 pb-16 dark:border-zinc-700/40">
<InnerContainer>
{process.env.NODE_ENV !== 'development' &&
<SpotifyPlayer/>
}
<div className="flex flex-col items-center justify-between gap-6 mt-12">
<div
className="flex flex-wrap justify-center gap-6 text-sm font-medium text-zinc-800 dark:text-zinc-200">

View File

@ -1,124 +0,0 @@
'use client'
import useSWR from 'swr'
import fetcher from '@/lib/fetcher'
import Image from 'next/image'
import clsx from 'clsx'
import {ElementType, ReactElement} from 'react'
import Link from 'next/link'
type Artist = {
as?: ElementType
artist: string
}
type Title = {
as?: ElementType
title: string
songUrl: string
className?: string
}
type Album = {
album: string
albumImageUrl: string
}
type Song = {
as?: ElementType
} & Artist & Title & Album
type PlayerStateResponse = {
data: Song
error: string
isLoading: boolean
}
function usePlayerState(path: string) {
const {data, error, isLoading} = useSWR(`/api/spotify/${path}`, fetcher) as PlayerStateResponse
return {
song: data,
isLoading,
isError: error
}
}
function Song({as: Component = 'div', artist, title, songUrl, album, albumImageUrl}: Song) {
return (
<Component
className="flex items-center space-x-4">
<Song.Album
album={album}
albumImageUrl={albumImageUrl}
/>
<div>
<Song.Title
title={title}
songUrl={songUrl}
/>
<Song.Artist artist={artist}/>
</div>
</Component>
)
}
Song.Album = function SongAlbum({album, albumImageUrl}: Album) {
return (
<Image
width="64"
height="64"
alt={album}
src={albumImageUrl}
className="aspect-square rounded-2xl object-cover"
/>
)
}
Song.Title = function SongTitle({as: Component = 'h2', title, songUrl, className}: Title) {
return (
<Component
className={clsx(className, 'text-sm font-semibold text-zinc-800 dark:text-zinc-100 line-clamp-1 lg:line-clamp-none')}>
<Link href={songUrl}>
{title}
</Link>
</Component>
)
}
Song.Artist = function SongArtist({as: Component = 'p', artist}: Artist) {
return (
<Component className="text-sm text-zinc-600 dark:text-zinc-400 line-clamp-1 lg:line-clamp-none">
{artist}
</Component>
)
}
Song.Skeleton = function SongSkeleton() {
return (
<div className="flex items-center space-x-4 animate-pulse">
<div
className="w-[64px] h-[64px] bg-zinc-100 rounded-2xl dark:bg-zinc-900"
/>
<div>
<p className="w-[128px] h-3 bg-zinc-100 rounded-2xl dark:bg-zinc-900"/>
<p className="mt-3 w-[128px] h-3 bg-zinc-100 rounded-2xl dark:bg-zinc-900"/>
</div>
</div>
)
}
export function SpotifyPlayer(): ReactElement | null {
const lastPlayed = usePlayerState('last-played')
if (lastPlayed.isError) return null
return (
<div className="grid">
{lastPlayed.isLoading
? <Song.Skeleton/>
: <Song {...lastPlayed.song}/>
}
</div>
)
}

View File

@ -1,55 +0,0 @@
import fetch from 'node-fetch'
const SPOTIFY_CLIENT_ID = process.env.SPOTIFY_CLIENT_ID
const SPOTIFY_CLIENT_SECRET = process.env.SPOTIFY_CLIENT_SECRET
const SPOTIFY_REFRESH_TOKEN = process.env.SPOTIFY_REFRESH_TOKEN
const SPOTIFY_TOKEN = 'https://accounts.spotify.com/api/token'
const SPOTIFY_CURRENTLY_PLAYING = 'https://api.spotify.com/v1/me/player/currently-playing'
const SPOTIFY_RECENTLY_PLAYED = 'https://api.spotify.com/v1/me/player/recently-played'
const basic = Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString('base64')
type Response = {
access_token: string
token_type: string
scope: string
expires_in: number
refresh_token: string
}
const getAccessToken = async () => {
const response = await fetch(SPOTIFY_TOKEN, {
method: 'POST',
headers: {
Authorization: `Basic ${basic}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
'grant_type': 'refresh_token',
'refresh_token': `${SPOTIFY_REFRESH_TOKEN}`
})
})
return response.json()
}
export const getCurrentlyPlaying = async () => {
const {access_token}: Response = await getAccessToken() as Response
return await fetch(SPOTIFY_CURRENTLY_PLAYING, {
headers: {
Authorization: `Bearer ${access_token}`
}
})
}
export const getRecentlyPlayed = async () => {
const {access_token}: Response = await getAccessToken() as Response
return await fetch(SPOTIFY_RECENTLY_PLAYED, {
headers: {
Authorization: `Bearer ${access_token}`
}
})
}