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