feat: ajout des services spécialisés (base, book, home, library, series) et des types associés
This commit is contained in:
69
src/lib/services/base-api.service.ts
Normal file
69
src/lib/services/base-api.service.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/lib/services/book.service.ts
Normal file
48
src/lib/services/book.service.ts
Normal 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`;
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/lib/services/home.service.ts
Normal file
83
src/lib/services/home.service.ts
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/lib/services/library.service.ts
Normal file
58
src/lib/services/library.service.ts
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/lib/services/series.service.ts
Normal file
60
src/lib/services/series.service.ts
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/types/library.ts
Normal file
37
src/types/library.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export interface Library {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
root: string;
|
||||||
|
importLastModified: string;
|
||||||
|
lastModified: string;
|
||||||
|
unavailable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LibraryResponse<T> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
50
src/types/series.ts
Normal file
50
src/types/series.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user