From 545f44749ac2dff5c90b240781e51f463f16a2b7 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Wed, 12 Feb 2025 08:47:13 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20ajout=20des=20services=20sp=C3=A9cialis?= =?UTF-8?q?=C3=A9s=20(base,=20book,=20home,=20library,=20series)=20et=20de?= =?UTF-8?q?s=20types=20associ=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/services/base-api.service.ts | 69 +++++++++++++++++++++++ src/lib/services/book.service.ts | 48 ++++++++++++++++ src/lib/services/home.service.ts | 83 ++++++++++++++++++++++++++++ src/lib/services/library.service.ts | 58 +++++++++++++++++++ src/lib/services/series.service.ts | 60 ++++++++++++++++++++ src/types/library.ts | 37 +++++++++++++ src/types/series.ts | 50 +++++++++++++++++ 7 files changed, 405 insertions(+) create mode 100644 src/lib/services/base-api.service.ts create mode 100644 src/lib/services/book.service.ts create mode 100644 src/lib/services/home.service.ts create mode 100644 src/lib/services/library.service.ts create mode 100644 src/lib/services/series.service.ts create mode 100644 src/types/library.ts create mode 100644 src/types/series.ts diff --git a/src/lib/services/base-api.service.ts b/src/lib/services/base-api.service.ts new file mode 100644 index 0000000..9106903 --- /dev/null +++ b/src/lib/services/base-api.service.ts @@ -0,0 +1,69 @@ +import { cookies } from "next/headers"; +import { AuthConfig } from "@/types/auth"; +import { serverCacheService } from "./server-cache.service"; + +export abstract class BaseApiService { + protected static async getKomgaConfig(): Promise { + const configCookie = cookies().get("komgaCredentials"); + if (!configCookie) { + throw new Error("Configuration Komga manquante"); + } + + try { + return JSON.parse(atob(configCookie.value)); + } catch (error) { + throw new Error("Configuration Komga invalide"); + } + } + + protected static getAuthHeaders(config: AuthConfig): Headers { + if (!config.credentials?.username || !config.credentials?.password) { + throw new Error("Credentials Komga manquants"); + } + + const auth = Buffer.from( + `${config.credentials.username}:${config.credentials.password}` + ).toString("base64"); + + return new Headers({ + Authorization: `Basic ${auth}`, + Accept: "application/json", + }); + } + + protected static async fetchWithCache( + key: string, + fetcher: () => Promise, + ttl: number = 5 * 60 // 5 minutes par défaut + ): Promise { + return serverCacheService.getOrSet(key, fetcher, ttl); + } + + protected static handleError(error: unknown, defaultMessage: string): never { + console.error("API Error:", error); + + if (error instanceof Error) { + throw error; + } + + throw new Error(defaultMessage); + } + + protected static buildUrl( + config: AuthConfig, + path: string, + params?: Record + ): string { + const url = new URL(`${config.serverUrl}/api/v1/${path}`); + + if (params) { + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined) { + url.searchParams.append(key, value); + } + }); + } + + return url.toString(); + } +} diff --git a/src/lib/services/book.service.ts b/src/lib/services/book.service.ts new file mode 100644 index 0000000..2f32675 --- /dev/null +++ b/src/lib/services/book.service.ts @@ -0,0 +1,48 @@ +import { BaseApiService } from "./base-api.service"; +import { KomgaBook } from "@/types/komga"; + +export class BookService extends BaseApiService { + static async getBook(bookId: string): Promise<{ book: KomgaBook; pages: number[] }> { + try { + const config = await this.getKomgaConfig(); + const headers = this.getAuthHeaders(config); + + return this.fetchWithCache<{ book: KomgaBook; pages: number[] }>( + `book-${bookId}`, + async () => { + // Récupération des détails du tome + const bookResponse = await fetch(this.buildUrl(config, `books/${bookId}`), { headers }); + if (!bookResponse.ok) { + 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(this.buildUrl(config, `books/${bookId}/pages`), { + headers, + }); + if (!pagesResponse.ok) { + throw new Error("Erreur lors de la récupération des pages du tome"); + } + const pages = await pagesResponse.json(); + + return { + book, + pages: pages.map((page: any) => page.number), + }; + }, + 5 * 60 // Cache de 5 minutes + ); + } catch (error) { + return this.handleError(error, "Impossible de récupérer le tome"); + } + } + + static getPageUrl(bookId: string, pageNumber: number): string { + return `/api/komga/books/${bookId}/pages/${pageNumber}`; + } + + static getThumbnailUrl(bookId: string): string { + return `/api/komga/images/books/${bookId}/thumbnail`; + } +} diff --git a/src/lib/services/home.service.ts b/src/lib/services/home.service.ts new file mode 100644 index 0000000..24952a1 --- /dev/null +++ b/src/lib/services/home.service.ts @@ -0,0 +1,83 @@ +import { BaseApiService } from "./base-api.service"; +import { KomgaBook, KomgaSeries } from "@/types/komga"; +import { LibraryResponse } from "@/types/library"; + +interface HomeData { + ongoing: KomgaSeries[]; + recentlyRead: KomgaBook[]; + popular: KomgaSeries[]; +} + +export class HomeService extends BaseApiService { + static async getHomeData(): Promise { + try { + const config = await this.getKomgaConfig(); + const headers = this.getAuthHeaders(config); + + return this.fetchWithCache( + "home", + async () => { + // Appels API parallèles + const [ongoingResponse, recentlyReadResponse, popularResponse] = await Promise.all([ + // Séries en cours + fetch( + this.buildUrl(config, "series", { + read_status: "IN_PROGRESS", + sort: "readDate,desc", + page: "0", + size: "20", + media_status: "READY", + }), + { headers } + ), + // Derniers livres lus + fetch( + this.buildUrl(config, "books", { + read_status: "READ", + sort: "readDate,desc", + page: "0", + size: "20", + }), + { headers } + ), + // Séries populaires + fetch( + this.buildUrl(config, "series", { + page: "0", + size: "20", + sort: "metadata.titleSort,asc", + media_status: "READY", + }), + { headers } + ), + ]); + + // Vérifier les réponses + if (!ongoingResponse.ok || !recentlyReadResponse.ok || !popularResponse.ok) { + throw new Error("Erreur lors de la récupération des données"); + } + + // Récupérer les données + const [ongoing, recentlyRead, popular] = (await Promise.all([ + ongoingResponse.json(), + recentlyReadResponse.json(), + popularResponse.json(), + ])) as [ + LibraryResponse, + LibraryResponse, + LibraryResponse + ]; + + return { + ongoing: ongoing.content || [], + recentlyRead: recentlyRead.content || [], + popular: popular.content || [], + }; + }, + 5 * 60 // Cache de 5 minutes + ); + } catch (error) { + return this.handleError(error, "Impossible de récupérer les données de la page d'accueil"); + } + } +} diff --git a/src/lib/services/library.service.ts b/src/lib/services/library.service.ts new file mode 100644 index 0000000..616a8db --- /dev/null +++ b/src/lib/services/library.service.ts @@ -0,0 +1,58 @@ +import { BaseApiService } from "./base-api.service"; +import { Library, LibraryResponse } from "@/types/library"; +import { Series } from "@/types/series"; + +export class LibraryService extends BaseApiService { + static async getLibraries(): Promise { + try { + const config = await this.getKomgaConfig(); + const url = this.buildUrl(config, "libraries"); + const headers = this.getAuthHeaders(config); + + return this.fetchWithCache( + "libraries", + async () => { + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error("Erreur lors de la récupération des bibliothèques"); + } + return response.json(); + }, + 5 * 60 // Cache de 5 minutes + ); + } catch (error) { + return this.handleError(error, "Impossible de récupérer les bibliothèques"); + } + } + + static async getLibrarySeries( + libraryId: string, + page: number = 0, + size: number = 20, + unreadOnly: boolean = false + ): Promise> { + try { + const config = await this.getKomgaConfig(); + const url = this.buildUrl(config, `libraries/${libraryId}/series`, { + page: page.toString(), + size: size.toString(), + ...(unreadOnly && { read_status: "UNREAD" }), + }); + const headers = this.getAuthHeaders(config); + + return this.fetchWithCache>( + `library-${libraryId}-series-${page}-${size}-${unreadOnly}`, + async () => { + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error("Erreur lors de la récupération des séries"); + } + return response.json(); + }, + 5 * 60 // Cache de 5 minutes + ); + } catch (error) { + return this.handleError(error, "Impossible de récupérer les séries"); + } + } +} diff --git a/src/lib/services/series.service.ts b/src/lib/services/series.service.ts new file mode 100644 index 0000000..3422060 --- /dev/null +++ b/src/lib/services/series.service.ts @@ -0,0 +1,60 @@ +import { BaseApiService } from "./base-api.service"; +import { Series } from "@/types/series"; +import { LibraryResponse } from "@/types/library"; +import { KomgaBook } from "@/types/komga"; + +export class SeriesService extends BaseApiService { + static async getSeries(seriesId: string): Promise { + try { + const config = await this.getKomgaConfig(); + const url = this.buildUrl(config, `series/${seriesId}`); + const headers = this.getAuthHeaders(config); + + return this.fetchWithCache( + `series-${seriesId}`, + async () => { + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error("Erreur lors de la récupération de la série"); + } + return response.json(); + }, + 5 * 60 // Cache de 5 minutes + ); + } catch (error) { + return this.handleError(error, "Impossible de récupérer la série"); + } + } + + static async getSeriesBooks( + seriesId: string, + page: number = 0, + size: number = 24, + unreadOnly: boolean = false + ): Promise> { + try { + const config = await this.getKomgaConfig(); + const url = this.buildUrl(config, `series/${seriesId}/books`, { + page: page.toString(), + size: size.toString(), + sort: "metadata.numberSort,asc", + ...(unreadOnly && { read_status: "UNREAD,IN_PROGRESS" }), + }); + const headers = this.getAuthHeaders(config); + + return this.fetchWithCache>( + `series-${seriesId}-books-${page}-${size}-${unreadOnly}`, + async () => { + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error("Erreur lors de la récupération des tomes"); + } + return response.json(); + }, + 5 * 60 // Cache de 5 minutes + ); + } catch (error) { + return this.handleError(error, "Impossible de récupérer les tomes"); + } + } +} diff --git a/src/types/library.ts b/src/types/library.ts new file mode 100644 index 0000000..e9cdcf6 --- /dev/null +++ b/src/types/library.ts @@ -0,0 +1,37 @@ +export interface Library { + id: string; + name: string; + root: string; + importLastModified: string; + lastModified: string; + unavailable: boolean; +} + +export interface LibraryResponse { + content: T[]; + empty: boolean; + first: boolean; + last: boolean; + number: number; + numberOfElements: number; + pageable: { + offset: number; + pageNumber: number; + pageSize: number; + paged: boolean; + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + unpaged: boolean; + }; + size: number; + sort: { + empty: boolean; + sorted: boolean; + unsorted: boolean; + }; + totalElements: number; + totalPages: number; +} diff --git a/src/types/series.ts b/src/types/series.ts new file mode 100644 index 0000000..9ed4ac8 --- /dev/null +++ b/src/types/series.ts @@ -0,0 +1,50 @@ +export interface Series { + id: string; + libraryId: string; + name: string; + url: string; + created: string; + lastModified: string; + fileLastModified: string; + booksCount: number; + booksReadCount: number; + booksUnreadCount: number; + booksInProgressCount: number; + metadata: { + status: string; + created: string; + lastModified: string; + title: string; + titleSort: string; + summary: string; + readingDirection: string; + publisher: string; + ageRating: number; + language: string; + genres: string[]; + tags: string[]; + totalBookCount: number; + sharingLabels: string[]; + links: Array<{ + label: string; + url: string; + }>; + alternateTitles: string[]; + }; + booksMetadata: { + status: string; + created: string; + lastModified: string; + title: string; + titleSort: string; + summary: string; + number: string; + numberSort: number; + releaseDate: string; + authors: Array<{ + name: string; + role: string; + }>; + }; + deleted: boolean; +}