Compare commits
10 Commits
c4ae6a1b2f
...
87ac116b9b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87ac116b9b | ||
|
|
90b213a407 | ||
|
|
181240cd5f | ||
|
|
6b6fed34fb | ||
|
|
feb8444b35 | ||
|
|
10b903a136 | ||
|
|
e242b919ac | ||
|
|
1fa4024f91 | ||
|
|
daeb90262a | ||
|
|
c76d960dca |
@@ -83,20 +83,21 @@ size: "1000"; // Récupère TOUS les livres d'un coup
|
|||||||
- Garder uniquement le cache SW pour : images, static, navigation
|
- Garder uniquement le cache SW pour : images, static, navigation
|
||||||
- Le cache serveur suffit pour les données
|
- Le cache serveur suffit pour les données
|
||||||
|
|
||||||
- [ ] **2.2 Supprimer les headers HTTP Cache-Control**
|
- [x] **2.2 Supprimer les headers HTTP Cache-Control**
|
||||||
|
|
||||||
- Retirer `Cache-Control` des NextResponse dans les routes API
|
- Retirer `Cache-Control` des NextResponse dans les routes API
|
||||||
- Évite les conflits avec le cache serveur
|
- Évite les conflits avec le cache serveur
|
||||||
|
- Note: Conservé pour les images de pages de livres (max-age=31536000)
|
||||||
|
|
||||||
- [ ] **2.3 Supprimer `revalidate` des routes dynamiques**
|
- [x] **2.3 Supprimer `revalidate` des routes dynamiques**
|
||||||
|
|
||||||
- Routes API = dynamiques, pas besoin d'ISR
|
- Routes API = dynamiques, pas besoin d'ISR
|
||||||
- Le cache serveur suffit
|
- Le cache serveur suffit
|
||||||
|
|
||||||
- [ ] **2.4 Optimiser les TTL ServerCacheService**
|
- [x] **2.4 Optimiser les TTL ServerCacheService**
|
||||||
- Réduire TTL des listes paginées (1-2 min)
|
- Réduire TTL des listes paginées (2 min) ✅
|
||||||
- Garder TTL court pour les données avec progression (5 min)
|
- Garder TTL court pour les données avec progression (2 min) ✅
|
||||||
- Garder TTL long pour les images (7 jours)
|
- Garder TTL long pour les images (7 jours) ✅
|
||||||
|
|
||||||
**Résultat final :**
|
**Résultat final :**
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { BookService } from "@/lib/services/book.service";
|
import { BookService } from "@/lib/services/book.service";
|
||||||
|
import { SeriesService } from "@/lib/services/series.service";
|
||||||
import { ERROR_CODES } from "@/constants/errorCodes";
|
import { ERROR_CODES } from "@/constants/errorCodes";
|
||||||
import { getErrorMessage } from "@/utils/errors";
|
import { getErrorMessage } from "@/utils/errors";
|
||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
@@ -28,6 +29,17 @@ export async function PATCH(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await BookService.updateReadProgress(bookId, page, completed);
|
await BookService.updateReadProgress(bookId, page, completed);
|
||||||
|
|
||||||
|
// Invalider le cache de la série après avoir mis à jour la progression
|
||||||
|
try {
|
||||||
|
const seriesId = await BookService.getBookSeriesId(bookId);
|
||||||
|
await SeriesService.invalidateSeriesBooksCache(seriesId);
|
||||||
|
await SeriesService.invalidateSeriesCache(seriesId);
|
||||||
|
} catch (cacheError) {
|
||||||
|
// Ne pas faire échouer la requête si l'invalidation du cache échoue
|
||||||
|
logger.error({ err: cacheError }, "Erreur lors de l'invalidation du cache de la série:");
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ message: "📖 Progression mise à jour avec succès" });
|
return NextResponse.json({ message: "📖 Progression mise à jour avec succès" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, "Erreur lors de la mise à jour de la progression:");
|
logger.error({ err: error }, "Erreur lors de la mise à jour de la progression:");
|
||||||
@@ -64,6 +76,17 @@ export async function DELETE(
|
|||||||
const bookId: string = (await params).bookId;
|
const bookId: string = (await params).bookId;
|
||||||
|
|
||||||
await BookService.deleteReadProgress(bookId);
|
await BookService.deleteReadProgress(bookId);
|
||||||
|
|
||||||
|
// Invalider le cache de la série après avoir supprimé la progression
|
||||||
|
try {
|
||||||
|
const seriesId = await BookService.getBookSeriesId(bookId);
|
||||||
|
await SeriesService.invalidateSeriesBooksCache(seriesId);
|
||||||
|
await SeriesService.invalidateSeriesCache(seriesId);
|
||||||
|
} catch (cacheError) {
|
||||||
|
// Ne pas faire échouer la requête si l'invalidation du cache échoue
|
||||||
|
logger.error({ err: cacheError }, "Erreur lors de l'invalidation du cache de la série:");
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({ message: "🗑️ Progression supprimée avec succès" });
|
return NextResponse.json({ message: "🗑️ Progression supprimée avec succès" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, "Erreur lors de la suppression de la progression:");
|
logger.error({ err: error }, "Erreur lors de la suppression de la progression:");
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { ERROR_CODES } from "@/constants/errorCodes";
|
|||||||
import { AppError } from "@/utils/errors";
|
import { AppError } from "@/utils/errors";
|
||||||
import { getErrorMessage } from "@/utils/errors";
|
import { getErrorMessage } from "@/utils/errors";
|
||||||
import logger from "@/lib/logger";
|
import logger from "@/lib/logger";
|
||||||
export const revalidate = 60;
|
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { AppError } from "@/utils/errors";
|
|||||||
import { getErrorMessage } from "@/utils/errors";
|
import { getErrorMessage } from "@/utils/errors";
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import logger from "@/lib/logger";
|
import logger from "@/lib/logger";
|
||||||
export const revalidate = 60;
|
|
||||||
|
|
||||||
const DEFAULT_PAGE_SIZE = 20;
|
const DEFAULT_PAGE_SIZE = 20;
|
||||||
|
|
||||||
@@ -27,14 +26,7 @@ export async function GET(
|
|||||||
LibraryService.getLibrary(libraryId),
|
LibraryService.getLibrary(libraryId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json({ series, library });
|
||||||
{ series, library },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=120",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, "API Library Series - Erreur:");
|
logger.error({ err: error }, "API Library Series - Erreur:");
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { AppError } from "@/utils/errors";
|
|||||||
import { getErrorMessage } from "@/utils/errors";
|
import { getErrorMessage } from "@/utils/errors";
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import logger from "@/lib/logger";
|
import logger from "@/lib/logger";
|
||||||
export const revalidate = 60;
|
|
||||||
|
|
||||||
const DEFAULT_PAGE_SIZE = 20;
|
const DEFAULT_PAGE_SIZE = 20;
|
||||||
|
|
||||||
@@ -26,14 +25,7 @@ export async function GET(
|
|||||||
SeriesService.getSeries(seriesId),
|
SeriesService.getSeries(seriesId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json({ books, series });
|
||||||
{ books, series },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=120",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, "API Series Books - Erreur:");
|
logger.error({ err: error }, "API Series Books - Erreur:");
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import type { KomgaSeries } from "@/types/komga";
|
|||||||
import { getErrorMessage } from "@/utils/errors";
|
import { getErrorMessage } from "@/utils/errors";
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import logger from "@/lib/logger";
|
import logger from "@/lib/logger";
|
||||||
export const revalidate = 60;
|
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
@@ -16,11 +15,7 @@ export async function GET(
|
|||||||
const seriesId: string = (await params).seriesId;
|
const seriesId: string = (await params).seriesId;
|
||||||
|
|
||||||
const series: KomgaSeries = await SeriesService.getSeries(seriesId);
|
const series: KomgaSeries = await SeriesService.getSeries(seriesId);
|
||||||
return NextResponse.json(series, {
|
return NextResponse.json(series);
|
||||||
headers: {
|
|
||||||
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=120",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error({ err: error }, "API Series - Erreur:");
|
logger.error({ err: error }, "API Series - Erreur:");
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ export function ClientSeriesPage({
|
|||||||
totalElements={books.totalElements}
|
totalElements={books.totalElements}
|
||||||
defaultShowOnlyUnread={preferences.showOnlyUnread}
|
defaultShowOnlyUnread={preferences.showOnlyUnread}
|
||||||
showOnlyUnread={unreadOnly}
|
showOnlyUnread={unreadOnly}
|
||||||
|
onRefresh={() => handleRefresh(seriesId)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { KomgaSeries } from "@/types/komga";
|
|||||||
import { SearchInput } from "./SearchInput";
|
import { SearchInput } from "./SearchInput";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
import { useDisplayPreferences } from "@/hooks/useDisplayPreferences";
|
import { useDisplayPreferences } from "@/hooks/useDisplayPreferences";
|
||||||
|
import { usePreferences } from "@/contexts/PreferencesContext";
|
||||||
import { PageSizeSelect } from "@/components/common/PageSizeSelect";
|
import { PageSizeSelect } from "@/components/common/PageSizeSelect";
|
||||||
import { CompactModeButton } from "@/components/common/CompactModeButton";
|
import { CompactModeButton } from "@/components/common/CompactModeButton";
|
||||||
import { ViewModeButton } from "@/components/common/ViewModeButton";
|
import { ViewModeButton } from "@/components/common/ViewModeButton";
|
||||||
@@ -38,6 +39,7 @@ export function PaginatedSeriesGrid({
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const [showOnlyUnread, setShowOnlyUnread] = useState(initialShowOnlyUnread);
|
const [showOnlyUnread, setShowOnlyUnread] = useState(initialShowOnlyUnread);
|
||||||
const { isCompact, itemsPerPage: displayItemsPerPage, viewMode } = useDisplayPreferences();
|
const { isCompact, itemsPerPage: displayItemsPerPage, viewMode } = useDisplayPreferences();
|
||||||
|
const { updatePreferences } = usePreferences();
|
||||||
|
|
||||||
// Utiliser la taille de page effective (depuis l'URL ou les préférences)
|
// Utiliser la taille de page effective (depuis l'URL ou les préférences)
|
||||||
const effectivePageSize = pageSize || displayItemsPerPage;
|
const effectivePageSize = pageSize || displayItemsPerPage;
|
||||||
@@ -87,6 +89,13 @@ export function PaginatedSeriesGrid({
|
|||||||
page: "1",
|
page: "1",
|
||||||
unread: newUnreadState ? "true" : "false",
|
unread: newUnreadState ? "true" : "false",
|
||||||
});
|
});
|
||||||
|
// Sauvegarder la préférence dans la base de données
|
||||||
|
try {
|
||||||
|
await updatePreferences({ showOnlyUnread: newUnreadState });
|
||||||
|
} catch (error) {
|
||||||
|
// Log l'erreur mais ne bloque pas l'utilisateur
|
||||||
|
console.error("Erreur lors de la sauvegarde de la préférence:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageSizeChange = async (size: number) => {
|
const handlePageSizeChange = async (size: number) => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { KomgaBook } from "@/types/komga";
|
import type { KomgaBook } from "@/types/komga";
|
||||||
import { BookCover } from "@/components/ui/book-cover";
|
import { BookCover } from "@/components/ui/book-cover";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useBookOfflineStatus } from "@/hooks/useBookOfflineStatus";
|
import { useBookOfflineStatus } from "@/hooks/useBookOfflineStatus";
|
||||||
@@ -11,6 +11,7 @@ interface BookGridProps {
|
|||||||
books: KomgaBook[];
|
books: KomgaBook[];
|
||||||
onBookClick: (book: KomgaBook) => void;
|
onBookClick: (book: KomgaBook) => void;
|
||||||
isCompact?: boolean;
|
isCompact?: boolean;
|
||||||
|
onRefresh?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BookCardProps {
|
interface BookCardProps {
|
||||||
@@ -61,12 +62,18 @@ function BookCard({ book, onBookClick, onSuccess, isCompact }: BookCardProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BookGrid({ books, onBookClick, isCompact = false }: BookGridProps) {
|
export function BookGrid({ books, onBookClick, isCompact = false, onRefresh }: BookGridProps) {
|
||||||
const [localBooks, setLocalBooks] = useState(books);
|
const [localBooks, setLocalBooks] = useState(books);
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
const previousBookIdsRef = useRef<string>(books.map((b) => b.id).join(","));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Ne réinitialiser que si les IDs des livres ont changé (nouvelle page, nouveau filtre, etc.)
|
||||||
|
const newIds = books.map((b) => b.id).join(",");
|
||||||
|
if (previousBookIdsRef.current !== newIds) {
|
||||||
setLocalBooks(books);
|
setLocalBooks(books);
|
||||||
|
previousBookIdsRef.current = newIds;
|
||||||
|
}
|
||||||
}, [books]);
|
}, [books]);
|
||||||
|
|
||||||
if (!localBooks.length) {
|
if (!localBooks.length) {
|
||||||
@@ -107,6 +114,8 @@ export function BookGrid({ books, onBookClick, isCompact = false }: BookGridProp
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Rafraîchir les données après avoir marqué comme lu/non lu
|
||||||
|
onRefresh?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import type { KomgaBook } from "@/types/komga";
|
import type { KomgaBook } from "@/types/komga";
|
||||||
import { BookCover } from "@/components/ui/book-cover";
|
import { BookCover } from "@/components/ui/book-cover";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useBookOfflineStatus } from "@/hooks/useBookOfflineStatus";
|
import { useBookOfflineStatus } from "@/hooks/useBookOfflineStatus";
|
||||||
@@ -18,6 +18,7 @@ interface BookListProps {
|
|||||||
books: KomgaBook[];
|
books: KomgaBook[];
|
||||||
onBookClick: (book: KomgaBook) => void;
|
onBookClick: (book: KomgaBook) => void;
|
||||||
isCompact?: boolean;
|
isCompact?: boolean;
|
||||||
|
onRefresh?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BookListItemProps {
|
interface BookListItemProps {
|
||||||
@@ -288,12 +289,18 @@ function BookListItem({ book, onBookClick, onSuccess, isCompact = false }: BookL
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BookList({ books, onBookClick, isCompact = false }: BookListProps) {
|
export function BookList({ books, onBookClick, isCompact = false, onRefresh }: BookListProps) {
|
||||||
const [localBooks, setLocalBooks] = useState(books);
|
const [localBooks, setLocalBooks] = useState(books);
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
const previousBookIdsRef = useRef<string>(books.map((b) => b.id).join(","));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Ne réinitialiser que si les IDs des livres ont changé (nouvelle page, nouveau filtre, etc.)
|
||||||
|
const newIds = books.map((b) => b.id).join(",");
|
||||||
|
if (previousBookIdsRef.current !== newIds) {
|
||||||
setLocalBooks(books);
|
setLocalBooks(books);
|
||||||
|
previousBookIdsRef.current = newIds;
|
||||||
|
}
|
||||||
}, [books]);
|
}, [books]);
|
||||||
|
|
||||||
if (!localBooks.length) {
|
if (!localBooks.length) {
|
||||||
@@ -334,6 +341,8 @@ export function BookList({ books, onBookClick, isCompact = false }: BookListProp
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Rafraîchir les données après avoir marqué comme lu/non lu
|
||||||
|
onRefresh?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useState, useEffect, useCallback } from "react";
|
|||||||
import type { KomgaBook } from "@/types/komga";
|
import type { KomgaBook } from "@/types/komga";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
import { useDisplayPreferences } from "@/hooks/useDisplayPreferences";
|
import { useDisplayPreferences } from "@/hooks/useDisplayPreferences";
|
||||||
|
import { usePreferences } from "@/contexts/PreferencesContext";
|
||||||
import { PageSizeSelect } from "@/components/common/PageSizeSelect";
|
import { PageSizeSelect } from "@/components/common/PageSizeSelect";
|
||||||
import { CompactModeButton } from "@/components/common/CompactModeButton";
|
import { CompactModeButton } from "@/components/common/CompactModeButton";
|
||||||
import { ViewModeButton } from "@/components/common/ViewModeButton";
|
import { ViewModeButton } from "@/components/common/ViewModeButton";
|
||||||
@@ -20,6 +21,7 @@ interface PaginatedBookGridProps {
|
|||||||
totalElements: number;
|
totalElements: number;
|
||||||
defaultShowOnlyUnread: boolean;
|
defaultShowOnlyUnread: boolean;
|
||||||
showOnlyUnread: boolean;
|
showOnlyUnread: boolean;
|
||||||
|
onRefresh?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PaginatedBookGrid({
|
export function PaginatedBookGrid({
|
||||||
@@ -29,12 +31,14 @@ export function PaginatedBookGrid({
|
|||||||
totalElements,
|
totalElements,
|
||||||
defaultShowOnlyUnread,
|
defaultShowOnlyUnread,
|
||||||
showOnlyUnread: initialShowOnlyUnread,
|
showOnlyUnread: initialShowOnlyUnread,
|
||||||
|
onRefresh,
|
||||||
}: PaginatedBookGridProps) {
|
}: PaginatedBookGridProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const [showOnlyUnread, setShowOnlyUnread] = useState(initialShowOnlyUnread);
|
const [showOnlyUnread, setShowOnlyUnread] = useState(initialShowOnlyUnread);
|
||||||
const { isCompact, itemsPerPage, viewMode } = useDisplayPreferences();
|
const { isCompact, itemsPerPage, viewMode } = useDisplayPreferences();
|
||||||
|
const { updatePreferences } = usePreferences();
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
|
||||||
const updateUrlParams = useCallback(
|
const updateUrlParams = useCallback(
|
||||||
@@ -81,6 +85,13 @@ export function PaginatedBookGrid({
|
|||||||
page: "1",
|
page: "1",
|
||||||
unread: newUnreadState ? "true" : "false",
|
unread: newUnreadState ? "true" : "false",
|
||||||
});
|
});
|
||||||
|
// Sauvegarder la préférence dans la base de données
|
||||||
|
try {
|
||||||
|
await updatePreferences({ showOnlyUnread: newUnreadState });
|
||||||
|
} catch (error) {
|
||||||
|
// Log l'erreur mais ne bloque pas l'utilisateur
|
||||||
|
console.error("Erreur lors de la sauvegarde de la préférence:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageSizeChange = async (size: number) => {
|
const handlePageSizeChange = async (size: number) => {
|
||||||
@@ -121,9 +132,19 @@ export function PaginatedBookGrid({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewMode === "grid" ? (
|
{viewMode === "grid" ? (
|
||||||
<BookGrid books={books} onBookClick={handleBookClick} isCompact={isCompact} />
|
<BookGrid
|
||||||
|
books={books}
|
||||||
|
onBookClick={handleBookClick}
|
||||||
|
isCompact={isCompact}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<BookList books={books} onBookClick={handleBookClick} isCompact={isCompact} />
|
<BookList
|
||||||
|
books={books}
|
||||||
|
onBookClick={handleBookClick}
|
||||||
|
isCompact={isCompact}
|
||||||
|
onRefresh={onRefresh}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-between">
|
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-between">
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface KomgaRequestInit extends RequestInit {
|
|||||||
|
|
||||||
interface KomgaUrlBuilder {
|
interface KomgaUrlBuilder {
|
||||||
path: string;
|
path: string;
|
||||||
params?: Record<string, string>;
|
params?: Record<string, string | string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class BaseApiService {
|
export abstract class BaseApiService {
|
||||||
@@ -136,15 +136,23 @@ export abstract class BaseApiService {
|
|||||||
protected static buildUrl(
|
protected static buildUrl(
|
||||||
config: AuthConfig,
|
config: AuthConfig,
|
||||||
path: string,
|
path: string,
|
||||||
params?: Record<string, string>
|
params?: Record<string, string | string[]>
|
||||||
): string {
|
): string {
|
||||||
const url = new URL(`${config.serverUrl}/api/v1/${path}`);
|
const url = new URL(`${config.serverUrl}/api/v1/${path}`);
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((v) => {
|
||||||
|
if (v !== undefined) {
|
||||||
|
url.searchParams.append(key, v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
url.searchParams.append(key, value);
|
url.searchParams.append(key, value);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ import type { ImageResponse } from "./image.service";
|
|||||||
import { ImageService } from "./image.service";
|
import { ImageService } from "./image.service";
|
||||||
import { PreferencesService } from "./preferences.service";
|
import { PreferencesService } from "./preferences.service";
|
||||||
import { ConfigDBService } from "./config-db.service";
|
import { ConfigDBService } from "./config-db.service";
|
||||||
|
import { SeriesService } from "./series.service";
|
||||||
import { ERROR_CODES } from "../../constants/errorCodes";
|
import { ERROR_CODES } from "../../constants/errorCodes";
|
||||||
import { AppError } from "../../utils/errors";
|
import { AppError } from "../../utils/errors";
|
||||||
import { SeriesService } from "./series.service";
|
|
||||||
import type { Series } from "@/types/series";
|
|
||||||
import logger from "@/lib/logger";
|
import logger from "@/lib/logger";
|
||||||
|
|
||||||
export class BookService extends BaseApiService {
|
export class BookService extends BaseApiService {
|
||||||
@@ -43,10 +42,33 @@ export class BookService extends BaseApiService {
|
|||||||
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {}, error);
|
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {}, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static async getNextBook(bookId: string, seriesId: string): Promise<KomgaBook | null> {
|
public static async getNextBook(bookId: string, _seriesId: string): Promise<KomgaBook | null> {
|
||||||
const books = await SeriesService.getAllSeriesBooks(seriesId);
|
try {
|
||||||
const currentIndex = books.findIndex((book) => book.id === bookId);
|
// Utiliser l'endpoint natif Komga pour obtenir le livre suivant
|
||||||
return books[currentIndex + 1] || null;
|
return await this.fetchFromApi<KomgaBook>({ path: `books/${bookId}/next` });
|
||||||
|
} catch (error) {
|
||||||
|
// Si le livre suivant n'existe pas, Komga retourne 404
|
||||||
|
// On retourne null dans ce cas
|
||||||
|
if (
|
||||||
|
error instanceof AppError &&
|
||||||
|
error.code === ERROR_CODES.KOMGA.HTTP_ERROR &&
|
||||||
|
(error as any).context?.status === 404
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Pour les autres erreurs, on les propage
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getBookSeriesId(bookId: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
// Récupérer le livre sans cache pour éviter les données obsolètes
|
||||||
|
const book = await this.fetchFromApi<KomgaBook>({ path: `books/${bookId}` });
|
||||||
|
return book.seriesId;
|
||||||
|
} catch (error) {
|
||||||
|
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {}, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async updateReadProgress(
|
static async updateReadProgress(
|
||||||
@@ -191,34 +213,7 @@ export class BookService extends BaseApiService {
|
|||||||
|
|
||||||
const { LibraryService } = await import("./library.service");
|
const { LibraryService } = await import("./library.service");
|
||||||
|
|
||||||
// Essayer d'abord d'utiliser le cache des bibliothèques
|
// Faire une requête légère : prendre une page de séries d'une bibliothèque au hasard
|
||||||
const allSeriesFromCache: Series[] = [];
|
|
||||||
|
|
||||||
for (const libraryId of libraryIds) {
|
|
||||||
try {
|
|
||||||
// Essayer de récupérer les séries depuis le cache (rapide si en cache)
|
|
||||||
const series = await LibraryService.getAllLibrarySeries(libraryId);
|
|
||||||
allSeriesFromCache.push(...series);
|
|
||||||
} catch {
|
|
||||||
// Si erreur, on continue avec les autres bibliothèques
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allSeriesFromCache.length > 0) {
|
|
||||||
// Choisir une série au hasard parmi toutes celles trouvées
|
|
||||||
const randomSeriesIndex = Math.floor(Math.random() * allSeriesFromCache.length);
|
|
||||||
const randomSeries = allSeriesFromCache[randomSeriesIndex];
|
|
||||||
|
|
||||||
// Récupérer les books de cette série
|
|
||||||
const books = await SeriesService.getAllSeriesBooks(randomSeries.id);
|
|
||||||
|
|
||||||
if (books.length > 0) {
|
|
||||||
const randomBookIndex = Math.floor(Math.random() * books.length);
|
|
||||||
return books[randomBookIndex].id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si pas de cache, faire une requête légère : prendre une page de séries d'une bibliothèque au hasard
|
|
||||||
const randomLibraryIndex = Math.floor(Math.random() * libraryIds.length);
|
const randomLibraryIndex = Math.floor(Math.random() * libraryIds.length);
|
||||||
const randomLibraryId = libraryIds[randomLibraryIndex];
|
const randomLibraryId = libraryIds[randomLibraryIndex];
|
||||||
|
|
||||||
@@ -235,17 +230,17 @@ export class BookService extends BaseApiService {
|
|||||||
const randomSeriesIndex = Math.floor(Math.random() * seriesResponse.content.length);
|
const randomSeriesIndex = Math.floor(Math.random() * seriesResponse.content.length);
|
||||||
const randomSeries = seriesResponse.content[randomSeriesIndex];
|
const randomSeries = seriesResponse.content[randomSeriesIndex];
|
||||||
|
|
||||||
// Récupérer les books de cette série
|
// Récupérer les books de cette série avec pagination
|
||||||
const books = await SeriesService.getAllSeriesBooks(randomSeries.id);
|
const booksResponse = await SeriesService.getSeriesBooks(randomSeries.id, 0, 100);
|
||||||
|
|
||||||
if (books.length === 0) {
|
if (booksResponse.content.length === 0) {
|
||||||
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {
|
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {
|
||||||
message: "Aucun livre trouvé dans la série",
|
message: "Aucun livre trouvé dans la série",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const randomBookIndex = Math.floor(Math.random() * books.length);
|
const randomBookIndex = Math.floor(Math.random() * booksResponse.content.length);
|
||||||
return books[randomBookIndex].id;
|
return booksResponse.content[randomBookIndex].id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AppError) {
|
if (error instanceof AppError) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -35,45 +35,6 @@ export class LibraryService extends BaseApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getAllLibrarySeries(libraryId: string): Promise<Series[]> {
|
|
||||||
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<LibraryResponse<Series>>(
|
|
||||||
cacheKey,
|
|
||||||
async () =>
|
|
||||||
this.fetchFromApi<LibraryResponse<Series>>(
|
|
||||||
{
|
|
||||||
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(
|
static async getLibrarySeries(
|
||||||
libraryId: string,
|
libraryId: string,
|
||||||
page: number = 0,
|
page: number = 0,
|
||||||
@@ -85,19 +46,47 @@ export class LibraryService extends BaseApiService {
|
|||||||
const headers = { "Content-Type": "application/json" };
|
const headers = { "Content-Type": "application/json" };
|
||||||
|
|
||||||
// Construction du body de recherche pour Komga
|
// Construction du body de recherche pour Komga
|
||||||
const condition: Record<string, any> = {
|
let condition: any;
|
||||||
|
|
||||||
|
if (unreadOnly) {
|
||||||
|
// Utiliser allOf pour combiner libraryId avec anyOf pour UNREAD ou IN_PROGRESS
|
||||||
|
condition = {
|
||||||
|
allOf: [
|
||||||
|
{
|
||||||
|
libraryId: {
|
||||||
|
operator: "is",
|
||||||
|
value: libraryId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
anyOf: [
|
||||||
|
{
|
||||||
|
readStatus: {
|
||||||
|
operator: "is",
|
||||||
|
value: "UNREAD",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
readStatus: {
|
||||||
|
operator: "is",
|
||||||
|
value: "IN_PROGRESS",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
condition = {
|
||||||
libraryId: {
|
libraryId: {
|
||||||
operator: "is",
|
operator: "is",
|
||||||
value: libraryId,
|
value: libraryId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const searchBody = { condition };
|
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
|
// Clé de cache incluant tous les paramètres
|
||||||
const cacheKey = `library-${libraryId}-series-p${page}-s${size}-u${unreadOnly}-q${
|
const cacheKey = `library-${libraryId}-series-p${page}-s${size}-u${unreadOnly}-q${
|
||||||
search || ""
|
search || ""
|
||||||
@@ -106,9 +95,9 @@ export class LibraryService extends BaseApiService {
|
|||||||
const response = await this.fetchWithCache<LibraryResponse<Series>>(
|
const response = await this.fetchWithCache<LibraryResponse<Series>>(
|
||||||
cacheKey,
|
cacheKey,
|
||||||
async () => {
|
async () => {
|
||||||
const params: Record<string, string> = {
|
const params: Record<string, string | string[]> = {
|
||||||
page: String(page),
|
page: String(page),
|
||||||
size: String(fetchSize),
|
size: String(size),
|
||||||
sort: "metadata.titleSort,asc",
|
sort: "metadata.titleSort,asc",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -129,27 +118,13 @@ export class LibraryService extends BaseApiService {
|
|||||||
"SERIES"
|
"SERIES"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filtrer les séries supprimées côté client (léger)
|
// Filtrer uniquement les séries supprimées côté client (léger)
|
||||||
let filteredContent = response.content.filter((series) => !series.deleted);
|
const 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 {
|
return {
|
||||||
...response,
|
...response,
|
||||||
content: filteredContent,
|
content: filteredContent,
|
||||||
numberOfElements: filteredContent.length,
|
numberOfElements: filteredContent.length,
|
||||||
// Garder totalElements et totalPages de Komga pour la pagination
|
|
||||||
// Ils seront légèrement inexacts mais fonctionnels
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
|
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
|
||||||
@@ -162,8 +137,6 @@ export class LibraryService extends BaseApiService {
|
|||||||
// Invalider toutes les clés de cache pour cette bibliothèque
|
// Invalider toutes les clés de cache pour cette bibliothèque
|
||||||
// Format: library-{id}-series-p{page}-s{size}-u{unread}-q{search}
|
// Format: library-{id}-series-p{page}-s{size}-u{unread}-q{search}
|
||||||
await cacheService.deleteAll(`library-${libraryId}-series-`);
|
await cacheService.deleteAll(`library-${libraryId}-series-`);
|
||||||
// Invalider aussi l'ancienne clé pour compatibilité
|
|
||||||
await cacheService.delete(`library-${libraryId}-all-series`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error);
|
throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,52 +45,6 @@ export class SeriesService extends BaseApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getAllSeriesBooks(seriesId: string): Promise<KomgaBook[]> {
|
|
||||||
try {
|
|
||||||
const headers = { "Content-Type": "application/json" };
|
|
||||||
|
|
||||||
const searchBody = {
|
|
||||||
condition: {
|
|
||||||
seriesId: {
|
|
||||||
operator: "is",
|
|
||||||
value: seriesId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const cacheKey = `series-${seriesId}-all-books`;
|
|
||||||
const response = await this.fetchWithCache<LibraryResponse<KomgaBook>>(
|
|
||||||
cacheKey,
|
|
||||||
async () =>
|
|
||||||
this.fetchFromApi<LibraryResponse<KomgaBook>>(
|
|
||||||
{
|
|
||||||
path: "books/list",
|
|
||||||
params: {
|
|
||||||
size: "1000", // On récupère un maximum de livres
|
|
||||||
},
|
|
||||||
},
|
|
||||||
headers,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(searchBody),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
"BOOKS"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.content.length) {
|
|
||||||
throw new AppError(ERROR_CODES.SERIES.NO_BOOKS_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.content;
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AppError) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getSeriesBooks(
|
static async getSeriesBooks(
|
||||||
seriesId: string,
|
seriesId: string,
|
||||||
page: number = 0,
|
page: number = 0,
|
||||||
@@ -101,36 +55,56 @@ export class SeriesService extends BaseApiService {
|
|||||||
const headers = { "Content-Type": "application/json" };
|
const headers = { "Content-Type": "application/json" };
|
||||||
|
|
||||||
// Construction du body de recherche pour Komga
|
// Construction du body de recherche pour Komga
|
||||||
const condition: Record<string, any> = {
|
let condition: any;
|
||||||
|
|
||||||
|
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: {
|
seriesId: {
|
||||||
operator: "is",
|
operator: "is",
|
||||||
value: seriesId,
|
value: seriesId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filtre unread natif Komga (readStatus != READ)
|
|
||||||
if (unreadOnly) {
|
|
||||||
condition.readStatus = {
|
|
||||||
operator: "isNot",
|
|
||||||
value: "READ",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchBody = { condition };
|
const searchBody = { condition };
|
||||||
|
|
||||||
// Pour le filtre unread, on récupère plus d'éléments car on filtre aussi les deleted côté client
|
|
||||||
// Estimation : ~10% des livres sont supprimés, donc on récupère légèrement plus
|
|
||||||
const fetchSize = unreadOnly ? size : size;
|
|
||||||
|
|
||||||
// Clé de cache incluant tous les paramètres
|
// Clé de cache incluant tous les paramètres
|
||||||
const cacheKey = `series-${seriesId}-books-p${page}-s${size}-u${unreadOnly}`;
|
const cacheKey = `series-${seriesId}-books-p${page}-s${size}-u${unreadOnly}`;
|
||||||
|
|
||||||
const response = await this.fetchWithCache<LibraryResponse<KomgaBook>>(
|
const response = await this.fetchWithCache<LibraryResponse<KomgaBook>>(
|
||||||
cacheKey,
|
cacheKey,
|
||||||
async () => {
|
async () => {
|
||||||
const params: Record<string, string> = {
|
const params: Record<string, string | string[]> = {
|
||||||
page: String(page),
|
page: String(page),
|
||||||
size: String(fetchSize),
|
size: String(size),
|
||||||
sort: "number,asc",
|
sort: "number,asc",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,23 +120,13 @@ export class SeriesService extends BaseApiService {
|
|||||||
"BOOKS"
|
"BOOKS"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filtrer les livres supprimés côté client (léger)
|
// Filtrer uniquement les livres supprimés côté client (léger)
|
||||||
let filteredContent = response.content.filter((book: KomgaBook) => !book.deleted);
|
const filteredContent = response.content.filter((book: KomgaBook) => !book.deleted);
|
||||||
|
|
||||||
// Si on a filtré des livres supprimés, prendre uniquement les `size` premiers
|
|
||||||
if (filteredContent.length > size) {
|
|
||||||
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)
|
|
||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
content: filteredContent,
|
content: filteredContent,
|
||||||
numberOfElements: filteredContent.length,
|
numberOfElements: filteredContent.length,
|
||||||
// Garder totalElements et totalPages de Komga pour la pagination
|
|
||||||
// Ils seront légèrement inexacts mais fonctionnels
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
|
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
|
||||||
@@ -175,8 +139,6 @@ export class SeriesService extends BaseApiService {
|
|||||||
// Invalider toutes les clés de cache pour cette série
|
// Invalider toutes les clés de cache pour cette série
|
||||||
// Format: series-{id}-books-p{page}-s{size}-u{unread}
|
// Format: series-{id}-books-p{page}-s{size}-u{unread}
|
||||||
await cacheService.deleteAll(`series-${seriesId}-books-`);
|
await cacheService.deleteAll(`series-${seriesId}-books-`);
|
||||||
// Invalider aussi l'ancienne clé pour compatibilité
|
|
||||||
await cacheService.delete(`series-${seriesId}-all-books`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error);
|
throw new AppError(ERROR_CODES.CACHE.DELETE_ERROR, {}, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,20 +19,26 @@ class ServerCacheService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Configuration des temps de cache en millisecondes
|
// Configuration des temps de cache en millisecondes
|
||||||
|
private static readonly oneMinute = 1 * 60 * 1000;
|
||||||
|
private static readonly twoMinutes = 2 * 60 * 1000;
|
||||||
private static readonly fiveMinutes = 5 * 60 * 1000;
|
private static readonly fiveMinutes = 5 * 60 * 1000;
|
||||||
private static readonly tenMinutes = 10 * 60 * 1000;
|
private static readonly tenMinutes = 10 * 60 * 1000;
|
||||||
private static readonly twentyFourHours = 24 * 60 * 60 * 1000;
|
private static readonly twentyFourHours = 24 * 60 * 60 * 1000;
|
||||||
private static readonly oneMinute = 1 * 60 * 1000;
|
|
||||||
private static readonly oneWeek = 7 * 24 * 60 * 60 * 1000;
|
private static readonly oneWeek = 7 * 24 * 60 * 60 * 1000;
|
||||||
private static readonly noCache = 0;
|
private static readonly noCache = 0;
|
||||||
|
|
||||||
// Configuration des temps de cache
|
// Configuration des temps de cache
|
||||||
|
// Optimisé pour la pagination native Komga :
|
||||||
|
// - Listes paginées (SERIES, BOOKS) : TTL court (2 min) car données fraîches + progression utilisateur
|
||||||
|
// - Données agrégées (HOME) : TTL moyen (10 min) car plusieurs sources
|
||||||
|
// - Données statiques (LIBRARIES) : TTL long (24h) car changent rarement
|
||||||
|
// - Images : TTL très long (7 jours) car immuables
|
||||||
private static readonly DEFAULT_TTL = {
|
private static readonly DEFAULT_TTL = {
|
||||||
DEFAULT: ServerCacheService.fiveMinutes,
|
DEFAULT: ServerCacheService.fiveMinutes,
|
||||||
HOME: ServerCacheService.tenMinutes,
|
HOME: ServerCacheService.tenMinutes,
|
||||||
LIBRARIES: ServerCacheService.twentyFourHours,
|
LIBRARIES: ServerCacheService.twentyFourHours,
|
||||||
SERIES: ServerCacheService.fiveMinutes,
|
SERIES: ServerCacheService.twoMinutes, // Listes paginées avec progression
|
||||||
BOOKS: ServerCacheService.fiveMinutes,
|
BOOKS: ServerCacheService.twoMinutes, // Listes paginées avec progression
|
||||||
IMAGES: ServerCacheService.oneWeek,
|
IMAGES: ServerCacheService.oneWeek,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -363,10 +369,47 @@ class ServerCacheService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const cacheDir = path.join(this.cacheDir, prefixKey);
|
// En mode fichier, parcourir récursivement tous les fichiers et supprimer ceux qui correspondent
|
||||||
if (fs.existsSync(cacheDir)) {
|
if (!fs.existsSync(this.cacheDir)) return;
|
||||||
fs.rmdirSync(cacheDir, { recursive: true });
|
|
||||||
|
const deleteMatchingFiles = (dirPath: string): void => {
|
||||||
|
const items = fs.readdirSync(dirPath);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const itemPath = path.join(dirPath, item);
|
||||||
|
try {
|
||||||
|
const stats = fs.statSync(itemPath);
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
deleteMatchingFiles(itemPath);
|
||||||
|
// Supprimer le répertoire s'il est vide après suppression des fichiers
|
||||||
|
try {
|
||||||
|
const remainingItems = fs.readdirSync(itemPath);
|
||||||
|
if (remainingItems.length === 0) {
|
||||||
|
fs.rmdirSync(itemPath);
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore les erreurs de suppression de répertoire
|
||||||
|
}
|
||||||
|
} else if (stats.isFile() && item.endsWith(".json")) {
|
||||||
|
// Extraire la clé du chemin relatif (sans l'extension .json)
|
||||||
|
const relativePath = path.relative(this.cacheDir, itemPath);
|
||||||
|
const key = relativePath.slice(0, -5).replace(/\\/g, "/"); // Remove .json and normalize slashes
|
||||||
|
|
||||||
|
if (key.startsWith(prefixKey)) {
|
||||||
|
fs.unlinkSync(itemPath);
|
||||||
|
if (process.env.CACHE_DEBUG === "true") {
|
||||||
|
logger.debug(`🗑️ [CACHE DELETE] ${key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ err: error, path: itemPath }, `Could not delete cache file ${itemPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteMatchingFiles(this.cacheDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user