diff --git a/src/app/api/komga/cache/clear/route.ts b/src/app/api/komga/cache/clear/route.ts index ebf4239..5bdbcec 100644 --- a/src/app/api/komga/cache/clear/route.ts +++ b/src/app/api/komga/cache/clear/route.ts @@ -1,15 +1,8 @@ import { NextResponse } from "next/server"; -import { serverCacheService } from "@/lib/services/server-cache.service"; +import { getServerCacheService } from "@/lib/services/server-cache.service"; export async function POST() { - try { - serverCacheService.clear(); - return NextResponse.json({ message: "Cache serveur supprimé avec succès" }); - } catch (error) { - console.error("Erreur lors de la suppression du cache serveur:", error); - return NextResponse.json( - { error: "Erreur lors de la suppression du cache serveur" }, - { status: 500 } - ); - } + const cacheService = await getServerCacheService(); + cacheService.clear(); + return NextResponse.json({ message: "Cache cleared" }); } diff --git a/src/app/api/komga/cache/mode/route.ts b/src/app/api/komga/cache/mode/route.ts index 28292c7..affb6d7 100644 --- a/src/app/api/komga/cache/mode/route.ts +++ b/src/app/api/komga/cache/mode/route.ts @@ -1,8 +1,9 @@ import { NextResponse } from "next/server"; -import { serverCacheService } from "@/lib/services/server-cache.service"; +import { getServerCacheService } from "@/lib/services/server-cache.service"; export async function GET() { - return NextResponse.json({ mode: serverCacheService.getCacheMode() }); + const cacheService = await getServerCacheService(); + return NextResponse.json({ mode: cacheService.getCacheMode() }); } export async function POST(request: Request) { @@ -15,8 +16,9 @@ export async function POST(request: Request) { ); } - serverCacheService.setCacheMode(mode); - return NextResponse.json({ mode: serverCacheService.getCacheMode() }); + const cacheService = await getServerCacheService(); + cacheService.setCacheMode(mode); + return NextResponse.json({ mode: cacheService.getCacheMode() }); } catch (error) { console.error("Erreur lors de la mise à jour du mode de cache:", error); return NextResponse.json({ error: "Invalid request" }, { status: 400 }); diff --git a/src/app/libraries/[libraryId]/page.tsx b/src/app/libraries/[libraryId]/page.tsx index df7fac2..8c6f21d 100644 --- a/src/app/libraries/[libraryId]/page.tsx +++ b/src/app/libraries/[libraryId]/page.tsx @@ -15,7 +15,7 @@ async function refreshLibrary(libraryId: string) { "use server"; try { - await LibraryService.clearLibrarySeriesCache(libraryId); + await LibraryService.invalidateLibrarySeriesCache(libraryId); revalidatePath(`/libraries/${libraryId}`); return { success: true }; diff --git a/src/app/page.tsx b/src/app/page.tsx index d751675..f05eb2b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,7 +7,7 @@ async function refreshHome() { "use server"; try { - await HomeService.clearHomeCache(); + await HomeService.invalidateHomeCache(); revalidatePath("/"); return { success: true }; } catch (error) { diff --git a/src/app/series/[seriesId]/page.tsx b/src/app/series/[seriesId]/page.tsx index db8909b..919b162 100644 --- a/src/app/series/[seriesId]/page.tsx +++ b/src/app/series/[seriesId]/page.tsx @@ -28,8 +28,8 @@ async function refreshSeries(seriesId: string) { "use server"; try { - await SeriesService.clearSeriesBooksCache(seriesId); - await SeriesService.clearSeriesCache(seriesId); + await SeriesService.invalidateSeriesBooksCache(seriesId); + await SeriesService.invalidateSeriesCache(seriesId); revalidatePath(`/series/${seriesId}`); return { success: true }; } catch (error) { diff --git a/src/components/downloads/DownloadManager.tsx b/src/components/downloads/DownloadManager.tsx index 0198c60..7f487f2 100644 --- a/src/components/downloads/DownloadManager.tsx +++ b/src/components/downloads/DownloadManager.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { KomgaBook } from "@/types/komga"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card } from "@/components/ui/card"; @@ -31,12 +31,11 @@ export function DownloadManager() { const [isLoading, setIsLoading] = useState(true); const { toast } = useToast(); - const getStorageKey = (bookId: string) => `book-status-${bookId}`; + const getStorageKey = useCallback((bookId: string) => `book-status-${bookId}`, []); - const loadDownloadedBooks = async () => { + const loadDownloadedBooks = useCallback(async () => { setIsLoading(true); try { - // Récupère tous les livres du localStorage const books: DownloadedBook[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); @@ -70,9 +69,9 @@ export function DownloadManager() { } finally { setIsLoading(false); } - }; + }, [toast]); - const updateBookStatuses = () => { + const updateBookStatuses = useCallback(() => { setDownloadedBooks((prevBooks) => { return prevBooks.map((downloadedBook) => { const status = JSON.parse( @@ -87,29 +86,25 @@ export function DownloadManager() { }; }); }); - }; + }, [getStorageKey]); useEffect(() => { loadDownloadedBooks(); - // Écoute les changements de statut des livres const handleStorageChange = (e: StorageEvent) => { if (e.key?.startsWith("book-status-")) { updateBookStatuses(); } }; - // Écoute les changements dans d'autres onglets window.addEventListener("storage", handleStorageChange); - - // Écoute les changements dans l'onglet courant const interval = setInterval(updateBookStatuses, 1000); return () => { window.removeEventListener("storage", handleStorageChange); clearInterval(interval); }; - }, []); + }, [loadDownloadedBooks, updateBookStatuses]); const handleDeleteBook = async (book: KomgaBook) => { try { diff --git a/src/components/ui/book-offline-button.tsx b/src/components/ui/book-offline-button.tsx index 3b1f815..3037345 100644 --- a/src/components/ui/book-offline-button.tsx +++ b/src/components/ui/book-offline-button.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import { Download, Check, Loader2 } from "lucide-react"; import { Button } from "./button"; import { useToast } from "./use-toast"; @@ -27,156 +27,141 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) { const [downloadProgress, setDownloadProgress] = useState(0); const { toast } = useToast(); - const getStorageKey = (bookId: string) => `book-status-${bookId}`; + const getStorageKey = useCallback((bookId: string) => `book-status-${bookId}`, []); - const getBookStatus = (bookId: string): BookDownloadStatus => { - try { - const status = localStorage.getItem(getStorageKey(bookId)); - return status ? JSON.parse(status) : { status: "idle", progress: 0, timestamp: 0 }; - } catch { - return { status: "idle", progress: 0, timestamp: 0 }; - } - }; - - const setBookStatus = (bookId: string, status: BookDownloadStatus) => { - localStorage.setItem(getStorageKey(bookId), JSON.stringify(status)); - }; - - const downloadBook = async (startFromPage: number = 1) => { - try { - const cache = await caches.open("stripstream-books"); - - // Marque le début du téléchargement - setBookStatus(book.id, { - status: "downloading", - progress: ((startFromPage - 1) / book.media.pagesCount) * 100, - timestamp: Date.now(), - lastDownloadedPage: startFromPage - 1, - }); - - // Ajoute le livre au cache si on commence depuis le début - if (startFromPage === 1) { - const pagesResponse = await fetch(`/api/komga/images/books/${book.id}/pages/1`); - if (!pagesResponse.ok) throw new Error("Erreur lors de la récupération des pages"); - await cache.put(`/api/komga/images/books/${book.id}/pages`, pagesResponse.clone()); + const getBookStatus = useCallback( + (bookId: string): BookDownloadStatus => { + try { + const status = localStorage.getItem(getStorageKey(bookId)); + return status ? JSON.parse(status) : { status: "idle", progress: 0, timestamp: 0 }; + } catch { + return { status: "idle", progress: 0, timestamp: 0 }; } + }, + [getStorageKey] + ); - // Cache chaque page avec retry - let failedPages = 0; - for (let i = startFromPage; i <= book.media.pagesCount; i++) { - let retryCount = 0; - const maxRetries = 3; + const setBookStatus = useCallback( + (bookId: string, status: BookDownloadStatus) => { + localStorage.setItem(getStorageKey(bookId), JSON.stringify(status)); + }, + [getStorageKey] + ); - while (retryCount < maxRetries) { - try { - const pageResponse = await fetch(`/api/komga/images/books/${book.id}/pages/${i}`); - if (!pageResponse.ok) { + const downloadBook = useCallback( + async (startFromPage: number = 1) => { + try { + const cache = await caches.open("stripstream-books"); + + // Marque le début du téléchargement + setBookStatus(book.id, { + status: "downloading", + progress: ((startFromPage - 1) / book.media.pagesCount) * 100, + timestamp: Date.now(), + lastDownloadedPage: startFromPage - 1, + }); + + // Ajoute le livre au cache si on commence depuis le début + if (startFromPage === 1) { + const pagesResponse = await fetch(`/api/komga/images/books/${book.id}/pages/1`); + if (!pagesResponse.ok) throw new Error("Erreur lors de la récupération des pages"); + await cache.put(`/api/komga/images/books/${book.id}/pages`, pagesResponse.clone()); + } + + // Cache chaque page avec retry + let failedPages = 0; + for (let i = startFromPage; i <= book.media.pagesCount; i++) { + let retryCount = 0; + const maxRetries = 3; + + while (retryCount < maxRetries) { + try { + const pageResponse = await fetch(`/api/komga/images/books/${book.id}/pages/${i}`); + if (!pageResponse.ok) { + retryCount++; + if (retryCount === maxRetries) { + failedPages++; + console.error( + `Échec du téléchargement de la page ${i} après ${maxRetries} tentatives` + ); + } + await new Promise((resolve) => setTimeout(resolve, 1000)); // Attendre 1s avant de réessayer + continue; + } + await cache.put( + `/api/komga/images/books/${book.id}/pages/${i}`, + pageResponse.clone() + ); + break; // Sortir de la boucle si réussi + } catch (error) { retryCount++; if (retryCount === maxRetries) { failedPages++; - console.error( - `Échec du téléchargement de la page ${i} après ${maxRetries} tentatives` - ); + console.error(`Erreur lors du téléchargement de la page ${i}:`, error); } - await new Promise((resolve) => setTimeout(resolve, 1000)); // Attendre 1s avant de réessayer - continue; + await new Promise((resolve) => setTimeout(resolve, 1000)); } - await cache.put(`/api/komga/images/books/${book.id}/pages/${i}`, pageResponse.clone()); - break; // Sortir de la boucle si réussi - } catch (error) { - retryCount++; - if (retryCount === maxRetries) { - failedPages++; - console.error(`Erreur lors du téléchargement de la page ${i}:`, error); - } - await new Promise((resolve) => setTimeout(resolve, 1000)); + } + + // Mise à jour du statut + const progress = (i / book.media.pagesCount) * 100; + setDownloadProgress(progress); + setBookStatus(book.id, { + status: "downloading", + progress, + timestamp: Date.now(), + lastDownloadedPage: i, + }); + + // Vérifier si le statut a changé pendant le téléchargement + const currentStatus = getBookStatus(book.id); + if (currentStatus.status === "idle") { + // Le téléchargement a été annulé + throw new Error("Téléchargement annulé"); } } - // Mise à jour du statut - const progress = (i / book.media.pagesCount) * 100; - setDownloadProgress(progress); - setBookStatus(book.id, { - status: "downloading", - progress, - timestamp: Date.now(), - lastDownloadedPage: i, - }); - - // Vérifier si le statut a changé pendant le téléchargement - const currentStatus = getBookStatus(book.id); - if (currentStatus.status === "idle") { - // Le téléchargement a été annulé - throw new Error("Téléchargement annulé"); - } - } - - if (failedPages > 0) { - // Si des pages ont échoué, on supprime tout le cache pour ce livre - await cache.delete(`/api/komga/images/books/${book.id}/pages`); - for (let i = 1; i <= book.media.pagesCount; i++) { - await cache.delete(`/api/komga/images/books/${book.id}/pages/${i}`); - } - setIsAvailableOffline(false); - setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() }); - toast({ - title: "Erreur", - description: `${failedPages} page(s) n'ont pas pu être téléchargées. Le livre ne sera pas disponible hors ligne.`, - variant: "destructive", - }); - } else { - setIsAvailableOffline(true); - setBookStatus(book.id, { status: "available", progress: 100, timestamp: Date.now() }); - toast({ - title: "Livre téléchargé", - description: "Le livre est maintenant disponible hors ligne", - }); - } - } catch (error) { - console.error("Erreur lors du téléchargement:", error); - // Ne pas changer le statut si le téléchargement a été volontairement annulé - if ((error as Error)?.message !== "Téléchargement annulé") { - setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() }); - toast({ - title: "Erreur", - description: "Une erreur est survenue lors du téléchargement", - variant: "destructive", - }); - } - } finally { - setIsLoading(false); - setDownloadProgress(0); - } - }; - - // Vérifie si le livre est déjà disponible hors ligne - useEffect(() => { - const checkStatus = async () => { - const storedStatus = getBookStatus(book.id); - - // Si le livre est marqué comme en cours de téléchargement - if (storedStatus.status === "downloading") { - // Si le téléchargement a commencé il y a plus de 5 minutes, on considère qu'il a échoué - if (Date.now() - storedStatus.timestamp > 5 * 60 * 1000) { + if (failedPages > 0) { + // Si des pages ont échoué, on supprime tout le cache pour ce livre + await cache.delete(`/api/komga/images/books/${book.id}/pages`); + for (let i = 1; i <= book.media.pagesCount; i++) { + await cache.delete(`/api/komga/images/books/${book.id}/pages/${i}`); + } + setIsAvailableOffline(false); setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() }); - setIsLoading(false); - setDownloadProgress(0); + toast({ + title: "Erreur", + description: `${failedPages} page(s) n'ont pas pu être téléchargées. Le livre ne sera pas disponible hors ligne.`, + variant: "destructive", + }); } else { - // On reprend le téléchargement là où il s'était arrêté - setIsLoading(true); - setDownloadProgress(storedStatus.progress); - const startFromPage = (storedStatus.lastDownloadedPage || 0) + 1; - downloadBook(startFromPage); + setIsAvailableOffline(true); + setBookStatus(book.id, { status: "available", progress: 100, timestamp: Date.now() }); + toast({ + title: "Livre téléchargé", + description: "Le livre est maintenant disponible hors ligne", + }); } + } catch (error) { + console.error("Erreur lors du téléchargement:", error); + // Ne pas changer le statut si le téléchargement a été volontairement annulé + if ((error as Error)?.message !== "Téléchargement annulé") { + setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() }); + toast({ + title: "Erreur", + description: "Une erreur est survenue lors du téléchargement", + variant: "destructive", + }); + } + } finally { + setIsLoading(false); + setDownloadProgress(0); } + }, + [book.id, book.media.pagesCount, getBookStatus, setBookStatus, toast] + ); - await checkOfflineAvailability(); - }; - - checkStatus(); - }, [book.id]); - - const checkOfflineAvailability = async () => { + const checkOfflineAvailability = useCallback(async () => { if (!("caches" in window)) return; try { @@ -209,7 +194,30 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) { console.error("Erreur lors de la vérification du cache:", error); setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() }); } - }; + }, [book.id, book.media.pagesCount, setBookStatus]); + + useEffect(() => { + const checkStatus = async () => { + const storedStatus = getBookStatus(book.id); + + if (storedStatus.status === "downloading") { + if (Date.now() - storedStatus.timestamp > 5 * 60 * 1000) { + setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() }); + setIsLoading(false); + setDownloadProgress(0); + } else { + setIsLoading(true); + setDownloadProgress(storedStatus.progress); + const startFromPage = (storedStatus.lastDownloadedPage || 0) + 1; + downloadBook(startFromPage); + } + } + + await checkOfflineAvailability(); + }; + + checkStatus(); + }, [book.id, checkOfflineAvailability, downloadBook, getBookStatus, setBookStatus]); const handleToggleOffline = async () => { if (!("caches" in window)) { diff --git a/src/lib/services/base-api.service.ts b/src/lib/services/base-api.service.ts index 29e0bc3..0539c24 100644 --- a/src/lib/services/base-api.service.ts +++ b/src/lib/services/base-api.service.ts @@ -1,5 +1,5 @@ import { AuthConfig } from "@/types/auth"; -import { serverCacheService } from "./server-cache.service"; +import { getServerCacheService } from "./server-cache.service"; import { ConfigDBService } from "./config-db.service"; // Types de cache disponibles @@ -51,7 +51,8 @@ export abstract class BaseApiService { fetcher: () => Promise, type: CacheType = "DEFAULT" ): Promise { - return serverCacheService.getOrSet(key, fetcher, type); + const cacheService = await getServerCacheService(); + return cacheService.getOrSet(key, fetcher, type); } protected static handleError(error: unknown, defaultMessage: string): never { diff --git a/src/lib/services/home.service.ts b/src/lib/services/home.service.ts index 2da6fc2..3c331db 100644 --- a/src/lib/services/home.service.ts +++ b/src/lib/services/home.service.ts @@ -1,7 +1,7 @@ import { BaseApiService } from "./base-api.service"; import { KomgaBook, KomgaSeries } from "@/types/komga"; import { LibraryResponse } from "@/types/library"; -import { serverCacheService } from "./server-cache.service"; +import { getServerCacheService } from "./server-cache.service"; interface HomeData { ongoing: KomgaSeries[]; @@ -67,9 +67,10 @@ export class HomeService extends BaseApiService { } } - static async clearHomeCache() { - serverCacheService.delete("home-ongoing"); - serverCacheService.delete("home-recently-read"); - serverCacheService.delete("home-on-deck"); + static async invalidateHomeCache(): Promise { + const cacheService = await getServerCacheService(); + cacheService.delete("home-ongoing"); + cacheService.delete("home-recently-read"); + cacheService.delete("home-on-deck"); } } diff --git a/src/lib/services/library.service.ts b/src/lib/services/library.service.ts index dc8be01..a2ffdf2 100644 --- a/src/lib/services/library.service.ts +++ b/src/lib/services/library.service.ts @@ -1,7 +1,7 @@ import { BaseApiService } from "./base-api.service"; import { Library, LibraryResponse } from "@/types/library"; import { Series } from "@/types/series"; -import { serverCacheService } from "./server-cache.service"; +import { getServerCacheService } from "./server-cache.service"; export class LibraryService extends BaseApiService { static async getLibraries(): Promise { @@ -134,7 +134,8 @@ export class LibraryService extends BaseApiService { } } - static async clearLibrarySeriesCache(libraryId: string) { - serverCacheService.delete(`library-${libraryId}-all-series`); + static async invalidateLibrarySeriesCache(libraryId: string): Promise { + const cacheService = await getServerCacheService(); + cacheService.delete(`library-${libraryId}-all-series`); } } diff --git a/src/lib/services/series.service.ts b/src/lib/services/series.service.ts index 78b4aa5..3f774f1 100644 --- a/src/lib/services/series.service.ts +++ b/src/lib/services/series.service.ts @@ -4,7 +4,7 @@ import { KomgaBook, KomgaSeries } from "@/types/komga"; import { BookService } from "./book.service"; import { ImageService } from "./image.service"; import { PreferencesService } from "./preferences.service"; -import { serverCacheService } from "./server-cache.service"; +import { getServerCacheService } from "./server-cache.service"; export class SeriesService extends BaseApiService { static async getSeries(seriesId: string): Promise { @@ -19,8 +19,9 @@ export class SeriesService extends BaseApiService { } } - static async clearSeriesCache(seriesId: string) { - serverCacheService.delete(`series-${seriesId}`); + static async invalidateSeriesCache(seriesId: string): Promise { + const cacheService = await getServerCacheService(); + cacheService.delete(`series-${seriesId}`); } static async getAllSeriesBooks(seriesId: string): Promise { @@ -125,8 +126,9 @@ export class SeriesService extends BaseApiService { } } - static async clearSeriesBooksCache(seriesId: string) { - serverCacheService.delete(`series-${seriesId}-all-books`); + static async invalidateSeriesBooksCache(seriesId: string): Promise { + const cacheService = await getServerCacheService(); + cacheService.delete(`series-${seriesId}-all-books`); } static async getFirstBook(seriesId: string): Promise { diff --git a/src/lib/services/server-cache.service.ts b/src/lib/services/server-cache.service.ts index fc98fb3..de71eed 100644 --- a/src/lib/services/server-cache.service.ts +++ b/src/lib/services/server-cache.service.ts @@ -1,5 +1,6 @@ import fs from "fs"; import path from "path"; +import { PreferencesService } from "./preferences.service"; type CacheMode = "file" | "memory"; @@ -37,6 +38,17 @@ class ServerCacheService { this.cacheDir = path.join(process.cwd(), ".cache"); this.ensureCacheDirectory(); this.cleanExpiredCache(); + this.initializeCacheMode(); + } + + private async initializeCacheMode(): Promise { + try { + const preferences = await PreferencesService.getPreferences(); + this.setCacheMode(preferences.cacheMode); + } catch (error) { + console.error("Error initializing cache mode from preferences:", error); + // Keep default memory mode if preferences can't be loaded + } } private ensureCacheDirectory(): void { @@ -117,9 +129,10 @@ class ServerCacheService { cleanDirectory(this.cacheDir); } - public static getInstance(): ServerCacheService { + public static async getInstance(): Promise { if (!ServerCacheService.instance) { ServerCacheService.instance = new ServerCacheService(); + await ServerCacheService.instance.initializeCacheMode(); } return ServerCacheService.instance; } @@ -376,4 +389,15 @@ class ServerCacheService { } } -export const serverCacheService = ServerCacheService.getInstance(); +// Créer une instance initialisée du service +let initializedInstance: Promise; + +export const getServerCacheService = async (): Promise => { + if (!initializedInstance) { + initializedInstance = ServerCacheService.getInstance(); + } + return initializedInstance; +}; + +// Exporter aussi la classe pour les tests +export { ServerCacheService };