spotify controls

This commit is contained in:
yohlo
2025-09-12 11:08:21 -05:00
parent 9d92a8a510
commit 0169468114
15 changed files with 1655 additions and 28 deletions

View File

@@ -0,0 +1,195 @@
import { createServerFileRoute } from '@tanstack/react-start/server'
import { SpotifyWebApiClient } from '@/lib/spotify/client'
// Helper function to get access token from cookies
function getAccessTokenFromCookies(request: Request): string | null {
const cookieHeader = request.headers.get('cookie')
if (!cookieHeader) return null
const cookies = Object.fromEntries(
cookieHeader.split('; ').map(c => c.split('='))
)
return cookies.spotify_access_token || null
}
export const ServerRoute = createServerFileRoute('/api/spotify/playback').methods({
POST: async ({ request }: { request: Request }) => {
try {
const accessToken = getAccessTokenFromCookies(request)
if (!accessToken) {
return new Response(
JSON.stringify({ error: 'No access token found' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' },
}
)
}
const body = await request.json()
const { action, deviceId, volumePercent } = body
const spotifyClient = new SpotifyWebApiClient(accessToken)
switch (action) {
case 'play':
await spotifyClient.play(deviceId)
break
case 'pause':
await spotifyClient.pause()
break
case 'next':
await spotifyClient.skipToNext()
break
case 'previous':
await spotifyClient.skipToPrevious()
break
case 'volume':
if (typeof volumePercent !== 'number') {
return new Response(
JSON.stringify({ error: 'volumePercent must be a number' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
)
}
await spotifyClient.setVolume(volumePercent)
break
case 'transfer':
if (!deviceId) {
return new Response(
JSON.stringify({ error: 'deviceId is required for transfer action' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
)
}
await spotifyClient.transferPlayback(deviceId)
break
default:
return new Response(
JSON.stringify({ error: 'Invalid action' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
)
}
return new Response(
JSON.stringify({ success: true }),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
)
} catch (error) {
console.error('Playback control error:', error)
// Handle specific Spotify API errors
if (error instanceof Error) {
if (error.message.includes('NO_ACTIVE_DEVICE')) {
return new Response(
JSON.stringify({ error: 'No active device found. Please select a device first.' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' },
}
)
}
if (error.message.includes('PREMIUM_REQUIRED')) {
return new Response(
JSON.stringify({ error: 'Spotify Premium is required for playback control.' }),
{
status: 403,
headers: { 'Content-Type': 'application/json' },
}
)
}
// Log the full error details for debugging
console.error('Full error details:', {
message: error.message,
stack: error.stack,
name: error.name,
})
}
return new Response(
JSON.stringify({ error: 'Playback control failed', details: error instanceof Error ? error.message : 'Unknown error' }),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
)
}
},
// GET endpoint for retrieving current playback state and devices
GET: async ({ request }: { request: Request }) => {
try {
const accessToken = getAccessTokenFromCookies(request)
if (!accessToken) {
return new Response(
JSON.stringify({ error: 'No access token found' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' },
}
)
}
const url = new URL(request.url)
const type = url.searchParams.get('type') // 'state' or 'devices'
const spotifyClient = new SpotifyWebApiClient(accessToken)
if (type === 'devices') {
const devices = await spotifyClient.getDevices()
return new Response(
JSON.stringify({ devices }),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
)
} else if (type === 'state') {
const playbackState = await spotifyClient.getPlaybackState()
return new Response(
JSON.stringify({ playbackState }),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
)
} else {
// Return both by default
const [devices, playbackState] = await Promise.all([
spotifyClient.getDevices(),
spotifyClient.getPlaybackState(),
])
return new Response(
JSON.stringify({ devices, playbackState }),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
)
}
} catch (error) {
console.error('Get playback data error:', error)
return new Response(
JSON.stringify({ error: 'Failed to get playback data' }),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
)
}
},
})