feat: eigene Sammlung — „hab ich" mit Quelle
CI / pruefen (push) Canceled after 0s
Release / veroeffentlichen (push) Canceled after 0s

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.
This commit is contained in:
Florian Wilde
2026-09-01 21:15:53 +02:00
parent cd9953c47c
commit 9ee8664233
26 changed files with 467 additions and 52 deletions
+12 -1
View File
@@ -110,7 +110,14 @@ export async function setStatus(userId: string, entryId: string, status: EntrySt
export async function updateEntryDetails(
userId: string,
entryId: string,
data: { rating?: number | null; notes?: string | null; whereToWatch?: string | null; favorite?: boolean },
data: {
rating?: number | null;
notes?: string | null;
whereToWatch?: string | null;
favorite?: boolean;
owned?: boolean;
ownedSource?: string | null;
},
) {
await requireEntry(userId, entryId);
@@ -127,6 +134,10 @@ export async function updateEntryDetails(
? { whereToWatch: data.whereToWatch?.trim() || null }
: {}),
...(data.favorite !== undefined ? { favorite: data.favorite } : {}),
...(data.owned !== undefined ? { owned: data.owned } : {}),
...(data.ownedSource !== undefined
? { ownedSource: data.ownedSource?.trim() || null }
: {}),
},
});
}
+12 -1
View File
@@ -19,6 +19,8 @@ export type LibraryFilters = {
genre?: string;
tag?: string;
provider?: string;
/** Nur Titel, die in der eigenen Sammlung liegen. */
owned?: boolean;
sort?: LibrarySort;
};
@@ -42,6 +44,8 @@ export type LibraryItem = {
favorite: boolean;
currentRun: number;
whereToWatch: string | null;
owned: boolean;
ownedSource: string | null;
tags: string[];
watched: number;
total: number;
@@ -56,6 +60,7 @@ export async function getLibrary(
userId,
deletedAt: null,
...(filters.status ? { status: filters.status } : {}),
...(filters.owned ? { owned: true } : {}),
...(filters.tag ? { tags: { some: { tag: { name: filters.tag } } } } : {}),
title: {
...(filters.type ? { type: filters.type } : {}),
@@ -116,6 +121,8 @@ export async function getLibrary(
favorite: entry.favorite,
currentRun: entry.currentRun,
whereToWatch: entry.whereToWatch,
owned: entry.owned,
ownedSource: entry.ownedSource,
tags: entry.tags.map((link) => link.tag.name),
watched,
total,
@@ -162,11 +169,13 @@ export type LibraryFacets = {
tags: string[];
providers: string[];
counts: Record<EntryStatus | "ALLE", number>;
/** Wie viele Titel in der eigenen Sammlung liegen. */
owned: number;
};
/** Werte für die Filterleiste — nur das, was in der Bibliothek wirklich vorkommt. */
export async function getLibraryFacets(userId: string): Promise<LibraryFacets> {
const [genreRows, tagRows, providerRows, statusRows, total] = await Promise.all([
const [genreRows, tagRows, providerRows, statusRows, total, owned] = await Promise.all([
prisma.$queryRaw<{ genre: string }[]>`
SELECT DISTINCT unnest(t."genres") AS genre
FROM "LibraryEntry" e
@@ -193,6 +202,7 @@ export async function getLibraryFacets(userId: string): Promise<LibraryFacets> {
_count: { _all: true },
}),
prisma.libraryEntry.count({ where: { userId, deletedAt: null } }),
prisma.libraryEntry.count({ where: { userId, deletedAt: null, owned: true } }),
]);
const counts = {
@@ -211,6 +221,7 @@ export async function getLibraryFacets(userId: string): Promise<LibraryFacets> {
tags: tagRows.map((row) => row.name),
providers: providerRows.map((row) => row.name),
counts,
owned,
};
}
+18 -1
View File
@@ -135,6 +135,11 @@ const CSV_HEADERS: Record<string, string> = {
notiz: "notes",
notes: "notes",
kommentar: "notes",
habich: "owned",
owned: "owned",
besitz: "owned",
sammlung: "ownedSource",
quelle: "ownedSource",
};
/**
@@ -185,6 +190,8 @@ export async function importCsv(userId: string, text: string): Promise<ImportSum
rating: record.rating ? Number.parseInt(record.rating, 10) : null,
year: record.year ? Number.parseInt(record.year, 10) : null,
notes: record.notes || null,
owned: /^(ja|yes|true|1|x)$/i.test(record.owned ?? ""),
ownedSource: record.ownedSource || null,
});
if (result === "added") summary.added += 1;
else summary.skipped += 1;
@@ -299,6 +306,8 @@ type ImportRecord = {
rating: number | null;
year: number | null;
notes: string | null;
owned?: boolean;
ownedSource?: string | null;
};
/**
@@ -353,12 +362,14 @@ async function importOne(userId: string, record: ImportRecord): Promise<"added"
const entry = await addToLibrary(userId, titleId, record.status);
if (record.rating || record.notes) {
if (record.rating || record.notes || record.owned || record.ownedSource) {
await prisma.libraryEntry.update({
where: { id: entry.id },
data: {
rating: record.rating && record.rating >= 1 && record.rating <= 10 ? record.rating : null,
notes: record.notes,
owned: record.owned ?? false,
ownedSource: record.ownedSource ?? null,
},
});
}
@@ -394,6 +405,8 @@ export async function exportCsv(userId: string): Promise<string> {
"Status",
"Bewertung",
"Durchlaeufe",
"HabIch",
"Sammlung",
"WoGeschaut",
"Tags",
"Notiz",
@@ -410,6 +423,8 @@ export async function exportCsv(userId: string): Promise<string> {
entry.status,
entry.rating ?? "",
entry.currentRun + 1,
entry.owned ? "ja" : "nein",
entry.ownedSource ?? "",
entry.whereToWatch ?? "",
entry.tags.map((link) => link.tag.name).join(" "),
entry.notes?.replace(/\r?\n/g, " ") ?? "",
@@ -479,6 +494,8 @@ export async function exportJson(userId: string): Promise<object> {
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),