import { BaseApiService } from "./base-api.service"; import type { LibraryResponse } from "@/types/library"; import type { KomgaBook, KomgaSeries } from "@/types/komga"; import { BookService } from "./book.service"; import { ImageService } from "./image.service"; import { PreferencesService } from "./preferences.service"; import { ERROR_CODES } from "../../constants/errorCodes"; import { AppError } from "../../utils/errors"; import type { UserPreferences } from "@/types/preferences"; import logger from "@/lib/logger"; type KomgaCondition = Record; export class SeriesService extends BaseApiService { private static readonly CACHE_TTL = 120; // 2 minutes static async getSeries(seriesId: string): Promise { try { return this.fetchFromApi( { path: `series/${seriesId}` }, {}, { revalidate: this.CACHE_TTL } ); } catch (error) { throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error); } } static async getSeriesBooks( seriesId: string, page: number = 0, size: number = 24, unreadOnly: boolean = false ): Promise> { try { const headers = { "Content-Type": "application/json" }; // Construction du body de recherche pour Komga let condition: KomgaCondition; if (unreadOnly) { // Utiliser allOf pour combiner seriesId avec anyOf pour UNREAD ou IN_PROGRESS condition = { allOf: [ { seriesId: { operator: "is", value: seriesId, }, }, { anyOf: [ { readStatus: { operator: "is", value: "UNREAD", }, }, { readStatus: { operator: "is", value: "IN_PROGRESS", }, }, ], }, ], }; } else { condition = { seriesId: { operator: "is", value: seriesId, }, }; } const searchBody = { condition }; const params: Record = { page: String(page), size: String(size), sort: "number,asc", }; const response = await this.fetchFromApi>( { path: "books/list", params }, headers, { method: "POST", body: JSON.stringify(searchBody), revalidate: this.CACHE_TTL, } ); // Filtrer uniquement les livres supprimés côté client (léger) const filteredContent = response.content.filter((book: KomgaBook) => !book.deleted); return { ...response, content: filteredContent, numberOfElements: filteredContent.length, }; } catch (error) { throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error); } } static async getFirstBook(seriesId: string): Promise { try { const data: LibraryResponse = await this.fetchFromApi>({ path: `series/${seriesId}/books`, params: { page: "0", size: "1" }, }); if (!data.content || data.content.length === 0) { throw new AppError(ERROR_CODES.SERIES.NO_BOOKS_FOUND); } return data.content[0].id; } catch (error) { logger.error({ err: error }, "Erreur lors de la récupération du premier livre"); throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error); } } static async getCover(seriesId: string): Promise { try { // Récupérer les préférences de l'utilisateur const preferences: UserPreferences = await PreferencesService.getPreferences(); // Si l'utilisateur préfère les vignettes, utiliser la miniature (streaming) if (preferences.showThumbnails) { return ImageService.streamImage(`series/${seriesId}/thumbnail`); } // Sinon, récupérer la première page (streaming) const firstBookId = await this.getFirstBook(seriesId); return BookService.getPage(firstBookId, 1); } catch (error) { throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error); } } static getCoverUrl(seriesId: string): string { return `/api/komga/images/series/${seriesId}/thumbnail`; } static async getMultipleSeries(seriesIds: string[]): Promise { try { const seriesPromises: Promise[] = seriesIds.map((id: string) => this.getSeries(id) ); const series: KomgaSeries[] = await Promise.all(seriesPromises); return series.filter(Boolean); } catch (error) { throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error); } } }