mirror of
https://github.com/r-freeman/portfolio.git
synced 2025-05-02 19:00:20 +00:00
Drop spotify integration
All checks were successful
Build And Publish / BuildAndPublish (push) Successful in 3m10s
All checks were successful
Build And Publish / BuildAndPublish (push) Successful in 3m10s
This commit is contained in:
parent
d420655464
commit
b7fa177d94
@ -1,6 +1,3 @@
|
||||
SPOTIFY_CLIENT_ID=
|
||||
SPOTIFY_CLIENT_SECRET=
|
||||
SPOTIFY_REFRESH_TOKEN=
|
||||
NEXT_PUBLIC_SITE_URL=
|
||||
GITHUB_ACCESS_TOKEN=
|
||||
GITHUB_USER_ID=
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
})
|
||||
}
|
@ -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
|
||||
})
|
||||
}
|
@ -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">
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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}`
|
||||
}
|
||||
})
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user