import { NextResponse } from "next/server"; import { env } from "@/lib/env"; import pkg from "../../../../../package.json"; /** * Maschinenlesbare Beschreibung der REST-API. * * Handgeschrieben statt aus Zod erzeugt: die Schnittstelle ist überschaubar, * und ein Generator brächte eine weitere Abhängigkeit samt Versionsrhythmus * für einen Nutzen, den ein Dutzend Zeilen JSON auch hergeben. */ export const dynamic = "force-dynamic"; export function GET() { const document = { openapi: "3.1.0", info: { title: "Flimmer API", version: pkg.version, description: "REST-Schnittstelle einer Flimmer-Instanz. Anmeldung über einen persönlichen " + "API-Schlüssel (Einstellungen → API-Schlüssel) als Bearer-Token. Alle Antworten " + "sind JSON; Zeitangaben in ISO 8601, Laufzeiten in Minuten.", license: { name: "MIT" }, }, servers: [{ url: `${env.appUrl}/api/v1` }], security: [{ bearerAuth: [] }], components: { securitySchemes: { bearerAuth: { type: "http", scheme: "bearer", description: "Persönlicher API-Schlüssel" }, }, schemas: { Fehler: { type: "object", properties: { error: { type: "string" }, details: {} }, required: ["error"], }, Sichtungsergebnis: { type: "object", properties: { watchIds: { type: "array", items: { type: "string" }, description: "Kennungen der neu angelegten Sichtungen — für DELETE zum Zurücknehmen.", }, status: { type: "string" }, label: { type: "string" }, }, }, }, }, paths: { "/me": { get: { summary: "Angemeldetes Konto", responses: { "200": { description: "Konto samt Umfang der Bibliothek" } }, }, }, "/bibliothek": { get: { summary: "Bibliothek lesen", parameters: [ { name: "q", in: "query", schema: { type: "string" }, description: "Volltextsuche" }, { name: "status", in: "query", schema: { type: "string", enum: ["WATCHING", "PAUSED", "COMPLETED", "DROPPED", "WATCHLIST"], }, }, { name: "typ", in: "query", schema: { type: "string", enum: ["SERIES", "MOVIE"] } }, { name: "genre", in: "query", schema: { type: "string" } }, { name: "tag", in: "query", schema: { type: "string" } }, { name: "dienst", in: "query", schema: { type: "string" } }, { name: "sammlung", in: "query", schema: { type: "string", enum: ["1"] }, description: "Nur Titel, die in der eigenen Sammlung liegen.", }, { name: "limit", in: "query", schema: { type: "integer", default: 50, maximum: 200 } }, { name: "offset", in: "query", schema: { type: "integer", default: 0 } }, ], responses: { "200": { description: "Einträge mit Fortschritt" } }, }, post: { summary: "Titel aufnehmen", requestBody: { required: true, content: { "application/json": { schema: { type: "object", properties: { titleId: { type: "string", description: "Interne Kennung" }, tmdbId: { type: "integer", description: "Alternativ: TMDB-Kennung" }, type: { type: "string", enum: ["SERIES", "MOVIE"] }, status: { type: "string", enum: ["WATCHING", "PAUSED", "COMPLETED", "DROPPED", "WATCHLIST"], default: "WATCHLIST", }, }, }, }, }, }, responses: { "200": { description: "Angelegter oder vorhandener Eintrag" } }, }, }, "/bibliothek/{entryId}": { parameters: [{ name: "entryId", in: "path", required: true, schema: { type: "string" } }], get: { summary: "Eintrag mit Fortschritt", responses: { "200": { description: "Eintrag" } } }, patch: { summary: "Status, Bewertung, Notiz ändern", requestBody: { required: true, content: { "application/json": { schema: { type: "object", properties: { status: { type: "string", enum: ["WATCHING", "PAUSED", "COMPLETED", "DROPPED", "WATCHLIST"], }, rating: { type: "integer", minimum: 1, maximum: 10, nullable: true }, notes: { type: "string", nullable: true }, whereToWatch: { type: "string", nullable: true }, favorite: { type: "boolean" }, owned: { type: "boolean", description: "Liegt der Titel in der eigenen Sammlung?", }, ownedSource: { type: "string", nullable: true, description: "Wo genau — Jellyfin, Plex, Blu-ray …", }, }, }, }, }, }, responses: { "200": { description: "Gespeichert" } }, }, delete: { summary: "In den Papierkorb legen", responses: { "200": { description: "Verschoben" } }, }, }, "/bibliothek/{entryId}/sichtungen": { parameters: [{ name: "entryId", in: "path", required: true, schema: { type: "string" } }], get: { summary: "Sichtungen auflisten", responses: { "200": { description: "Sichtungen" } } }, post: { summary: "Als gesehen markieren", description: "action=next hakt die nächste offene Episode ab, movie einen Film, " + "episodes eine Auswahl, season eine ganze Staffel, upTo alles bis zur genannten Episode.", requestBody: { required: true, content: { "application/json": { schema: { oneOf: [ { type: "object", properties: { action: { const: "next" } }, required: ["action"], }, { type: "object", properties: { action: { const: "movie" } }, required: ["action"], }, { type: "object", properties: { action: { const: "episodes" }, episodeIds: { type: "array", items: { type: "string" } }, }, required: ["action", "episodeIds"], }, { type: "object", properties: { action: { const: "season" }, seasonId: { type: "string" } }, required: ["action", "seasonId"], }, { type: "object", properties: { action: { const: "upTo" }, episodeId: { type: "string" } }, required: ["action", "episodeId"], }, ], }, }, }, }, responses: { "200": { description: "Angelegte Sichtungen", content: { "application/json": { schema: { $ref: "#/components/schemas/Sichtungsergebnis" }, }, }, }, }, }, delete: { summary: "Sichtungen zurücknehmen", description: "Gezielt über watchIds, über episodeIds, oder mit action=movie als Gegenstück " + "zum gleichnamigen POST — dafür braucht man die watchIds nicht zu kennen. " + "Zurückgenommen wird immer nur der laufende Durchlauf; frühere Durchläufe " + "bleiben für die Statistik erhalten.", requestBody: { required: true, content: { "application/json": { schema: { oneOf: [ { type: "object", properties: { watchIds: { type: "array", items: { type: "string" } } }, required: ["watchIds"], }, { type: "object", properties: { episodeIds: { type: "array", items: { type: "string" } } }, required: ["episodeIds"], }, { type: "object", properties: { action: { const: "movie" } }, required: ["action"], }, ], }, }, }, }, responses: { "200": { description: "Zurückgenommen" } }, }, }, "/suche": { get: { summary: "TMDB durchsuchen", parameters: [ { name: "q", in: "query", required: true, schema: { type: "string", minLength: 2 } }, { name: "typ", in: "query", schema: { type: "string", enum: ["serie", "film"] } }, ], responses: { "200": { description: "Treffer mit TMDB-Kennung" }, "503": { description: "Kein TMDB-Schlüssel hinterlegt" }, }, }, }, "/kalender": { get: { summary: "Kommende Episoden", parameters: [ { name: "von", in: "query", schema: { type: "string", format: "date" } }, { name: "bis", in: "query", schema: { type: "string", format: "date" } }, ], responses: { "200": { description: "Termine im Zeitraum" } }, }, }, "/statistik": { get: { summary: "Kennzahlen und Verteilungen", parameters: [{ name: "jahr", in: "query", schema: { type: "integer" } }], responses: { "200": { description: "Statistik" } }, }, }, "/universen": { get: { summary: "Universen auflisten", responses: { "200": { description: "Universen" } } }, post: { summary: "Universum anlegen", requestBody: { required: true, content: { "application/json": { schema: { type: "object", properties: { name: { type: "string" }, description: { type: "string" } }, required: ["name"], }, }, }, }, responses: { "200": { description: "Angelegt" } }, }, }, "/universen/{id}": { parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }], get: { summary: "Universum mit Reihenfolge und Fortschritt", description: "Ohne 'reihenfolge' gilt die Standardreihenfolge des Universums — sie trägt in " + "'orders' das Kennzeichen 'isDefault'.", parameters: [{ name: "reihenfolge", in: "query", schema: { type: "string" } }], responses: { "200": { description: "Universum" }, "404": { description: "Unbekannt" } }, }, post: { summary: "Titel ins Universum aufnehmen", requestBody: { required: true, content: { "application/json": { schema: { type: "object", properties: { titleId: { type: "string" } }, required: ["titleId"], }, }, }, }, responses: { "200": { description: "Aufgenommen" } }, }, patch: { summary: "Standardreihenfolge festlegen", description: "Bestimmt, welche Reihenfolge beim Öffnen erscheint. Braucht Schreibrecht; je " + "Universum trägt immer genau eine Reihenfolge den Standard.", requestBody: { required: true, content: { "application/json": { schema: { type: "object", properties: { defaultOrderId: { type: "string", description: "Muss eine Reihenfolge dieses Universums sein.", }, }, required: ["defaultOrderId"], }, }, }, }, responses: { "200": { description: "Gesetzt" }, "404": { description: "Die Reihenfolge gehört nicht zu diesem Universum" }, }, }, }, "/export": { get: { summary: "Eigene Daten exportieren", parameters: [ { name: "format", in: "query", schema: { type: "string", enum: ["json", "csv"] } }, ], responses: { "200": { description: "Datei" } }, }, }, }, }; return NextResponse.json(document, { headers: { "cache-control": "public, max-age=300" }, }); }