feat: add scan library functionality and related error handling in LibraryHeader and services
This commit is contained in:
45
src/app/api/komga/libraries/[libraryId]/scan/route.ts
Normal file
45
src/app/api/komga/libraries/[libraryId]/scan/route.ts
Normal file
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { Library, BookOpen } from "lucide-react";
|
import { Library } from "lucide-react";
|
||||||
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
import type { KomgaLibrary, KomgaSeries } from "@/types/komga";
|
||||||
import { RefreshButton } from "./RefreshButton";
|
import { RefreshButton } from "./RefreshButton";
|
||||||
|
import { ScanButton } from "./ScanButton";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
import { StatusBadge } from "@/components/ui/status-badge";
|
import { StatusBadge } from "@/components/ui/status-badge";
|
||||||
import { SeriesCover } from "@/components/ui/series-cover";
|
import { SeriesCover } from "@/components/ui/series-cover";
|
||||||
@@ -84,14 +85,8 @@ export const LibraryHeader = ({ library, seriesCount, series, refreshLibrary }:
|
|||||||
}
|
}
|
||||||
</StatusBadge>
|
</StatusBadge>
|
||||||
|
|
||||||
<StatusBadge status="reading" icon={BookOpen}>
|
|
||||||
{library.booksCount === 1
|
|
||||||
? t("library.header.books", { count: library.booksCount })
|
|
||||||
: t("library.header.books_plural", { count: library.booksCount })
|
|
||||||
}
|
|
||||||
</StatusBadge>
|
|
||||||
|
|
||||||
<RefreshButton libraryId={library.id} refreshLibrary={refreshLibrary} />
|
<RefreshButton libraryId={library.id} refreshLibrary={refreshLibrary} />
|
||||||
|
<ScanButton libraryId={library.id} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{library.unavailable && (
|
{library.unavailable && (
|
||||||
|
|||||||
88
src/components/library/ScanButton.tsx
Normal file
88
src/components/library/ScanButton.tsx
Normal file
@@ -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 (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleScan}
|
||||||
|
disabled={isScanning}
|
||||||
|
className="ml-2"
|
||||||
|
aria-label={t("library.scan.button")}
|
||||||
|
>
|
||||||
|
<FolderSearch className={cn("h-4 w-4", isScanning && "animate-pulse")} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ export const ERROR_CODES = {
|
|||||||
LIBRARY: {
|
LIBRARY: {
|
||||||
NOT_FOUND: "LIBRARY_NOT_FOUND",
|
NOT_FOUND: "LIBRARY_NOT_FOUND",
|
||||||
FETCH_ERROR: "LIBRARY_FETCH_ERROR",
|
FETCH_ERROR: "LIBRARY_FETCH_ERROR",
|
||||||
|
SCAN_ERROR: "LIBRARY_SCAN_ERROR",
|
||||||
},
|
},
|
||||||
SERIES: {
|
SERIES: {
|
||||||
FETCH_ERROR: "SERIES_FETCH_ERROR",
|
FETCH_ERROR: "SERIES_FETCH_ERROR",
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const ERROR_MESSAGES: Record<string, string> = {
|
|||||||
// Library
|
// Library
|
||||||
[ERROR_CODES.LIBRARY.NOT_FOUND]: "📚 Library {libraryId} not found",
|
[ERROR_CODES.LIBRARY.NOT_FOUND]: "📚 Library {libraryId} not found",
|
||||||
[ERROR_CODES.LIBRARY.FETCH_ERROR]: "📚 Error fetching libraries",
|
[ERROR_CODES.LIBRARY.FETCH_ERROR]: "📚 Error fetching libraries",
|
||||||
|
[ERROR_CODES.LIBRARY.SCAN_ERROR]: "🔍 Error scanning library",
|
||||||
|
|
||||||
// Series
|
// Series
|
||||||
[ERROR_CODES.SERIES.FETCH_ERROR]: "📖 Error fetching series",
|
[ERROR_CODES.SERIES.FETCH_ERROR]: "📖 Error fetching series",
|
||||||
|
|||||||
@@ -198,6 +198,23 @@
|
|||||||
"description": "An error occurred"
|
"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": {
|
"header": {
|
||||||
"series": "{{count}} series",
|
"series": "{{count}} series",
|
||||||
"series_plural": "{{count}} series",
|
"series_plural": "{{count}} series",
|
||||||
@@ -363,6 +380,7 @@
|
|||||||
|
|
||||||
"LIBRARY_NOT_FOUND": "Library not found",
|
"LIBRARY_NOT_FOUND": "Library not found",
|
||||||
"LIBRARY_FETCH_ERROR": "Error fetching library",
|
"LIBRARY_FETCH_ERROR": "Error fetching library",
|
||||||
|
"LIBRARY_SCAN_ERROR": "Error scanning library",
|
||||||
|
|
||||||
"SERIES_FETCH_ERROR": "Error fetching series",
|
"SERIES_FETCH_ERROR": "Error fetching series",
|
||||||
"SERIES_NO_BOOKS_FOUND": "No books found in series",
|
"SERIES_NO_BOOKS_FOUND": "No books found in series",
|
||||||
|
|||||||
@@ -198,6 +198,23 @@
|
|||||||
"description": "Une erreur est survenue"
|
"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": {
|
"header": {
|
||||||
"series": "{{count}} série",
|
"series": "{{count}} série",
|
||||||
"series_plural": "{{count}} séries",
|
"series_plural": "{{count}} séries",
|
||||||
@@ -361,6 +378,7 @@
|
|||||||
|
|
||||||
"LIBRARY_NOT_FOUND": "Bibliothèque introuvable",
|
"LIBRARY_NOT_FOUND": "Bibliothèque introuvable",
|
||||||
"LIBRARY_FETCH_ERROR": "Erreur lors de la récupération de la bibliothèque",
|
"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_FETCH_ERROR": "Erreur lors de la récupération des séries",
|
||||||
"SERIES_NO_BOOKS_FOUND": "Aucun livre trouvé dans la série",
|
"SERIES_NO_BOOKS_FOUND": "Aucun livre trouvé dans la série",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type { CacheType };
|
|||||||
|
|
||||||
interface KomgaRequestInit extends RequestInit {
|
interface KomgaRequestInit extends RequestInit {
|
||||||
isImage?: boolean;
|
isImage?: boolean;
|
||||||
|
noJson?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KomgaUrlBuilder {
|
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) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -159,4 +159,15 @@ export class LibraryService extends BaseApiService {
|
|||||||
throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error);
|
throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async scanLibrary(libraryId: string, deep: boolean = false): Promise<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user