import { BaseApiService } from "./base-api.service"; import type { LibraryResponse } from "@/types/library"; import type { Series } from "@/types/series"; import { getServerCacheService } from "./server-cache.service"; import { ERROR_CODES } from "../../constants/errorCodes"; import { AppError } from "../../utils/errors"; import type { KomgaLibrary } from "@/types/komga"; export class LibraryService extends BaseApiService { static async getLibraries(): Promise { try { return this.fetchWithCache( "libraries", async () => this.fetchFromApi({ path: "libraries" }), "LIBRARIES" ); } catch (error) { throw new AppError(ERROR_CODES.LIBRARY.FETCH_ERROR, {}, error); } } static async getLibrary(libraryId: string): Promise { try { const libraries = await this.getLibraries(); const library = libraries.find((library) => library.id === libraryId); if (!library) { throw new AppError(ERROR_CODES.LIBRARY.NOT_FOUND, { libraryId }); } return library; } catch (error) { if (error instanceof AppError) { throw error; } throw new AppError(ERROR_CODES.LIBRARY.FETCH_ERROR, {}, error); } } static async getAllLibrarySeries(libraryId: string): Promise { try { const headers = { "Content-Type": "application/json" }; const searchBody = { condition: { libraryId: { operator: "is", value: libraryId, }, }, }; const cacheKey = `library-${libraryId}-all-series`; const response = await this.fetchWithCache>( cacheKey, async () => this.fetchFromApi>( { path: "series/list", params: { size: "5000", // On récupère un maximum de livres }, }, headers, { method: "POST", body: JSON.stringify(searchBody), } ), "SERIES" ); return response.content; } catch (error) { throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error); } } static async getLibrarySeries( libraryId: string, page: number = 0, size: number = 20, unreadOnly: boolean = false, search?: string ): Promise> { try { const headers = { "Content-Type": "application/json" }; // Construction du body de recherche pour Komga const condition: Record = { libraryId: { operator: "is", value: libraryId, }, }; const searchBody = { condition }; // Pour le filtre unread, on récupère plus d'éléments car on filtre côté client // Estimation : ~50% des séries sont unread, donc on récupère 2x pour être sûr const fetchSize = unreadOnly ? size * 2 : size; // Clé de cache incluant tous les paramètres const cacheKey = `library-${libraryId}-series-p${page}-s${size}-u${unreadOnly}-q${ search || "" }`; const response = await this.fetchWithCache>( cacheKey, async () => { const params: Record = { page: String(page), size: String(fetchSize), sort: "metadata.titleSort,asc", }; // Filtre de recherche Komga (recherche dans le titre) if (search) { params.search = search; } return this.fetchFromApi>( { path: "series/list", params }, headers, { method: "POST", body: JSON.stringify(searchBody), } ); }, "SERIES" ); // Filtrer les séries supprimées côté client (léger) let filteredContent = response.content.filter((series) => !series.deleted); // Filtre unread côté client (Komga n'a pas de filtre natif pour booksReadCount < booksCount) if (unreadOnly) { filteredContent = filteredContent.filter( (series) => series.booksReadCount < series.booksCount ); // Prendre uniquement les `size` premiers après filtrage filteredContent = filteredContent.slice(0, size); } // Note: Les totaux (totalElements, totalPages) restent ceux de Komga // Ils sont approximatifs après filtrage côté client mais fonctionnels pour la pagination // Le filtrage côté client est léger (seulement deleted + unread) return { ...response, content: filteredContent, numberOfElements: filteredContent.length, // Garder totalElements et totalPages de Komga pour la pagination // Ils seront légèrement inexacts mais fonctionnels }; } catch (error) { throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error); } } static async invalidateLibrarySeriesCache(libraryId: string): Promise { try { const cacheService = await getServerCacheService(); // Invalider toutes les clés de cache pour cette bibliothèque // Format: library-{id}-series-p{page}-s{size}-u{unread}-q{search} await cacheService.deleteAll(`library-${libraryId}-series-`); // Invalider aussi l'ancienne clé pour compatibilité await cacheService.delete(`library-${libraryId}-all-series`); } catch (error) { throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error); } } static async scanLibrary(libraryId: string, deep: boolean = false): Promise { try { await this.fetchFromApi( { path: `libraries/${libraryId}/scan`, params: { deep: String(deep) }, }, {}, { method: "POST", noJson: true } ); } catch (error) { throw new AppError(ERROR_CODES.LIBRARY.SCAN_ERROR, { libraryId }, error); } } }