import Link from "next/link"; import { notFound } from "next/navigation"; import type { Metadata } from "next"; import { requireUser } from "@/lib/auth"; import { getSettings } from "@/lib/settings"; import { prisma } from "@/lib/db"; import { ensureTitle } from "@/lib/catalog"; import { getNextEpisode, getProgress, getWatchedEpisodeIds } from "@/lib/library"; import { formatMinutes, STATUS_COLOR, STATUS_LABEL, type StatusKey } from "@/lib/labels"; import { AppShell } from "@/components/AppShell"; import { Backdrop, Poster } from "@/components/Poster"; import { ProgressBar } from "@/components/ProgressBar"; import { ProviderList, type ProviderBadgeData, type ProviderKindKey, } from "@/components/ProviderBadges"; import { SeasonList, type SeasonData } from "@/components/library/SeasonList"; import { AddToLibrary, EntryActions, RatingPicker, StatusPicker, } from "@/components/library/EntryControls"; import { EntryNotes } from "@/components/library/EntryNotes"; import { NextEpisodeCard, WatchMovieButton } from "@/components/library/NextEpisode"; type Params = Promise<{ id: string }>; export async function generateMetadata({ params }: { params: Params }): Promise { const { id } = await params; const title = await prisma.title.findUnique({ where: { id }, select: { title: true } }); return { title: title?.title ?? "Titel" }; } export default async function TitlePage({ params }: { params: Params }) { const user = await requireUser(); const settings = await getSettings(); const { id } = await params; const title = await prisma.title.findUnique({ where: { id }, include: { seasons: { orderBy: { seasonNumber: "asc" }, include: { episodes: { orderBy: { episodeNumber: "asc" } } }, }, providers: { where: { region: user.region }, include: { provider: true } }, entries: { where: { userId: user.id }, include: { tags: { include: { tag: true } } }, }, }, }); if (!title) notFound(); // Im Hintergrund auffrischen, falls der Eintrag alt ist. Schlägt das fehl, // bleibt der lokale Stand stehen — die Seite funktioniert trotzdem. if (title.tmdbId) { await ensureTitle(title.type, title.tmdbId, { region: user.region }).catch(() => undefined); } const entry = title.entries[0] ?? null; const isSeries = title.type === "SERIES"; const [progress, next, watchedIds] = entry ? await Promise.all([ getProgress(user.id, entry.id), isSeries ? getNextEpisode(user.id, entry.id) : Promise.resolve(null), isSeries ? getWatchedEpisodeIds(user.id, entry.id) : Promise.resolve(new Set()), ]) : [null, null, new Set()]; const providerGroups: Partial> = {}; for (const row of title.providers) { (providerGroups[row.kind as ProviderKindKey] ??= []).push({ id: row.provider.id, name: row.provider.name, logoPath: row.provider.logoPath, color: row.provider.color, }); } const suggestion = providerGroups.FLATRATE?.[0]?.name ?? providerGroups.FREE?.[0]?.name ?? null; const seasons: SeasonData[] = title.seasons.map((season) => ({ id: season.id, seasonNumber: season.seasonNumber, name: season.name, overview: season.overview, airDate: season.airDate?.toISOString() ?? null, episodes: season.episodes.map((episode) => ({ id: episode.id, episodeNumber: episode.episodeNumber, name: episode.name, overview: episode.overview, airDate: episode.airDate?.toISOString() ?? null, runtime: episode.runtime, })), })); const year = title.releaseDate?.getFullYear() ?? null; const movieWatched = !isSeries && (progress?.watched ?? 0) > 0; return (

{isSeries ? "Serie" : "Film"} {year ? ` · ${year}` : ""} {title.status ? ` · ${title.status}` : ""}

{entry?.owned && ( Hab ich{entry.ownedSource ? ` · ${entry.ownedSource}` : ""} )} {entry && ( {STATUS_LABEL[entry.status as StatusKey]} )}

{title.title}

{title.originalTitle && title.originalTitle !== title.title && (

{title.originalTitle}

)} {title.tagline && (

„{title.tagline}"

)}
{isSeries && ( {title.seasonCount} {title.seasonCount === 1 ? "Staffel" : "Staffeln"} ·{" "} {title.episodeCount} Episoden )} {title.runtime ? ( {title.runtime} min ) : null} {title.voteAverage ? {title.voteAverage.toFixed(1)} / 10 : null} {entry && progress ? ( {formatMinutes(progress.minutesWatched)} ) : null}
{title.genres.length > 0 && (
    {title.genres.map((genre) => (
  • {genre}
  • ))}
)} {title.overview && (

{title.overview}

)}
{entry ? (
{progress && (
)} {isSeries && next && } {isSeries && !next && progress?.total ? (

Durchgeschaut — alle {progress.total} Episoden abgehakt.

) : null} {!isSeries && }
) : ( )}
{entry && (

{isSeries ? "Staffeln" : "Wo läuft das?"}

{isSeries ? ( ) : (
)}
)} {!entry && ( <>

Wo läuft das?

Verfügbarkeitsdaten von JustWatch über TMDB, Region {user.region}.

{isSeries && (

Staffeln

)} )}

← Zur Bibliothek

); } /** * Hinweis auf die eigene Sammlung — steht bewusst **vor** der * Streaming-Verfügbarkeit: was man hat, ist die verlässlichere Antwort auf * „wo kann ich das schauen". */ function OwnedNotice({ owned, source }: { owned: boolean; source: string | null }) { if (!owned) return null; return (

In deiner Sammlung {source ? · {source} : null}

); } function Fact({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); }