blastoise/routes/playlists.ts

261 lines
7.8 KiB
TypeScript

import {
createPlaylist,
getPlaylist,
getPlaylistsByUser,
getPublicPlaylists,
getPlaylistByShareToken,
updatePlaylist,
deletePlaylist,
setPlaylistTracks,
addTracksToPlaylist,
removeTrackFromPlaylist,
generatePlaylistShareToken,
removePlaylistShareToken,
findUserById,
} from "../db";
import { getOrCreateUser, userHasPermission } from "./helpers";
// GET /api/playlists - List user's + shared playlists
export function handleListPlaylists(req: Request, server: any): Response {
const { user } = getOrCreateUser(req, server);
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const myPlaylists = getPlaylistsByUser(user.id);
const sharedPlaylists = getPublicPlaylists(user.id);
return Response.json({
mine: myPlaylists,
shared: sharedPlaylists,
});
}
// POST /api/playlists - Create new playlist
export async function handleCreatePlaylist(req: Request, server: any): Promise<Response> {
const { user } = getOrCreateUser(req, server);
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
if (user.is_guest) {
return Response.json({ error: "Guests cannot create playlists" }, { status: 403 });
}
let body: { name: string; description?: string };
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
if (!body.name?.trim()) {
return Response.json({ error: "Name required" }, { status: 400 });
}
const playlist = createPlaylist(body.name.trim(), user.id, body.description?.trim() || "");
return Response.json(playlist, { status: 201 });
}
// GET /api/playlists/:id - Get playlist details
export function handleGetPlaylist(req: Request, server: any, playlistId: string): Response {
const { user } = getOrCreateUser(req, server);
const playlist = getPlaylist(playlistId);
if (!playlist) {
return Response.json({ error: "Playlist not found" }, { status: 404 });
}
// Check access: owner, public, or has share token
const url = new URL(req.url);
const shareToken = url.searchParams.get("token");
if (
playlist.ownerId !== user?.id &&
!playlist.isPublic &&
playlist.shareToken !== shareToken
) {
return Response.json({ error: "Access denied" }, { status: 403 });
}
// Include owner username
const owner = findUserById(playlist.ownerId);
return Response.json({
...playlist,
ownerName: owner?.username || "Unknown",
});
}
// PATCH /api/playlists/:id - Update playlist
export async function handleUpdatePlaylist(req: Request, server: any, playlistId: string): Promise<Response> {
const { user } = getOrCreateUser(req, server);
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const playlist = getPlaylist(playlistId);
if (!playlist) {
return Response.json({ error: "Playlist not found" }, { status: 404 });
}
if (playlist.ownerId !== user.id && !user.is_admin) {
return Response.json({ error: "Not your playlist" }, { status: 403 });
}
let body: { name?: string; description?: string; isPublic?: boolean };
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
updatePlaylist(playlistId, {
name: body.name?.trim(),
description: body.description?.trim(),
isPublic: body.isPublic,
});
return Response.json({ ok: true });
}
// DELETE /api/playlists/:id - Delete playlist
export function handleDeletePlaylist(req: Request, server: any, playlistId: string): Response {
const { user } = getOrCreateUser(req, server);
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const playlist = getPlaylist(playlistId);
if (!playlist) {
return Response.json({ error: "Playlist not found" }, { status: 404 });
}
if (playlist.ownerId !== user.id && !user.is_admin) {
return Response.json({ error: "Not your playlist" }, { status: 403 });
}
deletePlaylist(playlistId);
return Response.json({ ok: true });
}
// PATCH /api/playlists/:id/tracks - Modify tracks
export async function handleModifyPlaylistTracks(req: Request, server: any, playlistId: string): Promise<Response> {
const { user } = getOrCreateUser(req, server);
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const playlist = getPlaylist(playlistId);
if (!playlist) {
return Response.json({ error: "Playlist not found" }, { status: 404 });
}
if (playlist.ownerId !== user.id && !user.is_admin) {
return Response.json({ error: "Not your playlist" }, { status: 403 });
}
let body: { add?: string[]; remove?: number[]; set?: string[] };
try {
body = await req.json();
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
// If 'set' is provided, replace entire track list
if (body.set !== undefined) {
setPlaylistTracks(playlistId, body.set);
return Response.json({ ok: true });
}
// Remove tracks by position (do removes first, in reverse order)
if (body.remove?.length) {
const positions = [...body.remove].sort((a, b) => b - a);
for (const pos of positions) {
removeTrackFromPlaylist(playlistId, pos);
}
}
// Add tracks
if (body.add?.length) {
addTracksToPlaylist(playlistId, body.add);
}
return Response.json({ ok: true });
}
// POST /api/playlists/:id/share - Generate share token
export function handleSharePlaylist(req: Request, server: any, playlistId: string): Response {
const { user } = getOrCreateUser(req, server);
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const playlist = getPlaylist(playlistId);
if (!playlist) {
return Response.json({ error: "Playlist not found" }, { status: 404 });
}
if (playlist.ownerId !== user.id && !user.is_admin) {
return Response.json({ error: "Not your playlist" }, { status: 403 });
}
const token = generatePlaylistShareToken(playlistId);
return Response.json({ shareToken: token });
}
// DELETE /api/playlists/:id/share - Remove sharing
export function handleUnsharePlaylist(req: Request, server: any, playlistId: string): Response {
const { user } = getOrCreateUser(req, server);
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const playlist = getPlaylist(playlistId);
if (!playlist) {
return Response.json({ error: "Playlist not found" }, { status: 404 });
}
if (playlist.ownerId !== user.id && !user.is_admin) {
return Response.json({ error: "Not your playlist" }, { status: 403 });
}
removePlaylistShareToken(playlistId);
return Response.json({ ok: true });
}
// GET /api/playlists/shared/:token - Get shared playlist by token
export function handleGetSharedPlaylist(req: Request, server: any, token: string): Response {
const playlist = getPlaylistByShareToken(token);
if (!playlist) {
return Response.json({ error: "Playlist not found" }, { status: 404 });
}
const owner = findUserById(playlist.ownerId);
return Response.json({
...playlist,
ownerName: owner?.username || "Unknown",
});
}
// POST /api/playlists/shared/:token/copy - Copy shared playlist to own
export function handleCopySharedPlaylist(req: Request, server: any, token: string): Response {
const { user } = getOrCreateUser(req, server);
if (!user) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
if (user.is_guest) {
return Response.json({ error: "Guests cannot copy playlists" }, { status: 403 });
}
const original = getPlaylistByShareToken(token);
if (!original) {
return Response.json({ error: "Playlist not found" }, { status: 404 });
}
const copy = createPlaylist(`${original.name} (Copy)`, user.id, original.description);
setPlaylistTracks(copy.id, original.trackIds);
return Response.json(copy, { status: 201 });
}