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.
This commit is contained in:
+12
-1
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user