feat: add multi-provider support (Komga + Stripstream Librarian)
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
- Introduce provider abstraction layer (IMediaProvider, KomgaProvider, StripstreamProvider) - Add Stripstream Librarian as second media provider with full feature parity - Migrate all pages and components from direct Komga services to provider factory - Remove dead service code (BaseApiService, HomeService, LibraryService, SearchService, TestService) - Fix library/series page-based pagination for both providers (Komga 0-indexed, Stripstream 1-indexed) - Fix unread filter and search on library page for both providers - Fix read progress display for Stripstream (reading_status mapping) - Fix series read status (books_read_count) for Stripstream - Add global search with series results for Stripstream (series_hits from Meilisearch) - Fix thumbnail proxy to return 404 gracefully instead of JSON on upstream error - Replace duration-based cache debug detection with x-nextjs-cache header Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,36 +1,40 @@
|
||||
"use server";
|
||||
|
||||
import { BookService } from "@/lib/services/book.service";
|
||||
import { getProvider } from "@/lib/providers/provider.factory";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import type { KomgaBook } from "@/types/komga";
|
||||
import type { NormalizedBook } from "@/lib/providers/types";
|
||||
import logger from "@/lib/logger";
|
||||
|
||||
interface BookDataResult {
|
||||
success: boolean;
|
||||
data?: {
|
||||
book: KomgaBook;
|
||||
book: NormalizedBook;
|
||||
pages: number[];
|
||||
nextBook: KomgaBook | null;
|
||||
nextBook: NormalizedBook | null;
|
||||
};
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export async function getBookData(bookId: string): Promise<BookDataResult> {
|
||||
try {
|
||||
const data = await BookService.getBook(bookId);
|
||||
let nextBook = null;
|
||||
const provider = await getProvider();
|
||||
if (!provider) {
|
||||
return { success: false, message: "KOMGA_MISSING_CONFIG" };
|
||||
}
|
||||
|
||||
const book = await provider.getBook(bookId);
|
||||
const pages = Array.from({ length: book.pageCount }, (_, i) => i + 1);
|
||||
|
||||
let nextBook: NormalizedBook | null = null;
|
||||
try {
|
||||
nextBook = await BookService.getNextBook(bookId, data.book.seriesId);
|
||||
nextBook = await provider.getNextBook(bookId);
|
||||
} catch (error) {
|
||||
logger.warn({ err: error, bookId }, "Failed to fetch next book in server action");
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...data,
|
||||
nextBook,
|
||||
},
|
||||
data: { book, pages, nextBook },
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { ConfigDBService } from "@/lib/services/config-db.service";
|
||||
import { TestService } from "@/lib/services/test.service";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import type { KomgaConfig, KomgaConfigData, KomgaLibrary } from "@/types/komga";
|
||||
|
||||
interface SaveConfigInput {
|
||||
@@ -13,9 +13,6 @@ interface SaveConfigInput {
|
||||
authHeader?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Teste la connexion à Komga
|
||||
*/
|
||||
export async function testKomgaConnection(
|
||||
serverUrl: string,
|
||||
username: string,
|
||||
@@ -23,12 +20,19 @@ export async function testKomgaConnection(
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const authHeader = Buffer.from(`${username}:${password}`).toString("base64");
|
||||
|
||||
const { libraries }: { libraries: KomgaLibrary[] } = await TestService.testConnection({
|
||||
serverUrl,
|
||||
authHeader,
|
||||
const url = new URL(`${serverUrl}/api/v1/libraries`).toString();
|
||||
const headers = new Headers({
|
||||
Authorization: `Basic ${authHeader}`,
|
||||
Accept: "application/json",
|
||||
});
|
||||
|
||||
const response = await fetch(url, { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new AppError(ERROR_CODES.KOMGA.CONNECTION_ERROR);
|
||||
}
|
||||
|
||||
const libraries: KomgaLibrary[] = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
message: `Connexion réussie ! ${libraries.length} bibliothèque${libraries.length > 1 ? "s" : ""} trouvée${libraries.length > 1 ? "s" : ""}`,
|
||||
@@ -41,9 +45,6 @@ export async function testKomgaConnection(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde la configuration Komga
|
||||
*/
|
||||
export async function saveKomgaConfig(
|
||||
config: SaveConfigInput
|
||||
): Promise<{ success: boolean; message: string; data?: KomgaConfig }> {
|
||||
@@ -55,15 +56,8 @@ export async function saveKomgaConfig(
|
||||
authHeader: config.authHeader || "",
|
||||
};
|
||||
const mongoConfig = await ConfigDBService.saveConfig(configData);
|
||||
|
||||
// Invalider le cache
|
||||
revalidatePath("/settings");
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Configuration sauvegardée",
|
||||
data: mongoConfig,
|
||||
};
|
||||
return { success: true, message: "Configuration sauvegardée", data: mongoConfig };
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return { success: false, message: error.message };
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { LibraryService } from "@/lib/services/library.service";
|
||||
import { BookService } from "@/lib/services/book.service";
|
||||
import { getProvider } from "@/lib/providers/provider.factory";
|
||||
import { AppError } from "@/utils/errors";
|
||||
|
||||
/**
|
||||
* Lance un scan de bibliothèque
|
||||
*/
|
||||
export async function scanLibrary(
|
||||
libraryId: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await LibraryService.scanLibrary(libraryId, false);
|
||||
const provider = await getProvider();
|
||||
if (!provider) return { success: false, message: "Provider non configuré" };
|
||||
|
||||
await provider.scanLibrary(libraryId);
|
||||
|
||||
// Invalider le cache de la bibliothèque
|
||||
revalidatePath(`/libraries/${libraryId}`);
|
||||
revalidatePath("/libraries");
|
||||
|
||||
@@ -27,9 +25,6 @@ export async function scanLibrary(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne un livre aléatoire depuis les bibliothèques sélectionnées
|
||||
*/
|
||||
export async function getRandomBookFromLibraries(
|
||||
libraryIds: string[]
|
||||
): Promise<{ success: boolean; bookId?: string; message?: string }> {
|
||||
@@ -38,13 +33,15 @@ export async function getRandomBookFromLibraries(
|
||||
return { success: false, message: "Au moins une bibliothèque doit être sélectionnée" };
|
||||
}
|
||||
|
||||
const bookId = await BookService.getRandomBookFromLibraries(libraryIds);
|
||||
return { success: true, bookId };
|
||||
const provider = await getProvider();
|
||||
if (!provider) return { success: false, message: "Provider non configuré" };
|
||||
|
||||
const bookId = await provider.getRandomBook(libraryIds);
|
||||
return { success: true, bookId: bookId ?? undefined };
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
|
||||
return { success: false, message: "Erreur lors de la récupération d'un livre aléatoire" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
"use server";
|
||||
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { BookService } from "@/lib/services/book.service";
|
||||
import { LIBRARY_SERIES_CACHE_TAG } from "@/lib/services/library.service";
|
||||
import { getProvider } from "@/lib/providers/provider.factory";
|
||||
import { HOME_CACHE_TAG, LIBRARY_SERIES_CACHE_TAG, SERIES_BOOKS_CACHE_TAG } from "@/constants/cacheConstants";
|
||||
import { AppError } from "@/utils/errors";
|
||||
|
||||
const HOME_CACHE_TAG = "home-data";
|
||||
function revalidateReadCaches() {
|
||||
revalidateTag(HOME_CACHE_TAG, "max");
|
||||
revalidateTag(LIBRARY_SERIES_CACHE_TAG, "max");
|
||||
revalidateTag(SERIES_BOOKS_CACHE_TAG, "max");
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour la progression de lecture d'un livre
|
||||
* Note: ne pas utiliser "use server" avec redirect - on gère manuellement
|
||||
*/
|
||||
export async function updateReadProgress(
|
||||
bookId: string,
|
||||
page: number,
|
||||
completed: boolean = false
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await BookService.updateReadProgress(bookId, page, completed);
|
||||
const provider = await getProvider();
|
||||
if (!provider) return { success: false, message: "Provider non configuré" };
|
||||
|
||||
// Invalider le cache home et libraries (statut de lecture des séries)
|
||||
revalidateTag(HOME_CACHE_TAG, "max");
|
||||
revalidateTag(LIBRARY_SERIES_CACHE_TAG, "max");
|
||||
await provider.saveReadProgress(bookId, page, completed);
|
||||
revalidateReadCaches();
|
||||
|
||||
return { success: true, message: "Progression mise à jour" };
|
||||
} catch (error) {
|
||||
@@ -32,18 +32,15 @@ export async function updateReadProgress(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime la progression de lecture d'un livre
|
||||
*/
|
||||
export async function deleteReadProgress(
|
||||
bookId: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
await BookService.deleteReadProgress(bookId);
|
||||
const provider = await getProvider();
|
||||
if (!provider) return { success: false, message: "Provider non configuré" };
|
||||
|
||||
// Invalider le cache home et libraries (statut de lecture des séries)
|
||||
revalidateTag(HOME_CACHE_TAG, "max");
|
||||
revalidateTag(LIBRARY_SERIES_CACHE_TAG, "max");
|
||||
await provider.resetReadProgress(bookId);
|
||||
revalidateReadCaches();
|
||||
|
||||
return { success: true, message: "Progression supprimée" };
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath, revalidateTag } from "next/cache";
|
||||
import { LIBRARY_SERIES_CACHE_TAG } from "@/lib/services/library.service";
|
||||
|
||||
const HOME_CACHE_TAG = "home-data";
|
||||
import { HOME_CACHE_TAG, LIBRARY_SERIES_CACHE_TAG } from "@/constants/cacheConstants";
|
||||
|
||||
export type RefreshScope = "home" | "library" | "series";
|
||||
|
||||
|
||||
183
src/app/actions/stripstream-config.ts
Normal file
183
src/app/actions/stripstream-config.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { getCurrentUser } from "@/lib/auth-utils";
|
||||
import { StripstreamProvider } from "@/lib/providers/stripstream/stripstream.provider";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import type { ProviderType } from "@/lib/providers/types";
|
||||
|
||||
/**
|
||||
* Sauvegarde la configuration Stripstream
|
||||
*/
|
||||
export async function saveStripstreamConfig(
|
||||
url: string,
|
||||
token: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return { success: false, message: "Non authentifié" };
|
||||
}
|
||||
const userId = parseInt(user.id, 10);
|
||||
|
||||
await prisma.stripstreamConfig.upsert({
|
||||
where: { userId },
|
||||
update: { url, token },
|
||||
create: { userId, url, token },
|
||||
});
|
||||
|
||||
revalidatePath("/settings");
|
||||
return { success: true, message: "Configuration Stripstream sauvegardée" };
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
return { success: false, message: "Erreur lors de la sauvegarde" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Teste la connexion à Stripstream Librarian
|
||||
*/
|
||||
export async function testStripstreamConnection(
|
||||
url: string,
|
||||
token: string
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const provider = new StripstreamProvider(url, token);
|
||||
const result = await provider.testConnection();
|
||||
|
||||
if (!result.ok) {
|
||||
return { success: false, message: result.error ?? "Connexion échouée" };
|
||||
}
|
||||
|
||||
return { success: true, message: "Connexion Stripstream réussie !" };
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
return { success: false, message: "Erreur lors du test de connexion" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Définit le provider actif de l'utilisateur
|
||||
*/
|
||||
export async function setActiveProvider(
|
||||
provider: ProviderType
|
||||
): Promise<{ success: boolean; message: string }> {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return { success: false, message: "Non authentifié" };
|
||||
}
|
||||
const userId = parseInt(user.id, 10);
|
||||
|
||||
// Vérifier que le provider est configuré avant de l'activer
|
||||
if (provider === "komga") {
|
||||
const config = await prisma.komgaConfig.findUnique({ where: { userId } });
|
||||
if (!config) {
|
||||
return { success: false, message: "Komga n'est pas encore configuré" };
|
||||
}
|
||||
} else if (provider === "stripstream") {
|
||||
const config = await prisma.stripstreamConfig.findUnique({ where: { userId } });
|
||||
if (!config) {
|
||||
return { success: false, message: "Stripstream n'est pas encore configuré" };
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { activeProvider: provider },
|
||||
});
|
||||
|
||||
revalidatePath("/");
|
||||
revalidatePath("/settings");
|
||||
return {
|
||||
success: true,
|
||||
message: `Provider actif : ${provider === "komga" ? "Komga" : "Stripstream Librarian"}`,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return { success: false, message: error.message };
|
||||
}
|
||||
return { success: false, message: "Erreur lors du changement de provider" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la configuration Stripstream de l'utilisateur
|
||||
*/
|
||||
export async function getStripstreamConfig(): Promise<{
|
||||
url?: string;
|
||||
hasToken: boolean;
|
||||
} | null> {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) return null;
|
||||
const userId = parseInt(user.id, 10);
|
||||
|
||||
const config = await prisma.stripstreamConfig.findUnique({
|
||||
where: { userId },
|
||||
select: { url: true },
|
||||
});
|
||||
|
||||
if (!config) return null;
|
||||
return { url: config.url, hasToken: true };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le provider actif de l'utilisateur
|
||||
*/
|
||||
export async function getActiveProvider(): Promise<ProviderType> {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) return "komga";
|
||||
const userId = parseInt(user.id, 10);
|
||||
|
||||
const dbUser = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { activeProvider: true },
|
||||
});
|
||||
|
||||
return (dbUser?.activeProvider as ProviderType) ?? "komga";
|
||||
} catch {
|
||||
return "komga";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie quels providers sont configurés
|
||||
*/
|
||||
export async function getProvidersStatus(): Promise<{
|
||||
komgaConfigured: boolean;
|
||||
stripstreamConfigured: boolean;
|
||||
activeProvider: ProviderType;
|
||||
}> {
|
||||
try {
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return { komgaConfigured: false, stripstreamConfigured: false, activeProvider: "komga" };
|
||||
}
|
||||
const userId = parseInt(user.id, 10);
|
||||
|
||||
const [dbUser, komgaConfig, stripstreamConfig] = await Promise.all([
|
||||
prisma.user.findUnique({ where: { id: userId }, select: { activeProvider: true } }),
|
||||
prisma.komgaConfig.findUnique({ where: { userId }, select: { id: true } }),
|
||||
prisma.stripstreamConfig.findUnique({ where: { userId }, select: { id: true } }),
|
||||
]);
|
||||
|
||||
return {
|
||||
komgaConfigured: !!komgaConfig,
|
||||
stripstreamConfigured: !!stripstreamConfig,
|
||||
activeProvider: (dbUser?.activeProvider as ProviderType) ?? "komga",
|
||||
};
|
||||
} catch {
|
||||
return { komgaConfigured: false, stripstreamConfigured: false, activeProvider: "komga" };
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { SearchService } from "@/lib/services/search.service";
|
||||
import { AppError, getErrorMessage } from "@/utils/errors";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
|
||||
const MIN_QUERY_LENGTH = 2;
|
||||
const DEFAULT_LIMIT = 6;
|
||||
const MAX_LIMIT = 10;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const query = request.nextUrl.searchParams.get("q")?.trim() ?? "";
|
||||
const limitParam = request.nextUrl.searchParams.get("limit");
|
||||
const parsedLimit = limitParam ? Number(limitParam) : Number.NaN;
|
||||
const limit = Number.isFinite(parsedLimit)
|
||||
? Math.max(1, Math.min(parsedLimit, MAX_LIMIT))
|
||||
: DEFAULT_LIMIT;
|
||||
|
||||
if (query.length < MIN_QUERY_LENGTH) {
|
||||
return NextResponse.json({ series: [], books: [] }, { headers: { "Cache-Control": "no-store" } });
|
||||
}
|
||||
|
||||
const results = await SearchService.globalSearch(query, limit);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
series: results.series.map((series) => ({
|
||||
id: series.id,
|
||||
title: series.metadata.title,
|
||||
libraryId: series.libraryId,
|
||||
booksCount: series.booksCount,
|
||||
href: `/series/${series.id}`,
|
||||
coverUrl: `/api/komga/images/series/${series.id}/thumbnail`,
|
||||
})),
|
||||
books: results.books.map((book) => ({
|
||||
id: book.id,
|
||||
title: book.metadata.title || book.name,
|
||||
seriesTitle: book.seriesTitle,
|
||||
seriesId: book.seriesId,
|
||||
href: `/books/${book.id}`,
|
||||
coverUrl: `/api/komga/images/books/${book.id}/thumbnail`,
|
||||
})),
|
||||
},
|
||||
{ headers: { "Cache-Control": "no-store" } }
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: error.code,
|
||||
name: "Search fetch error",
|
||||
message: getErrorMessage(error.code),
|
||||
},
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: {
|
||||
code: ERROR_CODES.SERIES.FETCH_ERROR,
|
||||
name: "Search fetch error",
|
||||
message: getErrorMessage(ERROR_CODES.SERIES.FETCH_ERROR),
|
||||
},
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
44
src/app/api/provider/search/route.ts
Normal file
44
src/app/api/provider/search/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProvider } from "@/lib/providers/provider.factory";
|
||||
import { AppError, getErrorMessage } from "@/utils/errors";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
|
||||
const MIN_QUERY_LENGTH = 2;
|
||||
const DEFAULT_LIMIT = 6;
|
||||
const MAX_LIMIT = 10;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const query = request.nextUrl.searchParams.get("q")?.trim() ?? "";
|
||||
const limitParam = request.nextUrl.searchParams.get("limit");
|
||||
const parsedLimit = limitParam ? Number(limitParam) : Number.NaN;
|
||||
const limit = Number.isFinite(parsedLimit)
|
||||
? Math.max(1, Math.min(parsedLimit, MAX_LIMIT))
|
||||
: DEFAULT_LIMIT;
|
||||
|
||||
if (query.length < MIN_QUERY_LENGTH) {
|
||||
return NextResponse.json([], { headers: { "Cache-Control": "no-store" } });
|
||||
}
|
||||
|
||||
const provider = await getProvider();
|
||||
if (!provider) {
|
||||
return NextResponse.json([], { headers: { "Cache-Control": "no-store" } });
|
||||
}
|
||||
|
||||
const results = await provider.search(query, limit);
|
||||
|
||||
return NextResponse.json(results, { headers: { "Cache-Control": "no-store" } });
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return NextResponse.json(
|
||||
{ error: { code: error.code, message: getErrorMessage(error.code) } },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: { code: ERROR_CODES.CLIENT.FETCH_ERROR, message: "Search error" } },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getCurrentUser } from "@/lib/auth-utils";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { StripstreamClient } from "@/lib/providers/stripstream/stripstream.client";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { getErrorMessage } from "@/utils/errors";
|
||||
import logger from "@/lib/logger";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ bookId: string; pageNumber: string }> }
|
||||
) {
|
||||
try {
|
||||
const { bookId, pageNumber } = await params;
|
||||
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: { code: "AUTH_UNAUTHENTICATED" } }, { status: 401 });
|
||||
}
|
||||
|
||||
const userId = parseInt(user.id, 10);
|
||||
const config = await prisma.stripstreamConfig.findUnique({ where: { userId } });
|
||||
if (!config) {
|
||||
throw new AppError(ERROR_CODES.STRIPSTREAM.MISSING_CONFIG);
|
||||
}
|
||||
|
||||
const queryString = request.nextUrl.search.slice(1); // strip leading '?'
|
||||
const path = `books/${bookId}/pages/${pageNumber}${queryString ? `?${queryString}` : ""}`;
|
||||
|
||||
const client = new StripstreamClient(config.url, config.token);
|
||||
const response = await client.fetchImage(path);
|
||||
|
||||
const contentType = response.headers.get("content-type") ?? "image/jpeg";
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"Cache-Control": "public, max-age=86400",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, "Stripstream page fetch error");
|
||||
|
||||
if (error instanceof AppError) {
|
||||
return NextResponse.json(
|
||||
{ error: { code: error.code, message: getErrorMessage(error.code) } },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: { code: ERROR_CODES.IMAGE.FETCH_ERROR, message: "Image fetch error" } },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getCurrentUser } from "@/lib/auth-utils";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { StripstreamClient } from "@/lib/providers/stripstream/stripstream.client";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import logger from "@/lib/logger";
|
||||
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ bookId: string }> }
|
||||
) {
|
||||
try {
|
||||
const { bookId } = await params;
|
||||
|
||||
const user = await getCurrentUser();
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: { code: "AUTH_UNAUTHENTICATED" } }, { status: 401 });
|
||||
}
|
||||
|
||||
const userId = parseInt(user.id, 10);
|
||||
const config = await prisma.stripstreamConfig.findUnique({ where: { userId } });
|
||||
if (!config) {
|
||||
throw new AppError(ERROR_CODES.STRIPSTREAM.MISSING_CONFIG);
|
||||
}
|
||||
|
||||
const client = new StripstreamClient(config.url, config.token);
|
||||
const response = await client.fetchImage(`books/${bookId}/thumbnail`);
|
||||
|
||||
const contentType = response.headers.get("content-type") ?? "image/jpeg";
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
"Content-Type": contentType,
|
||||
"Cache-Control": "public, max-age=2592000, immutable",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, "Stripstream thumbnail fetch error");
|
||||
return new NextResponse(null, { status: 404 });
|
||||
}
|
||||
}
|
||||
44
src/app/api/stripstream/search/route.ts
Normal file
44
src/app/api/stripstream/search/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getProvider } from "@/lib/providers/provider.factory";
|
||||
import { AppError, getErrorMessage } from "@/utils/errors";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
|
||||
const MIN_QUERY_LENGTH = 2;
|
||||
const DEFAULT_LIMIT = 6;
|
||||
const MAX_LIMIT = 10;
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const query = request.nextUrl.searchParams.get("q")?.trim() ?? "";
|
||||
const limitParam = request.nextUrl.searchParams.get("limit");
|
||||
const parsedLimit = limitParam ? Number(limitParam) : Number.NaN;
|
||||
const limit = Number.isFinite(parsedLimit)
|
||||
? Math.max(1, Math.min(parsedLimit, MAX_LIMIT))
|
||||
: DEFAULT_LIMIT;
|
||||
|
||||
if (query.length < MIN_QUERY_LENGTH) {
|
||||
return NextResponse.json([], { headers: { "Cache-Control": "no-store" } });
|
||||
}
|
||||
|
||||
const provider = await getProvider();
|
||||
if (!provider) {
|
||||
throw new AppError(ERROR_CODES.STRIPSTREAM.MISSING_CONFIG);
|
||||
}
|
||||
|
||||
const results = await provider.search(query, limit);
|
||||
|
||||
return NextResponse.json(results, { headers: { "Cache-Control": "no-store" } });
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) {
|
||||
return NextResponse.json(
|
||||
{ error: { code: error.code, message: getErrorMessage(error.code) } },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: { code: ERROR_CODES.CLIENT.FETCH_ERROR, message: "Search error" } },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Suspense } from "react";
|
||||
import { ClientBookPage } from "@/components/reader/ClientBookPage";
|
||||
import { BookSkeleton } from "@/components/skeletons/BookSkeleton";
|
||||
import { BookService } from "@/lib/services/book.service";
|
||||
import { getProvider } from "@/lib/providers/provider.factory";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -11,23 +11,30 @@ export default async function BookPage({ params }: { params: Promise<{ bookId: s
|
||||
const { bookId } = await params;
|
||||
|
||||
try {
|
||||
// SSR: Fetch directly on server instead of client-side XHR
|
||||
const data = await BookService.getBook(bookId);
|
||||
const provider = await getProvider();
|
||||
if (!provider) redirect("/settings");
|
||||
|
||||
const book = await provider.getBook(bookId);
|
||||
const pages = Array.from({ length: book.pageCount }, (_, i) => i + 1);
|
||||
|
||||
let nextBook = null;
|
||||
try {
|
||||
nextBook = await BookService.getNextBook(bookId, data.book.seriesId);
|
||||
nextBook = await provider.getNextBook(bookId);
|
||||
} catch (error) {
|
||||
logger.warn({ err: error, bookId }, "Failed to fetch next book, continuing without it");
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<BookSkeleton />}>
|
||||
<ClientBookPage bookId={bookId} initialData={{ ...data, nextBook }} />
|
||||
<ClientBookPage bookId={bookId} initialData={{ book, pages, nextBook }} />
|
||||
</Suspense>
|
||||
);
|
||||
} catch (error) {
|
||||
// If config is missing, redirect to settings
|
||||
if (error instanceof AppError && error.code === ERROR_CODES.KOMGA.MISSING_CONFIG) {
|
||||
if (error instanceof AppError && (
|
||||
error.code === ERROR_CODES.KOMGA.MISSING_CONFIG ||
|
||||
error.code === ERROR_CODES.STRIPSTREAM.MISSING_CONFIG
|
||||
)) {
|
||||
redirect("/settings");
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { AuthProvider } from "@/components/providers/AuthProvider";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { defaultPreferences } from "@/types/preferences";
|
||||
import type { UserPreferences } from "@/types/preferences";
|
||||
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
||||
import type { NormalizedLibrary, NormalizedSeries } from "@/lib/providers/types";
|
||||
import logger from "@/lib/logger";
|
||||
|
||||
const inter = Inter({
|
||||
@@ -77,8 +77,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
|
||||
let preferences: UserPreferences = defaultPreferences;
|
||||
let userIsAdmin = false;
|
||||
let libraries: KomgaLibrary[] = [];
|
||||
let favorites: KomgaSeries[] = [];
|
||||
let libraries: NormalizedLibrary[] = [];
|
||||
let favorites: NormalizedSeries[] = [];
|
||||
|
||||
try {
|
||||
const currentUser = await import("@/lib/auth-utils").then((m) => m.getCurrentUser());
|
||||
@@ -86,7 +86,9 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
if (currentUser) {
|
||||
const [preferencesData, librariesData, favoritesData] = await Promise.allSettled([
|
||||
PreferencesService.getPreferences(),
|
||||
import("@/lib/services/library.service").then((m) => m.LibraryService.getLibraries()),
|
||||
import("@/lib/providers/provider.factory")
|
||||
.then((m) => m.getProvider())
|
||||
.then((provider) => provider?.getLibraries() ?? []),
|
||||
import("@/lib/services/favorites.service").then((m) =>
|
||||
m.FavoritesService.getFavorites({ requestPath, requestPathname })
|
||||
),
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { LibraryHeader } from "@/components/library/LibraryHeader";
|
||||
import { PaginatedSeriesGrid } from "@/components/library/PaginatedSeriesGrid";
|
||||
import { Container } from "@/components/ui/container";
|
||||
import type { KomgaLibrary } from "@/types/komga";
|
||||
import type { LibraryResponse } from "@/types/library";
|
||||
import type { Series } from "@/types/series";
|
||||
import type { NormalizedLibrary, NormalizedSeriesPage } from "@/lib/providers/types";
|
||||
import type { UserPreferences } from "@/types/preferences";
|
||||
|
||||
interface LibraryContentProps {
|
||||
library: KomgaLibrary;
|
||||
series: LibraryResponse<Series>;
|
||||
library: NormalizedLibrary;
|
||||
series: NormalizedSeriesPage;
|
||||
currentPage: number;
|
||||
preferences: UserPreferences;
|
||||
unreadOnly: boolean;
|
||||
@@ -28,15 +26,15 @@ export function LibraryContent({
|
||||
<>
|
||||
<LibraryHeader
|
||||
library={library}
|
||||
seriesCount={series.totalElements}
|
||||
series={series.content || []}
|
||||
seriesCount={series.totalElements ?? series.items.length}
|
||||
series={series.items}
|
||||
/>
|
||||
<Container>
|
||||
<PaginatedSeriesGrid
|
||||
series={series.content || []}
|
||||
series={series.items}
|
||||
currentPage={currentPage}
|
||||
totalPages={series.totalPages}
|
||||
totalElements={series.totalElements}
|
||||
totalPages={series.totalPages ?? 1}
|
||||
totalElements={series.totalElements ?? series.items.length}
|
||||
defaultShowOnlyUnread={preferences.showOnlyUnread}
|
||||
showOnlyUnread={unreadOnly}
|
||||
pageSize={pageSize}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { PreferencesService } from "@/lib/services/preferences.service";
|
||||
import { LibraryService } from "@/lib/services/library.service";
|
||||
import { getProvider } from "@/lib/providers/provider.factory";
|
||||
import { LibraryClientWrapper } from "./LibraryClientWrapper";
|
||||
import { LibraryContent } from "./LibraryContent";
|
||||
import { ErrorMessage } from "@/components/ui/ErrorMessage";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import type { UserPreferences } from "@/types/preferences";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ libraryId: string }>;
|
||||
@@ -17,9 +18,9 @@ const DEFAULT_PAGE_SIZE = 20;
|
||||
export default async function LibraryPage({ params, searchParams }: PageProps) {
|
||||
const libraryId = (await params).libraryId;
|
||||
const unread = (await searchParams).unread;
|
||||
const search = (await searchParams).search;
|
||||
const page = (await searchParams).page;
|
||||
const size = (await searchParams).size;
|
||||
const search = (await searchParams).search;
|
||||
|
||||
const currentPage = page ? parseInt(page) : 1;
|
||||
const preferences: UserPreferences = await PreferencesService.getPreferences();
|
||||
@@ -31,31 +32,36 @@ export default async function LibraryPage({ params, searchParams }: PageProps) {
|
||||
: preferences.displayMode?.itemsPerPage || DEFAULT_PAGE_SIZE;
|
||||
|
||||
try {
|
||||
const [series, library] = await Promise.all([
|
||||
LibraryService.getLibrarySeries(
|
||||
libraryId,
|
||||
currentPage - 1,
|
||||
effectivePageSize,
|
||||
unreadOnly,
|
||||
search
|
||||
),
|
||||
LibraryService.getLibrary(libraryId),
|
||||
const provider = await getProvider();
|
||||
if (!provider) redirect("/settings");
|
||||
|
||||
const [seriesPage, library] = await Promise.all([
|
||||
provider.getSeries(libraryId, String(currentPage), effectivePageSize, unreadOnly, search),
|
||||
provider.getLibraryById(libraryId),
|
||||
]);
|
||||
|
||||
if (!library) throw new AppError(ERROR_CODES.LIBRARY.NOT_FOUND);
|
||||
|
||||
return (
|
||||
<LibraryClientWrapper libraryId={libraryId}>
|
||||
<LibraryContent
|
||||
library={library}
|
||||
series={series}
|
||||
series={seriesPage}
|
||||
currentPage={currentPage}
|
||||
preferences={preferences}
|
||||
unreadOnly={unreadOnly}
|
||||
search={search}
|
||||
pageSize={effectivePageSize}
|
||||
/>
|
||||
</LibraryClientWrapper>
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof AppError && (
|
||||
error.code === ERROR_CODES.KOMGA.MISSING_CONFIG ||
|
||||
error.code === ERROR_CODES.STRIPSTREAM.MISSING_CONFIG
|
||||
)) {
|
||||
redirect("/settings");
|
||||
}
|
||||
|
||||
const errorCode = error instanceof AppError ? error.code : ERROR_CODES.SERIES.FETCH_ERROR;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HomeService } from "@/lib/services/home.service";
|
||||
import { getProvider } from "@/lib/providers/provider.factory";
|
||||
import { HomeContent } from "@/components/home/HomeContent";
|
||||
import { HomeClientWrapper } from "@/components/home/HomeClientWrapper";
|
||||
import { ErrorMessage } from "@/components/ui/ErrorMessage";
|
||||
@@ -8,7 +8,10 @@ import { redirect } from "next/navigation";
|
||||
|
||||
export default async function HomePage() {
|
||||
try {
|
||||
const data = await HomeService.getHomeData();
|
||||
const provider = await getProvider();
|
||||
if (!provider) redirect("/settings");
|
||||
|
||||
const data = await provider.getHomeData();
|
||||
|
||||
return (
|
||||
<HomeClientWrapper>
|
||||
@@ -16,13 +19,14 @@ export default async function HomePage() {
|
||||
</HomeClientWrapper>
|
||||
);
|
||||
} catch (error) {
|
||||
// Si la config Komga est manquante, rediriger vers les settings
|
||||
if (error instanceof AppError && error.code === ERROR_CODES.KOMGA.MISSING_CONFIG) {
|
||||
if (error instanceof AppError && (
|
||||
error.code === ERROR_CODES.KOMGA.MISSING_CONFIG ||
|
||||
error.code === ERROR_CODES.STRIPSTREAM.MISSING_CONFIG
|
||||
)) {
|
||||
redirect("/settings");
|
||||
}
|
||||
|
||||
// Afficher une erreur pour les autres cas
|
||||
const errorCode = error instanceof AppError ? error.code : ERROR_CODES.KOMGA.SERVER_UNREACHABLE;
|
||||
const errorCode = error instanceof AppError ? error.code : ERROR_CODES.HOME.FETCH_ERROR;
|
||||
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
|
||||
@@ -4,13 +4,12 @@ import { PaginatedBookGrid } from "@/components/series/PaginatedBookGrid";
|
||||
import { SeriesHeader } from "@/components/series/SeriesHeader";
|
||||
import { Container } from "@/components/ui/container";
|
||||
import { useRefresh } from "@/contexts/RefreshContext";
|
||||
import type { LibraryResponse } from "@/types/library";
|
||||
import type { KomgaBook, KomgaSeries } from "@/types/komga";
|
||||
import type { NormalizedBooksPage, NormalizedSeries } from "@/lib/providers/types";
|
||||
import type { UserPreferences } from "@/types/preferences";
|
||||
|
||||
interface SeriesContentProps {
|
||||
series: KomgaSeries;
|
||||
books: LibraryResponse<KomgaBook>;
|
||||
series: NormalizedSeries;
|
||||
books: NormalizedBooksPage;
|
||||
currentPage: number;
|
||||
preferences: UserPreferences;
|
||||
unreadOnly: boolean;
|
||||
@@ -37,10 +36,10 @@ export function SeriesContent({
|
||||
/>
|
||||
<Container>
|
||||
<PaginatedBookGrid
|
||||
books={books.content || []}
|
||||
books={books.items}
|
||||
currentPage={currentPage}
|
||||
totalPages={books.totalPages}
|
||||
totalElements={books.totalElements}
|
||||
totalPages={books.totalPages ?? 1}
|
||||
totalElements={books.totalElements ?? books.items.length}
|
||||
defaultShowOnlyUnread={preferences.showOnlyUnread}
|
||||
showOnlyUnread={unreadOnly}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PreferencesService } from "@/lib/services/preferences.service";
|
||||
import { SeriesService } from "@/lib/services/series.service";
|
||||
import { getProvider } from "@/lib/providers/provider.factory";
|
||||
|
||||
import { FavoriteService } from "@/lib/services/favorite.service";
|
||||
import { SeriesClientWrapper } from "./SeriesClientWrapper";
|
||||
import { SeriesContent } from "./SeriesContent";
|
||||
@@ -7,6 +8,7 @@ import { ErrorMessage } from "@/components/ui/ErrorMessage";
|
||||
import { AppError } from "@/utils/errors";
|
||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||
import type { UserPreferences } from "@/types/preferences";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ seriesId: string }>;
|
||||
@@ -18,28 +20,38 @@ const DEFAULT_PAGE_SIZE = 20;
|
||||
export default async function SeriesPage({ params, searchParams }: PageProps) {
|
||||
const seriesId = (await params).seriesId;
|
||||
const page = (await searchParams).page;
|
||||
const unread = (await searchParams).unread;
|
||||
const size = (await searchParams).size;
|
||||
|
||||
const unread = (await searchParams).unread;
|
||||
const currentPage = page ? parseInt(page) : 1;
|
||||
const preferences: UserPreferences = await PreferencesService.getPreferences();
|
||||
|
||||
// Utiliser le paramètre d'URL s'il existe, sinon utiliser la préférence utilisateur
|
||||
const unreadOnly = unread !== undefined ? unread === "true" : preferences.showOnlyUnread;
|
||||
const effectivePageSize = size ? parseInt(size) : preferences.displayMode?.itemsPerPage || DEFAULT_PAGE_SIZE;
|
||||
const effectivePageSize = size
|
||||
? parseInt(size)
|
||||
: preferences.displayMode?.itemsPerPage || DEFAULT_PAGE_SIZE;
|
||||
|
||||
try {
|
||||
const [books, series, isFavorite] = await Promise.all([
|
||||
SeriesService.getSeriesBooks(seriesId, currentPage - 1, effectivePageSize, unreadOnly),
|
||||
SeriesService.getSeries(seriesId),
|
||||
const provider = await getProvider();
|
||||
if (!provider) redirect("/settings");
|
||||
|
||||
const [booksPage, series, isFavorite] = await Promise.all([
|
||||
provider.getBooks({
|
||||
seriesName: seriesId,
|
||||
cursor: String(currentPage),
|
||||
limit: effectivePageSize,
|
||||
unreadOnly,
|
||||
}),
|
||||
provider.getSeriesById(seriesId),
|
||||
FavoriteService.isFavorite(seriesId),
|
||||
]);
|
||||
|
||||
if (!series) throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR);
|
||||
|
||||
return (
|
||||
<SeriesClientWrapper seriesId={seriesId}>
|
||||
<SeriesContent
|
||||
series={series}
|
||||
books={books}
|
||||
books={booksPage}
|
||||
currentPage={currentPage}
|
||||
preferences={preferences}
|
||||
unreadOnly={unreadOnly}
|
||||
@@ -49,10 +61,16 @@ export default async function SeriesPage({ params, searchParams }: PageProps) {
|
||||
</SeriesClientWrapper>
|
||||
);
|
||||
} catch (error) {
|
||||
const errorCode = error instanceof AppError
|
||||
? error.code
|
||||
: ERROR_CODES.BOOK.PAGES_FETCH_ERROR;
|
||||
|
||||
if (
|
||||
error instanceof AppError &&
|
||||
(error.code === ERROR_CODES.KOMGA.MISSING_CONFIG ||
|
||||
error.code === ERROR_CODES.STRIPSTREAM.MISSING_CONFIG)
|
||||
) {
|
||||
redirect("/settings");
|
||||
}
|
||||
|
||||
const errorCode = error instanceof AppError ? error.code : ERROR_CODES.BOOK.PAGES_FETCH_ERROR;
|
||||
|
||||
return (
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<ErrorMessage errorCode={errorCode} />
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ConfigDBService } from "@/lib/services/config-db.service";
|
||||
import { LibraryService } from "@/lib/services/library.service";
|
||||
import { ClientSettings } from "@/components/settings/ClientSettings";
|
||||
import { getProvider } from "@/lib/providers/provider.factory";
|
||||
import { getStripstreamConfig, getProvidersStatus } from "@/app/actions/stripstream-config";
|
||||
import type { Metadata } from "next";
|
||||
import type { KomgaConfig, KomgaLibrary } from "@/types/komga";
|
||||
import type { KomgaConfig } from "@/types/komga";
|
||||
import type { NormalizedLibrary } from "@/lib/providers/types";
|
||||
import logger from "@/lib/logger";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -14,10 +16,15 @@ export const metadata: Metadata = {
|
||||
|
||||
export default async function SettingsPage() {
|
||||
let config: KomgaConfig | null = null;
|
||||
let libraries: KomgaLibrary[] = [];
|
||||
let libraries: NormalizedLibrary[] = [];
|
||||
let stripstreamConfig: { url?: string; hasToken: boolean } | null = null;
|
||||
let providersStatus: {
|
||||
komgaConfigured: boolean;
|
||||
stripstreamConfigured: boolean;
|
||||
activeProvider: "komga" | "stripstream";
|
||||
} | undefined = undefined;
|
||||
|
||||
try {
|
||||
// Récupérer la configuration Komga
|
||||
const mongoConfig: KomgaConfig | null = await ConfigDBService.getConfig();
|
||||
if (mongoConfig) {
|
||||
config = {
|
||||
@@ -29,11 +36,31 @@ export default async function SettingsPage() {
|
||||
};
|
||||
}
|
||||
|
||||
libraries = await LibraryService.getLibraries();
|
||||
const [provider, stConfig, status] = await Promise.allSettled([
|
||||
getProvider().then((p) => p?.getLibraries() ?? []),
|
||||
getStripstreamConfig(),
|
||||
getProvidersStatus(),
|
||||
]);
|
||||
|
||||
if (provider.status === "fulfilled") {
|
||||
libraries = provider.value;
|
||||
}
|
||||
if (stConfig.status === "fulfilled") {
|
||||
stripstreamConfig = stConfig.value;
|
||||
}
|
||||
if (status.status === "fulfilled") {
|
||||
providersStatus = status.value;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, "Erreur lors de la récupération de la configuration:");
|
||||
// On ne fait rien si la config n'existe pas, on laissera le composant client gérer l'état initial
|
||||
}
|
||||
|
||||
return <ClientSettings initialConfig={config} initialLibraries={libraries} />;
|
||||
return (
|
||||
<ClientSettings
|
||||
initialConfig={config}
|
||||
initialLibraries={libraries}
|
||||
stripstreamConfig={stripstreamConfig}
|
||||
providersStatus={providersStatus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user