diff --git a/src/app/api/komga/libraries/[libraryId]/scan/route.ts b/src/app/api/komga/libraries/[libraryId]/scan/route.ts new file mode 100644 index 0000000..83112a9 --- /dev/null +++ b/src/app/api/komga/libraries/[libraryId]/scan/route.ts @@ -0,0 +1,45 @@ +import { NextResponse } from "next/server"; +import { LibraryService } from "@/lib/services/library.service"; +import { ERROR_CODES } from "@/constants/errorCodes"; +import { AppError } from "@/utils/errors"; +import { getErrorMessage } from "@/utils/errors"; +import type { NextRequest } from "next/server"; + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ libraryId: string }> } +) { + try { + const libraryId: string = (await params).libraryId; + + // Scan library with deep=false + await LibraryService.scanLibrary(libraryId, false); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("API Library Scan - Erreur:", error); + if (error instanceof AppError) { + return NextResponse.json( + { + error: { + code: error.code, + name: "Library scan error", + message: getErrorMessage(error.code), + }, + }, + { status: 500 } + ); + } + return NextResponse.json( + { + error: { + code: ERROR_CODES.LIBRARY.SCAN_ERROR, + name: "Library scan error", + message: getErrorMessage(ERROR_CODES.LIBRARY.SCAN_ERROR), + }, + }, + { status: 500 } + ); + } +} + diff --git a/src/components/library/LibraryHeader.tsx b/src/components/library/LibraryHeader.tsx index 1e19597..5b1fcca 100644 --- a/src/components/library/LibraryHeader.tsx +++ b/src/components/library/LibraryHeader.tsx @@ -1,9 +1,10 @@ "use client"; import { useMemo } from "react"; -import { Library, BookOpen } from "lucide-react"; +import { Library } from "lucide-react"; import type { KomgaLibrary, KomgaSeries } from "@/types/komga"; import { RefreshButton } from "./RefreshButton"; +import { ScanButton } from "./ScanButton"; import { useTranslate } from "@/hooks/useTranslate"; import { StatusBadge } from "@/components/ui/status-badge"; import { SeriesCover } from "@/components/ui/series-cover"; @@ -84,14 +85,8 @@ export const LibraryHeader = ({ library, seriesCount, series, refreshLibrary }: } - - {library.booksCount === 1 - ? t("library.header.books", { count: library.booksCount }) - : t("library.header.books_plural", { count: library.booksCount }) - } - - + {library.unavailable && ( diff --git a/src/components/library/ScanButton.tsx b/src/components/library/ScanButton.tsx new file mode 100644 index 0000000..3bd6200 --- /dev/null +++ b/src/components/library/ScanButton.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useState } from "react"; +import { FolderSearch } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/components/ui/use-toast"; +import { cn } from "@/lib/utils"; +import { useTranslation } from "react-i18next"; +import { useRouter } from "next/navigation"; + +interface ScanButtonProps { + libraryId: string; +} + +export function ScanButton({ libraryId }: ScanButtonProps) { + const [isScanning, setIsScanning] = useState(false); + const { toast } = useToast(); + const { t } = useTranslation(); + const router = useRouter(); + + const handleScan = async () => { + setIsScanning(true); + try { + const response = await fetch(`/api/komga/libraries/${libraryId}/scan`, { + method: "POST", + }); + + if (!response.ok) { + throw new Error("Failed to scan library"); + } + + toast({ + title: t("library.scan.success.title"), + description: t("library.scan.success.description"), + }); + + // Attendre 5 secondes pour que le scan se termine, puis invalider le cache et rafraîchir + setTimeout(async () => { + try { + // Invalider le cache + await fetch(`/api/komga/libraries/${libraryId}/series`, { + method: "DELETE", + }); + + // Rafraîchir la page pour voir les changements + router.refresh(); + + // Toast pour indiquer que l'analyse est terminée + toast({ + title: t("library.scan.complete.title"), + description: t("library.scan.complete.description"), + }); + } catch (error) { + console.error("Error invalidating cache after scan:", error); + toast({ + variant: "destructive", + title: t("library.scan.error.title"), + description: t("library.scan.error.refresh"), + }); + } finally { + setIsScanning(false); + } + }, 5000); + } catch (error) { + setIsScanning(false); + toast({ + variant: "destructive", + title: t("library.scan.error.title"), + description: + error instanceof Error ? error.message : t("library.scan.error.description"), + }); + } + }; + + return ( + + ); +} + diff --git a/src/constants/errorCodes.ts b/src/constants/errorCodes.ts index 0c8d5ce..fda6694 100644 --- a/src/constants/errorCodes.ts +++ b/src/constants/errorCodes.ts @@ -36,6 +36,7 @@ export const ERROR_CODES = { LIBRARY: { NOT_FOUND: "LIBRARY_NOT_FOUND", FETCH_ERROR: "LIBRARY_FETCH_ERROR", + SCAN_ERROR: "LIBRARY_SCAN_ERROR", }, SERIES: { FETCH_ERROR: "SERIES_FETCH_ERROR", diff --git a/src/constants/errorMessages.ts b/src/constants/errorMessages.ts index 3e0b03a..2a9c99f 100644 --- a/src/constants/errorMessages.ts +++ b/src/constants/errorMessages.ts @@ -32,6 +32,7 @@ export const ERROR_MESSAGES: Record = { // Library [ERROR_CODES.LIBRARY.NOT_FOUND]: "📚 Library {libraryId} not found", [ERROR_CODES.LIBRARY.FETCH_ERROR]: "📚 Error fetching libraries", + [ERROR_CODES.LIBRARY.SCAN_ERROR]: "🔍 Error scanning library", // Series [ERROR_CODES.SERIES.FETCH_ERROR]: "📖 Error fetching series", diff --git a/src/i18n/messages/en/common.json b/src/i18n/messages/en/common.json index 15ee1b4..386f970 100644 --- a/src/i18n/messages/en/common.json +++ b/src/i18n/messages/en/common.json @@ -198,6 +198,23 @@ "description": "An error occurred" } }, + "scan": { + "button": "Scan library", + "buttonLabel": "Scan", + "success": { + "title": "Library scan started", + "description": "The library is being scanned" + }, + "complete": { + "title": "Scan complete", + "description": "The library has been scanned and updated" + }, + "error": { + "title": "Error", + "description": "An error occurred while scanning the library", + "refresh": "An error occurred while updating the library data" + } + }, "header": { "series": "{{count}} series", "series_plural": "{{count}} series", @@ -363,6 +380,7 @@ "LIBRARY_NOT_FOUND": "Library not found", "LIBRARY_FETCH_ERROR": "Error fetching library", + "LIBRARY_SCAN_ERROR": "Error scanning library", "SERIES_FETCH_ERROR": "Error fetching series", "SERIES_NO_BOOKS_FOUND": "No books found in series", diff --git a/src/i18n/messages/fr/common.json b/src/i18n/messages/fr/common.json index b983574..54041c9 100644 --- a/src/i18n/messages/fr/common.json +++ b/src/i18n/messages/fr/common.json @@ -198,6 +198,23 @@ "description": "Une erreur est survenue" } }, + "scan": { + "button": "Analyser la bibliothèque", + "buttonLabel": "Analyser", + "success": { + "title": "Analyse lancée", + "description": "La bibliothèque est en cours d'analyse" + }, + "complete": { + "title": "Analyse terminée", + "description": "La bibliothèque a été analysée et mise à jour" + }, + "error": { + "title": "Erreur", + "description": "Une erreur est survenue lors de l'analyse de la bibliothèque", + "refresh": "Une erreur est survenue lors de la mise à jour des données" + } + }, "header": { "series": "{{count}} série", "series_plural": "{{count}} séries", @@ -361,6 +378,7 @@ "LIBRARY_NOT_FOUND": "Bibliothèque introuvable", "LIBRARY_FETCH_ERROR": "Erreur lors de la récupération de la bibliothèque", + "LIBRARY_SCAN_ERROR": "Erreur lors de l'analyse de la bibliothèque", "SERIES_FETCH_ERROR": "Erreur lors de la récupération des séries", "SERIES_NO_BOOKS_FOUND": "Aucun livre trouvé dans la série", diff --git a/src/lib/services/base-api.service.ts b/src/lib/services/base-api.service.ts index 6a6c8e1..ccc7187 100644 --- a/src/lib/services/base-api.service.ts +++ b/src/lib/services/base-api.service.ts @@ -13,6 +13,7 @@ export type { CacheType }; interface KomgaRequestInit extends RequestInit { isImage?: boolean; + noJson?: boolean; } interface KomgaUrlBuilder { @@ -175,7 +176,15 @@ export abstract class BaseApiService { }); } - return options.isImage ? (response as T) : response.json(); + if (options.isImage) { + return response as T; + } + + if (options.noJson) { + return undefined as T; + } + + return response.json(); } catch (error) { throw error; } finally { diff --git a/src/lib/services/library.service.ts b/src/lib/services/library.service.ts index 55cfd7f..79844f8 100644 --- a/src/lib/services/library.service.ts +++ b/src/lib/services/library.service.ts @@ -159,4 +159,15 @@ export class LibraryService extends BaseApiService { 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); + } + } }