spotify state resume/capture
This commit is contained in:
@@ -29,7 +29,9 @@ import { Route as AuthedAdminTournamentsIdRouteImport } from './routes/_authed/a
|
||||
import { Route as AuthedAdminTournamentsRunIdRouteImport } from './routes/_authed/admin/tournaments/run.$id'
|
||||
import { ServerRoute as ApiTournamentsUploadLogoServerRouteImport } from './routes/api/tournaments/upload-logo'
|
||||
import { ServerRoute as ApiSpotifyTokenServerRouteImport } from './routes/api/spotify/token'
|
||||
import { ServerRoute as ApiSpotifyResumeServerRouteImport } from './routes/api/spotify/resume'
|
||||
import { ServerRoute as ApiSpotifyPlaybackServerRouteImport } from './routes/api/spotify/playback'
|
||||
import { ServerRoute as ApiSpotifyCaptureServerRouteImport } from './routes/api/spotify/capture'
|
||||
import { ServerRoute as ApiSpotifyCallbackServerRouteImport } from './routes/api/spotify/callback'
|
||||
import { ServerRoute as ApiEventsSplatServerRouteImport } from './routes/api/events.$'
|
||||
import { ServerRoute as ApiAuthSplatServerRouteImport } from './routes/api/auth.$'
|
||||
@@ -131,12 +133,22 @@ const ApiSpotifyTokenServerRoute = ApiSpotifyTokenServerRouteImport.update({
|
||||
path: '/api/spotify/token',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiSpotifyResumeServerRoute = ApiSpotifyResumeServerRouteImport.update({
|
||||
id: '/api/spotify/resume',
|
||||
path: '/api/spotify/resume',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiSpotifyPlaybackServerRoute =
|
||||
ApiSpotifyPlaybackServerRouteImport.update({
|
||||
id: '/api/spotify/playback',
|
||||
path: '/api/spotify/playback',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiSpotifyCaptureServerRoute = ApiSpotifyCaptureServerRouteImport.update({
|
||||
id: '/api/spotify/capture',
|
||||
path: '/api/spotify/capture',
|
||||
getParentRoute: () => rootServerRouteImport,
|
||||
} as any)
|
||||
const ApiSpotifyCallbackServerRoute =
|
||||
ApiSpotifyCallbackServerRouteImport.update({
|
||||
id: '/api/spotify/callback',
|
||||
@@ -276,7 +288,9 @@ export interface FileServerRoutesByFullPath {
|
||||
'/api/auth/$': typeof ApiAuthSplatServerRoute
|
||||
'/api/events/$': typeof ApiEventsSplatServerRoute
|
||||
'/api/spotify/callback': typeof ApiSpotifyCallbackServerRoute
|
||||
'/api/spotify/capture': typeof ApiSpotifyCaptureServerRoute
|
||||
'/api/spotify/playback': typeof ApiSpotifyPlaybackServerRoute
|
||||
'/api/spotify/resume': typeof ApiSpotifyResumeServerRoute
|
||||
'/api/spotify/token': typeof ApiSpotifyTokenServerRoute
|
||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
|
||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
@@ -285,7 +299,9 @@ export interface FileServerRoutesByTo {
|
||||
'/api/auth/$': typeof ApiAuthSplatServerRoute
|
||||
'/api/events/$': typeof ApiEventsSplatServerRoute
|
||||
'/api/spotify/callback': typeof ApiSpotifyCallbackServerRoute
|
||||
'/api/spotify/capture': typeof ApiSpotifyCaptureServerRoute
|
||||
'/api/spotify/playback': typeof ApiSpotifyPlaybackServerRoute
|
||||
'/api/spotify/resume': typeof ApiSpotifyResumeServerRoute
|
||||
'/api/spotify/token': typeof ApiSpotifyTokenServerRoute
|
||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
|
||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
@@ -295,7 +311,9 @@ export interface FileServerRoutesById {
|
||||
'/api/auth/$': typeof ApiAuthSplatServerRoute
|
||||
'/api/events/$': typeof ApiEventsSplatServerRoute
|
||||
'/api/spotify/callback': typeof ApiSpotifyCallbackServerRoute
|
||||
'/api/spotify/capture': typeof ApiSpotifyCaptureServerRoute
|
||||
'/api/spotify/playback': typeof ApiSpotifyPlaybackServerRoute
|
||||
'/api/spotify/resume': typeof ApiSpotifyResumeServerRoute
|
||||
'/api/spotify/token': typeof ApiSpotifyTokenServerRoute
|
||||
'/api/tournaments/upload-logo': typeof ApiTournamentsUploadLogoServerRoute
|
||||
'/api/files/$collection/$recordId/$file': typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
@@ -306,7 +324,9 @@ export interface FileServerRouteTypes {
|
||||
| '/api/auth/$'
|
||||
| '/api/events/$'
|
||||
| '/api/spotify/callback'
|
||||
| '/api/spotify/capture'
|
||||
| '/api/spotify/playback'
|
||||
| '/api/spotify/resume'
|
||||
| '/api/spotify/token'
|
||||
| '/api/tournaments/upload-logo'
|
||||
| '/api/files/$collection/$recordId/$file'
|
||||
@@ -315,7 +335,9 @@ export interface FileServerRouteTypes {
|
||||
| '/api/auth/$'
|
||||
| '/api/events/$'
|
||||
| '/api/spotify/callback'
|
||||
| '/api/spotify/capture'
|
||||
| '/api/spotify/playback'
|
||||
| '/api/spotify/resume'
|
||||
| '/api/spotify/token'
|
||||
| '/api/tournaments/upload-logo'
|
||||
| '/api/files/$collection/$recordId/$file'
|
||||
@@ -324,7 +346,9 @@ export interface FileServerRouteTypes {
|
||||
| '/api/auth/$'
|
||||
| '/api/events/$'
|
||||
| '/api/spotify/callback'
|
||||
| '/api/spotify/capture'
|
||||
| '/api/spotify/playback'
|
||||
| '/api/spotify/resume'
|
||||
| '/api/spotify/token'
|
||||
| '/api/tournaments/upload-logo'
|
||||
| '/api/files/$collection/$recordId/$file'
|
||||
@@ -334,7 +358,9 @@ export interface RootServerRouteChildren {
|
||||
ApiAuthSplatServerRoute: typeof ApiAuthSplatServerRoute
|
||||
ApiEventsSplatServerRoute: typeof ApiEventsSplatServerRoute
|
||||
ApiSpotifyCallbackServerRoute: typeof ApiSpotifyCallbackServerRoute
|
||||
ApiSpotifyCaptureServerRoute: typeof ApiSpotifyCaptureServerRoute
|
||||
ApiSpotifyPlaybackServerRoute: typeof ApiSpotifyPlaybackServerRoute
|
||||
ApiSpotifyResumeServerRoute: typeof ApiSpotifyResumeServerRoute
|
||||
ApiSpotifyTokenServerRoute: typeof ApiSpotifyTokenServerRoute
|
||||
ApiTournamentsUploadLogoServerRoute: typeof ApiTournamentsUploadLogoServerRoute
|
||||
ApiFilesCollectionRecordIdFileServerRoute: typeof ApiFilesCollectionRecordIdFileServerRoute
|
||||
@@ -472,6 +498,13 @@ declare module '@tanstack/react-start/server' {
|
||||
preLoaderRoute: typeof ApiSpotifyTokenServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/spotify/resume': {
|
||||
id: '/api/spotify/resume'
|
||||
path: '/api/spotify/resume'
|
||||
fullPath: '/api/spotify/resume'
|
||||
preLoaderRoute: typeof ApiSpotifyResumeServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/spotify/playback': {
|
||||
id: '/api/spotify/playback'
|
||||
path: '/api/spotify/playback'
|
||||
@@ -479,6 +512,13 @@ declare module '@tanstack/react-start/server' {
|
||||
preLoaderRoute: typeof ApiSpotifyPlaybackServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/spotify/capture': {
|
||||
id: '/api/spotify/capture'
|
||||
path: '/api/spotify/capture'
|
||||
fullPath: '/api/spotify/capture'
|
||||
preLoaderRoute: typeof ApiSpotifyCaptureServerRouteImport
|
||||
parentRoute: typeof rootServerRouteImport
|
||||
}
|
||||
'/api/spotify/callback': {
|
||||
id: '/api/spotify/callback'
|
||||
path: '/api/spotify/callback'
|
||||
@@ -566,7 +606,9 @@ const rootServerRouteChildren: RootServerRouteChildren = {
|
||||
ApiAuthSplatServerRoute: ApiAuthSplatServerRoute,
|
||||
ApiEventsSplatServerRoute: ApiEventsSplatServerRoute,
|
||||
ApiSpotifyCallbackServerRoute: ApiSpotifyCallbackServerRoute,
|
||||
ApiSpotifyCaptureServerRoute: ApiSpotifyCaptureServerRoute,
|
||||
ApiSpotifyPlaybackServerRoute: ApiSpotifyPlaybackServerRoute,
|
||||
ApiSpotifyResumeServerRoute: ApiSpotifyResumeServerRoute,
|
||||
ApiSpotifyTokenServerRoute: ApiSpotifyTokenServerRoute,
|
||||
ApiTournamentsUploadLogoServerRoute: ApiTournamentsUploadLogoServerRoute,
|
||||
ApiFilesCollectionRecordIdFileServerRoute:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createFileRoute, redirect, useRouter } from "@tanstack/react-router";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
import {
|
||||
tournamentKeys,
|
||||
tournamentQueries,
|
||||
useTournament,
|
||||
} from "@/features/tournaments/queries";
|
||||
@@ -11,9 +10,6 @@ import { useMemo } from "react";
|
||||
import { BracketData } from "@/features/bracket/types";
|
||||
import { Match } from "@/features/matches/types";
|
||||
import BracketView from "@/features/bracket/components/bracket-view";
|
||||
import { startMatch } from "@/features/matches/server";
|
||||
import { useServerMutation } from "@/lib/tanstack-query/hooks";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { SpotifyControlsBar } from "@/features/spotify/components";
|
||||
|
||||
export const Route = createFileRoute("/_authed/admin/tournaments/run/$id")({
|
||||
@@ -30,6 +26,7 @@ export const Route = createFileRoute("/_authed/admin/tournaments/run/$id")({
|
||||
},
|
||||
loader: ({ context }) => ({
|
||||
fullWidth: true,
|
||||
withPadding: false,
|
||||
showSpotifyPanel: true,
|
||||
header: {
|
||||
withBackButton: true,
|
||||
@@ -78,7 +75,7 @@ function RouteComponent() {
|
||||
}, [tournament.matches]);
|
||||
|
||||
return (
|
||||
<Container size="md">
|
||||
<Container size="md" px={0}>
|
||||
<SpotifyControlsBar />
|
||||
{tournament.matches?.length ? (
|
||||
<BracketView bracket={bracket} showControls />
|
||||
|
||||
59
src/app/routes/api/spotify/capture.ts
Normal file
59
src/app/routes/api/spotify/capture.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||
import { SpotifyWebApiClient } from '@/lib/spotify/client'
|
||||
import type { SpotifyPlaybackSnapshot } from '@/lib/spotify/types'
|
||||
|
||||
export const ServerRoute = createServerFileRoute('/api/spotify/capture').methods({
|
||||
POST: async ({ request }: { request: Request }) => {
|
||||
try {
|
||||
// Get access token from cookies
|
||||
const cookies = request.headers.get('Cookie') || ''
|
||||
const accessTokenMatch = cookies.match(/spotify_access_token=([^;]+)/)
|
||||
|
||||
if (!accessTokenMatch) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No access token found' }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const accessToken = decodeURIComponent(accessTokenMatch[1])
|
||||
const spotifyClient = new SpotifyWebApiClient(accessToken)
|
||||
|
||||
// Create a snapshot of the current playback state
|
||||
const snapshot = await spotifyClient.createPlaybackSnapshot()
|
||||
|
||||
if (!snapshot) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No active playback to capture' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ snapshot }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Spotify capture error:', error)
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to capture playback state'
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ error: errorMessage }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
72
src/app/routes/api/spotify/resume.ts
Normal file
72
src/app/routes/api/spotify/resume.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { createServerFileRoute } from '@tanstack/react-start/server'
|
||||
import { SpotifyWebApiClient } from '@/lib/spotify/client'
|
||||
import type { SpotifyPlaybackSnapshot } from '@/lib/spotify/types'
|
||||
|
||||
export const ServerRoute = createServerFileRoute('/api/spotify/resume').methods({
|
||||
POST: async ({ request }: { request: Request }) => {
|
||||
try {
|
||||
// Get access token from cookies
|
||||
const cookies = request.headers.get('Cookie') || ''
|
||||
const accessTokenMatch = cookies.match(/spotify_access_token=([^;]+)/)
|
||||
|
||||
if (!accessTokenMatch) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No access token found' }),
|
||||
{
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const accessToken = decodeURIComponent(accessTokenMatch[1])
|
||||
const spotifyClient = new SpotifyWebApiClient(accessToken)
|
||||
|
||||
// Parse the request body to get the snapshot
|
||||
const body = await request.json()
|
||||
const { snapshot } = body as { snapshot: SpotifyPlaybackSnapshot }
|
||||
|
||||
if (!snapshot) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'No snapshot provided' }),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Restore the playback state from the snapshot
|
||||
await spotifyClient.restorePlaybackSnapshot(snapshot)
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Spotify resume error:', error)
|
||||
|
||||
let errorMessage = 'Failed to resume playback state'
|
||||
|
||||
// Handle common Spotify Premium requirement error
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('Premium') || error.message.includes('403')) {
|
||||
errorMessage = 'Spotify Premium required for playback control'
|
||||
} else {
|
||||
errorMessage = error.message
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ error: errorMessage }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user