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.
90 lines
2.8 KiB
TypeScript
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." };
|
|
});
|