feat: ajout des services spécialisés (base, book, home, library, series) et des types associés

This commit is contained in:
Julien Froidefond
2025-02-12 08:47:13 +01:00
parent 2fa96072e8
commit 545f44749a
7 changed files with 405 additions and 0 deletions

View File

@@ -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<AuthConfig> {
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<T>(
key: string,
fetcher: () => Promise<T>,
ttl: number = 5 * 60 // 5 minutes par défaut
): Promise<T> {
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, string>
): 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();
}
}

View File

@@ -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`;
}
}

View File

@@ -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<HomeData> {
try {
const config = await this.getKomgaConfig();
const headers = this.getAuthHeaders(config);
return this.fetchWithCache<HomeData>(
"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<KomgaSeries>,
LibraryResponse<KomgaBook>,
LibraryResponse<KomgaSeries>
];
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");
}
}
}

View File

@@ -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<Library[]> {
try {
const config = await this.getKomgaConfig();
const url = this.buildUrl(config, "libraries");
const headers = this.getAuthHeaders(config);
return this.fetchWithCache<Library[]>(
"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<LibraryResponse<Series>> {
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<LibraryResponse<Series>>(
`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");
}
}
}

View File

@@ -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<Series> {
try {
const config = await this.getKomgaConfig();
const url = this.buildUrl(config, `series/${seriesId}`);
const headers = this.getAuthHeaders(config);
return this.fetchWithCache<Series>(
`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<LibraryResponse<KomgaBook>> {
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<LibraryResponse<KomgaBook>>(
`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");
}
}
}