diff --git a/src/app/actions/config.ts b/src/app/actions/config.ts index 9f39aa9..9680b35 100644 --- a/src/app/actions/config.ts +++ b/src/app/actions/config.ts @@ -38,7 +38,7 @@ export async function testKomgaConnection( message: `Connexion réussie ! ${libraries.length} bibliothèque${libraries.length > 1 ? "s" : ""} trouvée${libraries.length > 1 ? "s" : ""}`, }; } catch (error) { - if (error instanceof AppError) { +if (error instanceof AppError) { return { success: false, message: error.message }; } return { success: false, message: "Erreur lors de la connexion" }; @@ -59,7 +59,7 @@ export async function saveKomgaConfig( revalidatePath("/settings"); return { success: true, message: "Configuration sauvegardée", data: mongoConfig }; } catch (error) { - if (error instanceof AppError) { +if (error instanceof AppError) { return { success: false, message: error.message }; } return { success: false, message: "Erreur lors de la sauvegarde" }; diff --git a/src/components/settings/BackgroundSettings.tsx b/src/components/settings/BackgroundSettings.tsx index 5a77628..59bd103 100644 --- a/src/components/settings/BackgroundSettings.tsx +++ b/src/components/settings/BackgroundSettings.tsx @@ -278,7 +278,7 @@ export function BackgroundSettings({ initialLibraries }: BackgroundSettingsProps htmlFor={`lib-${library.id}`} className="cursor-pointer font-normal text-sm" > - {library.name} ({library.bookCount} livres) + {library.name} ))} diff --git a/src/components/settings/KomgaSettings.tsx b/src/components/settings/KomgaSettings.tsx index 856275f..a6058ba 100644 --- a/src/components/settings/KomgaSettings.tsx +++ b/src/components/settings/KomgaSettings.tsx @@ -34,14 +34,8 @@ export function KomgaSettings({ initialConfig }: KomgaSettingsProps) { const handleTest = async () => { setIsLoading(true); - const form = document.querySelector("form") as HTMLFormElement; - const formData = new FormData(form); - const serverUrl = formData.get("serverUrl") as string; - const username = formData.get("username") as string; - const password = formData.get("password") as string; - try { - const result = await testKomgaConnection(serverUrl.trim(), username, password || config.password); + const result = await testKomgaConnection(config.serverUrl.trim(), config.username, config.password); if (!result.success) { throw new Error(result.message); @@ -55,8 +49,8 @@ export function KomgaSettings({ initialConfig }: KomgaSettingsProps) { logger.error({ err: error }, "Erreur:"); toast({ variant: "destructive", - title: t("settings.komga.error.title"), - description: t("settings.komga.error.message"), + title: t("settings.komga.error.connectionTitle"), + description: t("settings.komga.error.connectionMessage"), }); } finally { setIsLoading(false); diff --git a/src/i18n/messages/en/common.json b/src/i18n/messages/en/common.json index 5e0b7a4..104885f 100644 --- a/src/i18n/messages/en/common.json +++ b/src/i18n/messages/en/common.json @@ -134,7 +134,9 @@ }, "error": { "title": "Error saving configuration", - "message": "An error occurred while saving the configuration" + "message": "An error occurred while saving the configuration", + "connectionTitle": "Connection error", + "connectionMessage": "Unable to connect to the Komga server. Check the URL and credentials." } }, "cache": { @@ -361,7 +363,6 @@ "errors": { "MONGODB_MISSING_URI": "MongoDB URI missing", "MONGODB_CONNECTION_FAILED": "MongoDB connection failed", - "AUTH_UNAUTHENTICATED": "Unauthenticated", "AUTH_INVALID_CREDENTIALS": "Invalid credentials", "AUTH_PASSWORD_NOT_STRONG": "Password is not strong enough", @@ -371,7 +372,6 @@ "AUTH_LOGOUT_ERROR": "Error during logout", "AUTH_LOGIN_ERROR": "Error during login", "AUTH_REGISTER_ERROR": "Error during registration", - "KOMGA_MISSING_CONFIG": "Komga configuration missing", "KOMGA_MISSING_CREDENTIALS": "Komga credentials missing", "KOMGA_CONNECTION_ERROR": "Error connecting to Komga server", @@ -380,25 +380,20 @@ "STRIPSTREAM_MISSING_CONFIG": "Stripstream Librarian configuration missing", "STRIPSTREAM_CONNECTION_ERROR": "Error connecting to Stripstream Librarian", "STRIPSTREAM_HTTP_ERROR": "HTTP error while communicating with Stripstream Librarian", - "CONFIG_SAVE_ERROR": "Error saving configuration", "CONFIG_FETCH_ERROR": "Error fetching configuration", "CONFIG_TTL_SAVE_ERROR": "Error saving TTL configuration", "CONFIG_TTL_FETCH_ERROR": "Error fetching TTL configuration", - "LIBRARY_NOT_FOUND": "Library not found", "LIBRARY_FETCH_ERROR": "Error fetching library", "LIBRARY_SCAN_ERROR": "Error scanning library", - "SERIES_FETCH_ERROR": "Error fetching series", "SERIES_NO_BOOKS_FOUND": "No books found in series", - "BOOK_NOT_FOUND": "Book not found", "BOOK_PROGRESS_UPDATE_ERROR": "Error updating reading progress", "BOOK_PROGRESS_DELETE_ERROR": "Error deleting reading progress", "BOOK_PAGES_FETCH_ERROR": "Error fetching book pages", "BOOK_DOWNLOAD_CANCELLED": "Book download cancelled", - "FAVORITE_ADD_ERROR": "Error adding to favorites", "FAVORITE_DELETE_ERROR": "Error removing from favorites", "FAVORITE_FETCH_ERROR": "Error fetching favorites", @@ -406,26 +401,19 @@ "FAVORITE_NETWORK_ERROR": "Network error while accessing favorites", "FAVORITE_SERVER_ERROR": "Server error while accessing favorites", "FAVORITE_STATUS_CHECK_ERROR": "Error checking favorites status", - "PREFERENCES_FETCH_ERROR": "Error fetching preferences", "PREFERENCES_UPDATE_ERROR": "Error updating preferences", "PREFERENCES_CONTEXT_ERROR": "Preferences context error", - "UI_TABS_TRIGGER_ERROR": "Error triggering tabs", "UI_TABS_CONTENT_ERROR": "Error loading tabs content", - "IMAGE_FETCH_ERROR": "Error fetching image", - "HOME_FETCH_ERROR": "Error fetching home page", - "MIDDLEWARE_UNAUTHORIZED": "Unauthorized", "MIDDLEWARE_INVALID_TOKEN": "Invalid authentication token", "MIDDLEWARE_INVALID_SESSION": "Invalid session", - "CLIENT_FETCH_ERROR": "Error fetching data", "CLIENT_NETWORK_ERROR": "Network error", "CLIENT_REQUEST_FAILED": "Request failed", - "GENERIC_ERROR": "An error occurred" }, "reader": { diff --git a/src/i18n/messages/fr/common.json b/src/i18n/messages/fr/common.json index 1ee1e0c..14494fd 100644 --- a/src/i18n/messages/fr/common.json +++ b/src/i18n/messages/fr/common.json @@ -134,7 +134,9 @@ }, "error": { "title": "Erreur lors de la sauvegarde de la configuration", - "message": "Une erreur est survenue lors de la sauvegarde de la configuration" + "message": "Une erreur est survenue lors de la sauvegarde de la configuration", + "connectionTitle": "Erreur de connexion", + "connectionMessage": "Impossible de se connecter au serveur Komga. Vérifiez l'URL et les identifiants." } }, "cache": { @@ -359,7 +361,6 @@ "errors": { "MONGODB_MISSING_URI": "URI MongoDB manquante", "MONGODB_CONNECTION_FAILED": "Erreur lors de la connexion à MongoDB", - "AUTH_UNAUTHENTICATED": "Non authentifié", "AUTH_INVALID_CREDENTIALS": "Identifiants invalides", "AUTH_PASSWORD_NOT_STRONG": "Le mot de passe n'est pas assez fort", @@ -369,7 +370,6 @@ "AUTH_LOGOUT_ERROR": "Erreur lors de la déconnexion", "AUTH_LOGIN_ERROR": "Erreur lors de la connexion", "AUTH_REGISTER_ERROR": "Erreur lors de l'inscription", - "KOMGA_MISSING_CONFIG": "Configuration Komga manquante", "KOMGA_MISSING_CREDENTIALS": "Identifiants Komga manquants", "KOMGA_CONNECTION_ERROR": "Erreur de connexion au serveur Komga", @@ -378,25 +378,20 @@ "STRIPSTREAM_MISSING_CONFIG": "Configuration Stripstream Librarian manquante", "STRIPSTREAM_CONNECTION_ERROR": "Erreur de connexion à Stripstream Librarian", "STRIPSTREAM_HTTP_ERROR": "Erreur HTTP lors de la communication avec Stripstream Librarian", - "CONFIG_SAVE_ERROR": "Erreur lors de la sauvegarde de la configuration", "CONFIG_FETCH_ERROR": "Erreur lors de la récupération de la configuration", "CONFIG_TTL_SAVE_ERROR": "Erreur lors de la sauvegarde des TTL", "CONFIG_TTL_FETCH_ERROR": "Erreur lors de la récupération des TTL", - "LIBRARY_NOT_FOUND": "Bibliothèque introuvable", "LIBRARY_FETCH_ERROR": "Erreur lors de la récupération de la bibliothèque", "LIBRARY_SCAN_ERROR": "Erreur lors de l'analyse de la bibliothèque", - "SERIES_FETCH_ERROR": "Erreur lors de la récupération des séries", "SERIES_NO_BOOKS_FOUND": "Aucun livre trouvé dans la série", - "BOOK_NOT_FOUND": "Livre introuvable", "BOOK_PROGRESS_UPDATE_ERROR": "Erreur lors de la mise à jour de la progression", "BOOK_PROGRESS_DELETE_ERROR": "Erreur lors de la suppression de la progression", "BOOK_PAGES_FETCH_ERROR": "Erreur lors de la récupération des pages du livre", "BOOK_DOWNLOAD_CANCELLED": "Téléchargement du livre annulé", - "FAVORITE_ADD_ERROR": "Erreur lors de l'ajout aux favoris", "FAVORITE_DELETE_ERROR": "Erreur lors de la suppression des favoris", "FAVORITE_FETCH_ERROR": "Erreur lors de la récupération des favoris", @@ -404,26 +399,19 @@ "FAVORITE_NETWORK_ERROR": "Erreur réseau lors de l'accès aux favoris", "FAVORITE_SERVER_ERROR": "Erreur serveur lors de l'accès aux favoris", "FAVORITE_STATUS_CHECK_ERROR": "Erreur lors de la vérification du statut des favoris", - "PREFERENCES_FETCH_ERROR": "Erreur lors de la récupération des préférences", "PREFERENCES_UPDATE_ERROR": "Erreur lors de la mise à jour des préférences", "PREFERENCES_CONTEXT_ERROR": "Erreur de contexte des préférences", - "UI_TABS_TRIGGER_ERROR": "Erreur lors du déclenchement des onglets", "UI_TABS_CONTENT_ERROR": "Erreur lors du chargement du contenu des onglets", - "IMAGE_FETCH_ERROR": "Erreur lors de la récupération de l'image", - "HOME_FETCH_ERROR": "Erreur lors de la récupération de l'accueil", - "MIDDLEWARE_UNAUTHORIZED": "Non autorisé", "MIDDLEWARE_INVALID_TOKEN": "Jeton d'authentification invalide", "MIDDLEWARE_INVALID_SESSION": "Session invalide", - "CLIENT_FETCH_ERROR": "Erreur lors de la récupération des données", "CLIENT_NETWORK_ERROR": "Erreur réseau", "CLIENT_REQUEST_FAILED": "La requête a échoué", - "GENERIC_ERROR": "Une erreur est survenue" }, "reader": { diff --git a/src/lib/providers/komga/komga.provider.ts b/src/lib/providers/komga/komga.provider.ts index 8a3e006..dbed03e 100644 --- a/src/lib/providers/komga/komga.provider.ts +++ b/src/lib/providers/komga/komga.provider.ts @@ -17,6 +17,7 @@ import type { LibraryResponse } from "@/types/library"; import type { AuthConfig } from "@/types/auth"; import logger from "@/lib/logger"; import { HOME_CACHE_TAG, LIBRARY_SERIES_CACHE_TAG, SERIES_BOOKS_CACHE_TAG } from "@/constants/cacheConstants"; +import { unstable_cache } from "next/cache"; type KomgaCondition = Record; @@ -64,7 +65,6 @@ export class KomgaProvider implements IMediaProvider { const isDebug = process.env.KOMGA_DEBUG === "true"; const isCacheDebug = process.env.CACHE_DEBUG === "true"; - const startTime = isDebug ? Date.now() : 0; if (isDebug) { logger.info( @@ -72,8 +72,14 @@ export class KomgaProvider implements IMediaProvider { "🔵 Komga Request" ); } - if (isCacheDebug && options.revalidate) { - logger.info({ url, cache: "enabled", ttl: options.revalidate }, "💾 Cache enabled"); + if (isCacheDebug) { + if (options.tags) { + logger.info({ url, cache: "tags", tags: options.tags }, "💾 Cache tags"); + } else if (options.revalidate !== undefined) { + logger.info({ url, cache: "revalidate", ttl: options.revalidate }, "💾 Cache revalidate"); + } else { + logger.info({ url, cache: "none" }, "💾 Cache none"); + } } const nextOptions = options.tags @@ -88,6 +94,19 @@ export class KomgaProvider implements IMediaProvider { next: nextOptions, }; + // Next.js does not cache POST fetch requests — use unstable_cache to cache results instead + if (options.method === "POST" && nextOptions) { + const cacheKey = ["komga", this.config.authHeader, url, String(options.body ?? "")]; + return unstable_cache(() => this.executeRequest(url, fetchOptions), cacheKey, nextOptions)(); + } + + return this.executeRequest(url, fetchOptions); + } + + private async executeRequest(url: string, fetchOptions: RequestInit): Promise { + const isDebug = process.env.KOMGA_DEBUG === "true"; + const startTime = isDebug ? Date.now() : 0; + const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS); @@ -124,10 +143,6 @@ export class KomgaProvider implements IMediaProvider { "🟢 Komga Response" ); } - if (isCacheDebug && options.revalidate) { - const cacheStatus = response.headers.get("x-nextjs-cache") ?? "UNKNOWN"; - logger.info({ url, cacheStatus }, `💾 Cache ${cacheStatus}`); - } if (!response.ok) { if (isDebug) { @@ -163,25 +178,7 @@ export class KomgaProvider implements IMediaProvider { const raw = await this.fetch("libraries", undefined, { revalidate: CACHE_TTL_LONG, }); - // Enrich with book counts - const enriched = await Promise.all( - raw.map(async (lib) => { - try { - const resp = await this.fetch<{ totalElements: number }>( - "books", - { - library_id: lib.id, - size: "0", - }, - { revalidate: CACHE_TTL_LONG } - ); - return { ...lib, booksCount: resp.totalElements, booksReadCount: 0 } as KomgaLibrary; - } catch { - return { ...lib, booksCount: 0, booksReadCount: 0 } as KomgaLibrary; - } - }) - ); - return enriched.map(KomgaAdapter.toNormalizedLibrary); + return raw.map(KomgaAdapter.toNormalizedLibrary); } async getSeries(libraryId: string, cursor?: string, limit = 20, unreadOnly = false, search?: string): Promise { @@ -356,30 +353,8 @@ export class KomgaProvider implements IMediaProvider { } async getLibraryById(libraryId: string): Promise { - try { - const lib = await this.fetch(`libraries/${libraryId}`, undefined, { - revalidate: CACHE_TTL_LONG, - }); - try { - const resp = await this.fetch<{ totalElements: number }>( - "books", - { - library_id: lib.id, - size: "0", - }, - { revalidate: CACHE_TTL_LONG } - ); - return KomgaAdapter.toNormalizedLibrary({ - ...lib, - booksCount: resp.totalElements, - booksReadCount: 0, - }); - } catch { - return KomgaAdapter.toNormalizedLibrary({ ...lib, booksCount: 0, booksReadCount: 0 }); - } - } catch { - return null; - } + const libraries = await this.getLibraries(); + return libraries.find((lib) => lib.id === libraryId) ?? null; } async getNextBook(bookId: string): Promise { @@ -398,7 +373,14 @@ export class KomgaProvider implements IMediaProvider { } async getHomeData(): Promise { - const homeOpts = { revalidate: CACHE_TTL_MED, tags: [HOME_CACHE_TAG] }; + return unstable_cache( + () => this.fetchHomeData(), + ["komga-home", this.config.authHeader], + { revalidate: CACHE_TTL_MED, tags: [HOME_CACHE_TAG] } + )(); + } + + private async fetchHomeData(): Promise { const [ongoing, ongoingBooks, recentlyRead, onDeck, latestSeries] = await Promise.all([ this.fetch>( "series/list", @@ -408,7 +390,6 @@ export class KomgaProvider implements IMediaProvider { body: JSON.stringify({ condition: { readStatus: { operator: "is", value: "IN_PROGRESS" } }, }), - ...homeOpts, } ).catch(() => ({ content: [] as KomgaSeries[] })), this.fetch>( @@ -419,23 +400,19 @@ export class KomgaProvider implements IMediaProvider { body: JSON.stringify({ condition: { readStatus: { operator: "is", value: "IN_PROGRESS" } }, }), - ...homeOpts, } ).catch(() => ({ content: [] as KomgaBook[] })), this.fetch>( "books/latest", - { page: "0", size: "10", media_status: "READY" }, - { ...homeOpts } + { page: "0", size: "10", media_status: "READY" } ).catch(() => ({ content: [] as KomgaBook[] })), this.fetch>( "books/ondeck", - { page: "0", size: "10", media_status: "READY" }, - { ...homeOpts } + { page: "0", size: "10", media_status: "READY" } ).catch(() => ({ content: [] as KomgaBook[] })), this.fetch>( "series/latest", - { page: "0", size: "10", media_status: "READY" }, - { ...homeOpts } + { page: "0", size: "10", media_status: "READY" } ).catch(() => ({ content: [] as KomgaSeries[] })), ]); return { diff --git a/src/lib/providers/stripstream/stripstream.client.ts b/src/lib/providers/stripstream/stripstream.client.ts index 1f315ab..842039d 100644 --- a/src/lib/providers/stripstream/stripstream.client.ts +++ b/src/lib/providers/stripstream/stripstream.client.ts @@ -59,8 +59,14 @@ export class StripstreamClient { "🔵 Stripstream Request" ); } - if (isCacheDebug && options.revalidate) { - logger.info({ url, cache: "enabled", ttl: options.revalidate }, "💾 Cache enabled"); + if (isCacheDebug) { + if (options.tags) { + logger.info({ url, cache: "tags", tags: options.tags }, "💾 Cache tags"); + } else if (options.revalidate !== undefined) { + logger.info({ url, cache: "revalidate", ttl: options.revalidate }, "💾 Cache revalidate"); + } else { + logger.info({ url, cache: "none" }, "💾 Cache none"); + } } const nextOptions = options.tags @@ -106,10 +112,6 @@ export class StripstreamClient { "🟢 Stripstream Response" ); } - if (isCacheDebug && options.revalidate) { - const cacheStatus = response.headers.get("x-nextjs-cache") ?? "UNKNOWN"; - logger.info({ url, cacheStatus }, `💾 Cache ${cacheStatus}`); - } if (!response.ok) { if (isDebug) { diff --git a/src/lib/services/config-db.service.ts b/src/lib/services/config-db.service.ts index 9083aea..f73ae48 100644 --- a/src/lib/services/config-db.service.ts +++ b/src/lib/services/config-db.service.ts @@ -39,7 +39,7 @@ export class ConfigDBService { return config as KomgaConfig; } catch (error) { - if (error instanceof AppError) { +if (error instanceof AppError) { throw error; } throw new AppError(ERROR_CODES.CONFIG.SAVE_ERROR, {}, error);