diff --git a/devbook.md b/devbook.md index 1b75309..1ab83cb 100644 --- a/devbook.md +++ b/devbook.md @@ -37,9 +37,11 @@ Créer une application web moderne avec Next.js permettant de lire des fichiers ### Améliorations techniques -- [ ] Refactorisation des services API - - [ ] Mutualisation du code de gestion des cookies - - [ ] Création d'un middleware commun +- [x] Refactorisation des services API + - [x] Création d'un service de base avec gestion des cookies + - [x] Création d'un middleware commun + - [x] Mutualisation du code de gestion du cache + - [x] Création des services spécialisés (Library, Series, Book, Home) - [ ] Mise à jour des API deprecated - [ ] Synchronisation de l'état de lecture avec Komga - [ ] Revue du système de cache diff --git a/src/app/api/komga/books/[bookId]/route.ts b/src/app/api/komga/books/[bookId]/route.ts index d49770e..6df2218 100644 --- a/src/app/api/komga/books/[bookId]/route.ts +++ b/src/app/api/komga/books/[bookId]/route.ts @@ -1,104 +1,12 @@ import { NextResponse } from "next/server"; -import { cookies } from "next/headers"; -import { config } from "@/lib/config"; -import { serverCacheService } from "@/lib/services/server-cache.service"; +import { BookService } from "@/lib/services/book.service"; export async function GET(request: Request, { params }: { params: { bookId: string } }) { try { - // Récupérer les credentials Komga depuis le cookie - const cookieStore = cookies(); - const configCookie = cookieStore.get("komgaCredentials"); - console.log("API Books - Cookie komgaCredentials:", configCookie?.value); - - if (!configCookie) { - console.log("API Books - Cookie komgaCredentials manquant"); - return NextResponse.json({ error: "Configuration Komga manquante" }, { status: 401 }); - } - - let komgaConfig; - try { - komgaConfig = JSON.parse(atob(configCookie.value)); - console.log("API Books - Config décodée:", { - serverUrl: komgaConfig.serverUrl, - hasCredentials: !!komgaConfig.credentials, - }); - } catch (error) { - console.error("API Books - Erreur de décodage du cookie:", error); - return NextResponse.json({ error: "Configuration Komga invalide" }, { status: 401 }); - } - - if (!komgaConfig.credentials?.username || !komgaConfig.credentials?.password) { - console.log("API Books - Credentials manquants dans la config"); - return NextResponse.json({ error: "Credentials Komga manquants" }, { status: 401 }); - } - - const auth = Buffer.from( - `${komgaConfig.credentials.username}:${komgaConfig.credentials.password}` - ).toString("base64"); - - console.log("API Books - Appel à l'API Komga pour le livre:", params.bookId); - - // Clé de cache unique pour ce livre - const cacheKey = `book-${params.bookId}`; - - // Fonction pour récupérer les données du livre - const fetchBookData = async () => { - // Récupération des détails du tome - const bookResponse = await fetch(`${komgaConfig.serverUrl}/api/v1/books/${params.bookId}`, { - headers: { - Authorization: `Basic ${auth}`, - }, - }); - - if (!bookResponse.ok) { - console.error("API Books - Erreur de l'API Komga (book):", { - status: bookResponse.status, - statusText: bookResponse.statusText, - }); - throw new Error("Erreur lors de la récupération des détails du tome"); - } - - const book = await bookResponse.json(); - - // Récupération des pages du tome - const pagesResponse = await fetch( - `${komgaConfig.serverUrl}/api/v1/books/${params.bookId}/pages`, - { - headers: { - Authorization: `Basic ${auth}`, - }, - } - ); - - if (!pagesResponse.ok) { - console.error("API Books - Erreur de l'API Komga (pages):", { - status: pagesResponse.status, - statusText: pagesResponse.statusText, - }); - throw new Error("Erreur lors de la récupération des pages du tome"); - } - - const pages = await pagesResponse.json(); - - // Retourner les données combinées - return { - book, - pages: pages.map((page: any) => page.number), - }; - }; - - // Récupérer les données du cache ou faire l'appel API - const data = await serverCacheService.getOrSet(cacheKey, fetchBookData, 5 * 60); // Cache de 5 minutes - + const data = await BookService.getBook(params.bookId); return NextResponse.json(data); } catch (error) { console.error("API Books - Erreur:", error); - return NextResponse.json( - { - error: - error instanceof Error ? error.message : "Erreur lors de la récupération des données", - }, - { status: 500 } - ); + return NextResponse.json({ error: "Erreur lors de la récupération du tome" }, { status: 500 }); } } diff --git a/src/app/api/komga/home/route.ts b/src/app/api/komga/home/route.ts index 338eca2..4c75b03 100644 --- a/src/app/api/komga/home/route.ts +++ b/src/app/api/komga/home/route.ts @@ -1,119 +1,12 @@ import { NextResponse } from "next/server"; -import { cookies } from "next/headers"; -import { redirect } from "next/navigation"; -import { config } from "@/lib/config"; -import { cacheService } from "@/lib/services/cache.service"; +import { HomeService } from "@/lib/services/home.service"; export async function GET() { try { - // Récupérer les credentials Komga depuis le cookie - const cookieStore = cookies(); - const configCookie = cookieStore.get("komgaCredentials"); - console.log("API Home - Cookie komgaCredentials:", configCookie?.value); - - if (!configCookie) { - console.log("API Home - Cookie komgaCredentials manquant"); - return NextResponse.json({ error: "Configuration Komga manquante" }, { status: 401 }); - } - - let komgaConfig; - try { - komgaConfig = JSON.parse(atob(configCookie.value)); - console.log("API Home - Config décodée:", { - serverUrl: komgaConfig.serverUrl, - hasCredentials: !!komgaConfig.credentials, - }); - } catch (error) { - console.error("API Home - Erreur de décodage du cookie:", error); - return NextResponse.json({ error: "Configuration Komga invalide" }, { status: 401 }); - } - - if (!komgaConfig.credentials?.username || !komgaConfig.credentials?.password) { - console.log("API Home - Credentials manquants dans la config"); - return NextResponse.json({ error: "Credentials Komga manquants" }, { status: 401 }); - } - - const auth = Buffer.from( - `${komgaConfig.credentials.username}:${komgaConfig.credentials.password}` - ).toString("base64"); - - console.log("API Home - Début des appels API"); - - try { - // Appels API parallèles - const [ongoingResponse, recentlyReadResponse, popularResponse] = await Promise.all([ - // Séries en cours - fetch( - `${komgaConfig.serverUrl}/api/v1/series?read_status=IN_PROGRESS&sort=readDate,desc&page=0&size=20&media_status=READY`, - { - headers: { - Authorization: `Basic ${auth}`, - }, - cache: "no-store", // Désactiver le cache - } - ).catch((error) => { - console.error("API Home - Erreur fetch ongoing:", error); - throw error; - }), - // Derniers livres lus - fetch( - `${komgaConfig.serverUrl}/api/v1/books?read_status=READ&sort=readDate,desc&page=0&size=20`, - { - headers: { - Authorization: `Basic ${auth}`, - }, - cache: "no-store", // Désactiver le cache - } - ).catch((error) => { - console.error("API Home - Erreur fetch recently read:", error); - throw error; - }), - // Séries populaires - fetch( - `${komgaConfig.serverUrl}/api/v1/series?page=0&size=20&sort=metadata.titleSort,asc&media_status=READY`, - { - headers: { - Authorization: `Basic ${auth}`, - }, - cache: "no-store", // Désactiver le cache - } - ).catch((error) => { - console.error("API Home - Erreur fetch popular:", error); - throw error; - }), - ]); - - console.log("API Home - Status des réponses:", { - ongoing: ongoingResponse.status, - recentlyRead: recentlyReadResponse.status, - popular: popularResponse.status, - }); - - // Vérifier les réponses et récupérer les données - const [ongoing, recentlyRead, popular] = await Promise.all([ - ongoingResponse.json(), - recentlyReadResponse.json(), - popularResponse.json(), - ]); - - console.log("API Home - Données récupérées:", { - ongoingCount: ongoing.content?.length || 0, - recentlyReadCount: recentlyRead.content?.length || 0, - popularCount: popular.content?.length || 0, - }); - - // Retourner les données - return NextResponse.json({ - ongoing: ongoing.content || [], - recentlyRead: recentlyRead.content || [], - popular: popular.content || [], - }); - } catch (error) { - console.error("API Home - Erreur lors de la récupération des données:", error); - throw error; - } + const data = await HomeService.getHomeData(); + return NextResponse.json(data); } catch (error) { - console.error("API Home - Erreur générale:", error); + console.error("API Home - Erreur:", error); return NextResponse.json( { error: "Erreur lors de la récupération des données" }, { status: 500 } diff --git a/src/app/api/komga/libraries/[libraryId]/series/route.ts b/src/app/api/komga/libraries/[libraryId]/series/route.ts index 495bfbe..3b5f147 100644 --- a/src/app/api/komga/libraries/[libraryId]/series/route.ts +++ b/src/app/api/komga/libraries/[libraryId]/series/route.ts @@ -1,77 +1,20 @@ import { NextResponse } from "next/server"; -import { cookies } from "next/headers"; -import { serverCacheService } from "@/lib/services/server-cache.service"; +import { LibraryService } from "@/lib/services/library.service"; export async function GET(request: Request, { params }: { params: { libraryId: string } }) { try { - // Récupérer les credentials Komga depuis le cookie - const configCookie = cookies().get("komgaCredentials"); - if (!configCookie) { - return NextResponse.json({ error: "Configuration Komga manquante" }, { status: 401 }); - } - - let config; - try { - config = JSON.parse(atob(configCookie.value)); - } catch (error) { - return NextResponse.json({ error: "Configuration Komga invalide" }, { status: 401 }); - } - - if (!config.credentials?.username || !config.credentials?.password) { - return NextResponse.json({ error: "Credentials Komga manquants" }, { status: 401 }); - } - // Récupérer les paramètres de pagination et de filtre depuis l'URL const { searchParams } = new URL(request.url); - const page = searchParams.get("page") || "0"; - const size = searchParams.get("size") || "20"; + const page = parseInt(searchParams.get("page") || "0"); + const size = parseInt(searchParams.get("size") || "20"); const unreadOnly = searchParams.get("unread") === "true"; - // Clé de cache unique pour cette page de séries - const cacheKey = `library-${params.libraryId}-series-${page}-${size}-${unreadOnly}`; - - // Fonction pour récupérer les séries - const fetchSeries = async () => { - // Construire l'URL avec les paramètres - let url = `${config.serverUrl}/api/v1/series?library_id=${params.libraryId}&page=${page}&size=${size}`; - - // Ajouter le filtre pour les séries non lues et en cours si nécessaire - if (unreadOnly) { - url += "&read_status=UNREAD&read_status=IN_PROGRESS"; - } - - const response = await fetch(url, { - headers: { - Authorization: `Basic ${Buffer.from( - `${config.credentials.username}:${config.credentials.password}` - ).toString("base64")}`, - }, - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => null); - throw new Error( - JSON.stringify({ - error: "Erreur lors de la récupération des séries", - details: errorData, - }) - ); - } - - return response.json(); - }; - - // Récupérer les données du cache ou faire l'appel API - const data = await serverCacheService.getOrSet(cacheKey, fetchSeries, 5 * 60); // Cache de 5 minutes - - return NextResponse.json(data); + const series = await LibraryService.getLibrarySeries(params.libraryId, page, size, unreadOnly); + return NextResponse.json(series); } catch (error) { - console.error("Erreur lors de la récupération des séries:", error); + console.error("API Library Series - Erreur:", error); return NextResponse.json( - { - error: "Erreur serveur", - details: error instanceof Error ? error.message : "Erreur inconnue", - }, + { error: "Erreur lors de la récupération des séries" }, { status: 500 } ); } diff --git a/src/app/api/komga/libraries/route.ts b/src/app/api/komga/libraries/route.ts index 39c111e..7489d7f 100644 --- a/src/app/api/komga/libraries/route.ts +++ b/src/app/api/komga/libraries/route.ts @@ -1,64 +1,12 @@ import { NextResponse } from "next/server"; -import { cookies } from "next/headers"; -import { AuthConfig } from "@/types/auth"; -import { serverCacheService } from "@/lib/services/server-cache.service"; +import { LibraryService } from "@/lib/services/library.service"; export async function GET() { try { - // Vérifier l'authentification de l'utilisateur - const userCookie = cookies().get("komgaUser"); - if (!userCookie) { - return NextResponse.json({ error: "Non authentifié" }, { status: 401 }); - } - - try { - const userData = JSON.parse(atob(userCookie.value)); - if (!userData.authenticated) { - throw new Error("User not authenticated"); - } - } catch (error) { - return NextResponse.json({ error: "Session invalide" }, { status: 401 }); - } - - // Récupérer les credentials Komga depuis le cookie - const configCookie = cookies().get("komgaCredentials"); - if (!configCookie) { - return NextResponse.json({ error: "Configuration Komga manquante" }, { status: 401 }); - } - - let config: AuthConfig; - try { - config = JSON.parse(atob(configCookie.value)); - } catch (error) { - return NextResponse.json({ error: "Configuration Komga invalide" }, { status: 401 }); - } - - // Clé de cache unique pour les bibliothèques - const cacheKey = "libraries"; - - // Fonction pour récupérer les bibliothèques - const fetchLibraries = async () => { - const response = await fetch(`${config.serverUrl}/api/v1/libraries`, { - headers: { - Authorization: `Basic ${Buffer.from( - `${config.credentials?.username}:${config.credentials?.password}` - ).toString("base64")}`, - }, - }); - - if (!response.ok) { - throw new Error("Erreur lors de la récupération des bibliothèques"); - } - - return response.json(); - }; - - // Récupérer les données du cache ou faire l'appel API - const data = await serverCacheService.getOrSet(cacheKey, fetchLibraries, 5 * 60); // Cache de 5 minutes - - return NextResponse.json(data); + const libraries = await LibraryService.getLibraries(); + return NextResponse.json(libraries); } catch (error) { - console.error("Erreur lors de la récupération des bibliothèques:", error); + console.error("API Libraries - Erreur:", error); return NextResponse.json({ error: "Erreur serveur" }, { status: 500 }); } } diff --git a/src/app/api/komga/series/[seriesId]/route.ts b/src/app/api/komga/series/[seriesId]/route.ts index b7251c3..f091ab6 100644 --- a/src/app/api/komga/series/[seriesId]/route.ts +++ b/src/app/api/komga/series/[seriesId]/route.ts @@ -1,75 +1,23 @@ import { NextResponse } from "next/server"; -import { cookies } from "next/headers"; +import { SeriesService } from "@/lib/services/series.service"; export async function GET(request: Request, { params }: { params: { seriesId: string } }) { try { - // Récupérer les credentials Komga depuis le cookie - const configCookie = cookies().get("komgaCredentials"); - if (!configCookie) { - return NextResponse.json({ error: "Configuration Komga manquante" }, { status: 401 }); - } - - let config; - try { - config = JSON.parse(atob(configCookie.value)); - } catch (error) { - return NextResponse.json({ error: "Configuration Komga invalide" }, { status: 401 }); - } - - if (!config.credentials?.username || !config.credentials?.password) { - return NextResponse.json({ error: "Credentials Komga manquants" }, { status: 401 }); - } - - const auth = Buffer.from( - `${config.credentials.username}:${config.credentials.password}` - ).toString("base64"); - - // Récupérer les paramètres de pagination depuis l'URL const { searchParams } = new URL(request.url); - const page = searchParams.get("page") || "0"; - const size = searchParams.get("size") || "24"; + const page = parseInt(searchParams.get("page") || "0"); + const size = parseInt(searchParams.get("size") || "24"); + const unreadOnly = searchParams.get("unread") === "true"; - // Appel à l'API Komga pour récupérer les détails de la série - const [seriesResponse, booksResponse] = await Promise.all([ - // Détails de la série - fetch(`${config.serverUrl}/api/v1/series/${params.seriesId}`, { - headers: { - Authorization: `Basic ${auth}`, - }, - }), - // Liste des tomes avec pagination - fetch( - `${config.serverUrl}/api/v1/series/${params.seriesId}/books?page=${page}&size=${size}&sort=metadata.numberSort,asc`, - { - headers: { - Authorization: `Basic ${auth}`, - }, - } - ), + const [series, books] = await Promise.all([ + SeriesService.getSeries(params.seriesId), + SeriesService.getSeriesBooks(params.seriesId, page, size, unreadOnly), ]); - if (!seriesResponse.ok || !booksResponse.ok) { - const errorResponse = !seriesResponse.ok ? seriesResponse : booksResponse; - const errorData = await errorResponse.json().catch(() => null); - return NextResponse.json( - { - error: "Erreur lors de la récupération des données de la série", - details: errorData, - }, - { status: errorResponse.status } - ); - } - - const [series, books] = await Promise.all([seriesResponse.json(), booksResponse.json()]); - return NextResponse.json({ series, books }); } catch (error) { - console.error("Erreur lors de la récupération de la série:", error); + console.error("API Series - Erreur:", error); return NextResponse.json( - { - error: "Erreur serveur", - details: error instanceof Error ? error.message : "Erreur inconnue", - }, + { error: "Erreur lors de la récupération de la série" }, { status: 500 } ); } diff --git a/src/lib/services/server-cache.service.ts b/src/lib/services/server-cache.service.ts index eb5e639..e963e14 100644 --- a/src/lib/services/server-cache.service.ts +++ b/src/lib/services/server-cache.service.ts @@ -6,11 +6,11 @@ type CacheEntry = { class ServerCacheService { private static instance: ServerCacheService; - private cache: Map; + private cache: Map = new Map(); private defaultTTL = 5 * 60; // 5 minutes en secondes private constructor() { - this.cache = new Map(); + // Private constructor to prevent external instantiation } public static getInstance(): ServerCacheService { @@ -26,8 +26,7 @@ class ServerCacheService { set(key: string, data: any, ttl: number = this.defaultTTL): void { this.cache.set(key, { data, - timestamp: Date.now(), - ttl, + expiry: Date.now() + ttl * 1000, }); } @@ -35,16 +34,16 @@ class ServerCacheService { * Récupère des données du cache si elles sont valides */ get(key: string): any | null { - const entry = this.cache.get(key); - if (!entry) return null; + const cached = this.cache.get(key); + if (!cached) return null; const now = Date.now(); - if (now - entry.timestamp > entry.ttl * 1000) { - this.cache.delete(key); - return null; + if (cached.expiry > now) { + return cached.data; } - return entry.data; + this.cache.delete(key); + return null; } /** @@ -64,19 +63,28 @@ class ServerCacheService { /** * Récupère des données du cache ou exécute la fonction si nécessaire */ - async getOrSet( - key: string, - fetcher: () => Promise, - ttl: number = this.defaultTTL - ): Promise { - const cachedData = this.get(key); - if (cachedData) { - return cachedData; + async getOrSet(key: string, fetcher: () => Promise, ttl: number): Promise { + const now = Date.now(); + const cached = this.cache.get(key); + + if (cached && cached.expiry > now) { + return cached.data as T; } - const data = await fetcher(); - this.set(key, data, ttl); - return data; + try { + const data = await fetcher(); + this.cache.set(key, { + data, + expiry: now + ttl * 1000, + }); + return data; + } catch (error) { + throw error; + } + } + + invalidate(key: string): void { + this.cache.delete(key); } } diff --git a/src/types/auth.ts b/src/types/auth.ts index 524cfa7..12df4f7 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -2,7 +2,7 @@ import { KomgaUser } from "./komga"; export interface AuthConfig { serverUrl: string; - credentials?: { + credentials: { username: string; password: string; };