mirror of
https://github.com/r-freeman/portfolio.git
synced 2025-05-03 19:50: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=
|
NEXT_PUBLIC_SITE_URL=
|
||||||
GITHUB_ACCESS_TOKEN=
|
GITHUB_ACCESS_TOKEN=
|
||||||
GITHUB_USER_ID=
|
GITHUB_USER_ID=
|
||||||
|
@ -10,7 +10,6 @@ and skills, as well as provide information about me and my interests.
|
|||||||
- Database: [Supabase](https://supabase.com/)
|
- Database: [Supabase](https://supabase.com/)
|
||||||
- Deployment: [Self-Hosted on Raspberry Pi 5](https://ryanfreeman.dev/writing/migrating-from-vercel-to-raspberry-pi-5)
|
- Deployment: [Self-Hosted on Raspberry Pi 5](https://ryanfreeman.dev/writing/migrating-from-vercel-to-raspberry-pi-5)
|
||||||
- Styling: [Tailwind CSS](https://tailwindcss.com/)
|
- Styling: [Tailwind CSS](https://tailwindcss.com/)
|
||||||
- Integrations: [Spotify](https://spotify.com/)
|
|
||||||
|
|
||||||
## Project structure
|
## 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 React from 'react'
|
||||||
import {InnerContainer, OuterContainer} from './Container'
|
import {InnerContainer, OuterContainer} from './Container'
|
||||||
import {NavLink} from '@/components/ui/Navigation'
|
import {NavLink} from '@/components/ui/Navigation'
|
||||||
import {SpotifyPlayer} from '@/components/ui/SpotifyPlayer'
|
|
||||||
import {SocialLink} from '@/components/ui/SocialLink'
|
import {SocialLink} from '@/components/ui/SocialLink'
|
||||||
import {GitHubIcon, LinkedInIcon} from '@/components/icons/SocialIcons'
|
import {GitHubIcon, LinkedInIcon} from '@/components/icons/SocialIcons'
|
||||||
|
|
||||||
@ -10,11 +9,8 @@ export function Footer() {
|
|||||||
return (
|
return (
|
||||||
<footer className="mt-32">
|
<footer className="mt-32">
|
||||||
<OuterContainer>
|
<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>
|
<InnerContainer>
|
||||||
{process.env.NODE_ENV !== 'development' &&
|
|
||||||
<SpotifyPlayer/>
|
|
||||||
}
|
|
||||||
<div className="flex flex-col items-center justify-between gap-6 mt-12">
|
<div className="flex flex-col items-center justify-between gap-6 mt-12">
|
||||||
<div
|
<div
|
||||||
className="flex flex-wrap justify-center gap-6 text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
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