Files
flimmer/src/app/api/v1/bibliothek/[entryId]/route.ts
T
Florian Wilde 9ee8664233
CI / pruefen (push) Canceled after 0s
Release / veroeffentlichen (push) Canceled after 0s
feat: eigene Sammlung — „hab ich" mit Quelle
Streaming-Verfügbarkeit beantwortet nur die halbe Frage: manche Filme laufen
nirgends mehr, die hat man gekauft und liegen zu Hause — in Jellyfin, Plex,
Emby, auf dem NAS oder als Scheibe.

Bewusst getrennt vom Feld „wo schaue ich das": das eine sagt, wo ein Titel
gerade läuft, und das ändert sich ständig; das andere sagt, was einem gehört,
und das bleibt. Auf der Detailseite steht die Sammlung deshalb vor der
Verfügbarkeit — was man hat, ist die verlässlichere Antwort.

Durchgezogen bis in Filter, Export, CSV-Import und REST-API.
2026-09-01 21:15:53 +02:00

90 lines
2.8 KiB
TypeScript

import { z } from "zod";
import { apiError, readJson, withApi } from "@/lib/api";
import { prisma } from "@/lib/db";
import {
getNextEpisode,
getProgress,
moveToTrash,
setStatus,
updateEntryDetails,
} from "@/lib/library";
const STATUSES = ["WATCHING", "PAUSED", "COMPLETED", "DROPPED", "WATCHLIST"] as const;
/** GET /api/v1/bibliothek/{id} — ein Eintrag mit Fortschritt und nächster Episode. */
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 entry = await prisma.libraryEntry.findFirst({
where: { id: entryId, userId: user.id },
include: {
title: true,
tags: { include: { tag: { select: { name: true } } } },
},
});
if (!entry) return apiError("Diesen Eintrag gibt es nicht.", 404);
const [progress, next] = await Promise.all([
getProgress(user.id, entry.id),
getNextEpisode(user.id, entry.id),
]);
return {
id: entry.id,
status: entry.status,
rating: entry.rating,
notes: entry.notes,
whereToWatch: entry.whereToWatch,
owned: entry.owned,
ownedSource: entry.ownedSource,
favorite: entry.favorite,
currentRun: entry.currentRun,
tags: entry.tags.map((link) => link.tag.name),
title: {
id: entry.title.id,
title: entry.title.title,
type: entry.title.type,
tmdbId: entry.title.tmdbId,
year: entry.title.releaseDate?.getFullYear() ?? null,
genres: entry.title.genres,
},
progress,
nextEpisode: next,
};
});
const patchSchema = z.object({
status: z.enum(STATUSES).optional(),
rating: z.number().int().min(1).max(10).nullable().optional(),
notes: z.string().max(5000).nullable().optional(),
whereToWatch: z.string().max(120).nullable().optional(),
favorite: z.boolean().optional(),
owned: z.boolean().optional(),
ownedSource: z.string().max(80).nullable().optional(),
});
/** PATCH /api/v1/bibliothek/{id} — Status und Angaben ändern. */
export const PATCH = withApi(async (request, user, context) => {
const { entryId } = await context.params;
if (!entryId) return apiError("Es fehlt die Kennung des Eintrags.", 400);
const { status, ...details } = await readJson(request, patchSchema);
if (status) await setStatus(user.id, entryId, status);
if (Object.keys(details).length > 0) {
await updateEntryDetails(user.id, entryId, details);
}
return { ok: true };
});
/** DELETE /api/v1/bibliothek/{id} — in den Papierkorb legen. */
export const DELETE = withApi(async (_request, user, context) => {
const { entryId } = await context.params;
if (!entryId) return apiError("Es fehlt die Kennung des Eintrags.", 400);
await moveToTrash(user.id, entryId);
return { ok: true, message: "In den Papierkorb gelegt." };
});