204 lines
6.6 KiB
TypeScript
204 lines
6.6 KiB
TypeScript
import { createFileRoute } from "@tanstack/react-router";
|
|
import { SpotifyWebApiClient } from "@/lib/spotify/client";
|
|
|
|
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 Route = createFileRoute("/api/spotify/playback")({
|
|
server: {
|
|
handlers: {
|
|
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, trackId, positionMs } = body;
|
|
|
|
const spotifyClient = new SpotifyWebApiClient(accessToken);
|
|
|
|
switch (action) {
|
|
case "play":
|
|
await spotifyClient.play(deviceId);
|
|
break;
|
|
case "playTrack":
|
|
if (!trackId) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: "trackId is required for playTrack action",
|
|
}),
|
|
{
|
|
status: 400,
|
|
headers: { "Content-Type": "application/json" },
|
|
}
|
|
);
|
|
}
|
|
await spotifyClient.playTrack(trackId, deviceId, positionMs);
|
|
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);
|
|
|
|
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" },
|
|
}
|
|
);
|
|
}
|
|
|
|
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: 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");
|
|
|
|
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 {
|
|
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" },
|
|
}
|
|
);
|
|
}
|
|
},
|
|
},
|
|
},
|
|
});
|