feat: REST-API unter /api/v1 mit OpenAPI-Beschreibung
Dieselben Endpunkte bedienen Skripte und das eigene Frontend: erst wird ein Bearer-Token geprüft, sonst das Sitzungscookie. Sichtungsaktionen geben immer watchIds zurück — damit lässt sich genau diese Aktion über DELETE zurücknehmen, ohne raten zu müssen, was dazugehörte. Die OpenAPI-Beschreibung ist handgeschrieben statt aus Zod erzeugt: die Schnittstelle ist überschaubar, und ein Generator brächte eine weitere Abhängigkeit für einen Nutzen, den ein Dutzend Zeilen JSON auch hergeben.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { z } from "zod";
|
||||
import { apiError, readJson, withApi } from "@/lib/api";
|
||||
import { prisma } from "@/lib/db";
|
||||
import {
|
||||
markEpisodesWatched,
|
||||
markMovieWatched,
|
||||
markSeasonWatched,
|
||||
markUpTo,
|
||||
undoWatches,
|
||||
unmarkEpisodes,
|
||||
watchNextEpisode,
|
||||
} from "@/lib/library";
|
||||
|
||||
/** GET — die Sichtungen eines Eintrags, jüngste zuerst. */
|
||||
export const GET = withApi(async (_request, user, context) => {
|
||||
const { entryId } = await context.params;
|
||||
if (!entryId) return apiError("Es fehlt die Kennung des Eintrags.", 400);
|
||||
|
||||
const watches = await prisma.watch.findMany({
|
||||
where: { userId: user.id, entryId },
|
||||
orderBy: { watchedAt: "desc" },
|
||||
take: 500,
|
||||
include: { episode: { select: { seasonNumber: true, episodeNumber: true, name: true } } },
|
||||
});
|
||||
|
||||
return {
|
||||
total: watches.length,
|
||||
items: watches.map((watch) => ({
|
||||
id: watch.id,
|
||||
run: watch.run,
|
||||
watchedAt: watch.watchedAt,
|
||||
minutes: watch.minutes,
|
||||
episode: watch.episode,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const postSchema = z.union([
|
||||
z.object({ action: z.literal("next") }),
|
||||
z.object({ action: z.literal("movie") }),
|
||||
z.object({
|
||||
action: z.literal("episodes"),
|
||||
episodeIds: z.array(z.string().min(1)).min(1).max(500),
|
||||
}),
|
||||
z.object({ action: z.literal("season"), seasonId: z.string().min(1) }),
|
||||
z.object({ action: z.literal("upTo"), episodeId: z.string().min(1) }),
|
||||
]);
|
||||
|
||||
/**
|
||||
* POST — etwas als gesehen markieren.
|
||||
*
|
||||
* Die Antwort enthält immer `watchIds`. Damit lässt sich die Aktion über
|
||||
* DELETE gezielt zurücknehmen, ohne raten zu müssen, was dazugehörte.
|
||||
*/
|
||||
export const POST = withApi(async (request, user, context) => {
|
||||
const { entryId } = await context.params;
|
||||
if (!entryId) return apiError("Es fehlt die Kennung des Eintrags.", 400);
|
||||
|
||||
const body = await readJson(request, postSchema);
|
||||
|
||||
switch (body.action) {
|
||||
case "next":
|
||||
return watchNextEpisode(user.id, entryId);
|
||||
case "movie":
|
||||
return markMovieWatched(user.id, entryId);
|
||||
case "episodes":
|
||||
return markEpisodesWatched(user.id, entryId, body.episodeIds);
|
||||
case "season":
|
||||
return markSeasonWatched(user.id, entryId, body.seasonId);
|
||||
case "upTo":
|
||||
return markUpTo(user.id, entryId, body.episodeId);
|
||||
}
|
||||
});
|
||||
|
||||
const deleteSchema = z.union([
|
||||
z.object({ watchIds: z.array(z.string().min(1)).min(1).max(500) }),
|
||||
z.object({ episodeIds: z.array(z.string().min(1)).min(1).max(500) }),
|
||||
]);
|
||||
|
||||
/** DELETE — Sichtungen zurücknehmen, über watchIds oder episodeIds. */
|
||||
export const DELETE = withApi(async (request, user, context) => {
|
||||
const { entryId } = await context.params;
|
||||
if (!entryId) return apiError("Es fehlt die Kennung des Eintrags.", 400);
|
||||
|
||||
const body = await readJson(request, deleteSchema);
|
||||
|
||||
if ("watchIds" in body) {
|
||||
await undoWatches(user.id, entryId, body.watchIds);
|
||||
} else {
|
||||
await unmarkEpisodes(user.id, entryId, body.episodeIds);
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
Reference in New Issue
Block a user