fix: download manager fixes and retries

This commit is contained in:
Julien Froidefond
2025-02-21 13:46:54 +01:00
parent ade8b372b6
commit 3e102ae933
2 changed files with 101 additions and 47 deletions

View File

@@ -10,6 +10,7 @@ import { useToast } from "@/components/ui/use-toast";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { BookOfflineButton } from "@/components/ui/book-offline-button";
type BookStatus = "idle" | "downloading" | "available" | "error"; type BookStatus = "idle" | "downloading" | "available" | "error";
@@ -113,9 +114,9 @@ export function DownloadManager() {
const handleDeleteBook = async (book: KomgaBook) => { const handleDeleteBook = async (book: KomgaBook) => {
try { try {
const cache = await caches.open("stripstream-books"); const cache = await caches.open("stripstream-books");
await cache.delete(`/api/komga/books/${book.id}/pages`); await cache.delete(`/api/komga/images/books/${book.id}/pages`);
for (let i = 1; i <= book.media.pagesCount; i++) { for (let i = 1; i <= book.media.pagesCount; i++) {
await cache.delete(`/api/komga/books/${book.id}/pages/${i}`); await cache.delete(`/api/komga/images/books/${book.id}/pages/${i}`);
} }
localStorage.removeItem(getStorageKey(book.id)); localStorage.removeItem(getStorageKey(book.id));
setDownloadedBooks((prev) => prev.filter((b) => b.book.id !== book.id)); setDownloadedBooks((prev) => prev.filter((b) => b.book.id !== book.id));
@@ -153,6 +154,7 @@ export function DownloadManager() {
return ( return (
<Tabs defaultValue="all" className="space-y-4"> <Tabs defaultValue="all" className="space-y-4">
<div className="flex items-center justify-between">
<TabsList> <TabsList>
<TabsTrigger value="all">Tous ({downloadedBooks.length})</TabsTrigger> <TabsTrigger value="all">Tous ({downloadedBooks.length})</TabsTrigger>
<TabsTrigger value="downloading"> <TabsTrigger value="downloading">
@@ -165,6 +167,25 @@ export function DownloadManager() {
Erreurs ({downloadedBooks.filter((b) => b.status.status === "error").length}) Erreurs ({downloadedBooks.filter((b) => b.status.status === "error").length})
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
{downloadedBooks.some((b) => b.status.status === "error") && (
<Button
variant="outline"
size="sm"
onClick={() => {
const errorBooks = downloadedBooks.filter((b) => b.status.status === "error");
errorBooks.forEach((book) => handleRetryDownload(book.book));
toast({
title: "Relance des téléchargements",
description: `${errorBooks.length} téléchargement(s) relancé(s)`,
});
}}
className="gap-2"
>
<Download className="h-4 w-4" />
Tout relancer
</Button>
)}
</div>
<TabsContent value="all" className="space-y-4"> <TabsContent value="all" className="space-y-4">
{downloadedBooks.map(({ book, status }) => ( {downloadedBooks.map(({ book, status }) => (
@@ -332,6 +353,8 @@ function BookDownloadCard({ book, status, onDelete, onRetry }: BookDownloadCardP
<Download className="h-5 w-5" /> <Download className="h-5 w-5" />
</Button> </Button>
)} )}
<BookOfflineButton book={book} />
{status.status !== "downloading" && (
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
@@ -341,6 +364,7 @@ function BookDownloadCard({ book, status, onDelete, onRetry }: BookDownloadCardP
> >
<Trash2 className="h-5 w-5" /> <Trash2 className="h-5 w-5" />
</Button> </Button>
)}
</div> </div>
</div> </div>
</Card> </Card>

View File

@@ -56,24 +56,44 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) {
// Ajoute le livre au cache si on commence depuis le début // Ajoute le livre au cache si on commence depuis le début
if (startFromPage === 1) { if (startFromPage === 1) {
const pagesResponse = await fetch(`/api/komga/books/${book.id}/pages`); const pagesResponse = await fetch(`/api/komga/images/books/${book.id}/pages/1`);
await cache.put(`/api/komga/books/${book.id}/pages`, pagesResponse.clone()); 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 // Cache chaque page avec retry
let failedPages = 0; let failedPages = 0;
for (let i = startFromPage; i <= book.media.pagesCount; i++) { for (let i = startFromPage; i <= book.media.pagesCount; i++) {
let retryCount = 0;
const maxRetries = 3;
while (retryCount < maxRetries) {
try { try {
const pageResponse = await fetch(`/api/komga/books/${book.id}/pages/${i}`); const pageResponse = await fetch(`/api/komga/images/books/${book.id}/pages/${i}`);
if (!pageResponse.ok) { if (!pageResponse.ok) {
retryCount++;
if (retryCount === maxRetries) {
failedPages++; 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; continue;
} }
await cache.put(`/api/komga/books/${book.id}/pages/${i}`, pageResponse.clone()); await cache.put(`/api/komga/images/books/${book.id}/pages/${i}`, pageResponse.clone());
break; // Sortir de la boucle si réussi
} catch (error) { } catch (error) {
console.error(`Erreur lors du téléchargement de la page ${i}:`, error); retryCount++;
if (retryCount === maxRetries) {
failedPages++; 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; const progress = (i / book.media.pagesCount) * 100;
setDownloadProgress(progress); setDownloadProgress(progress);
setBookStatus(book.id, { setBookStatus(book.id, {
@@ -82,13 +102,20 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) {
timestamp: Date.now(), timestamp: Date.now(),
lastDownloadedPage: i, 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) { if (failedPages > 0) {
// Si des pages ont échoué, on supprime tout le cache pour ce livre // Si des pages ont échoué, on supprime tout le cache pour ce livre
await cache.delete(`/api/komga/books/${book.id}/pages`); await cache.delete(`/api/komga/images/books/${book.id}/pages`);
for (let i = 1; i <= book.media.pagesCount; i++) { for (let i = 1; i <= book.media.pagesCount; i++) {
await cache.delete(`/api/komga/books/${book.id}/pages/${i}`); await cache.delete(`/api/komga/images/books/${book.id}/pages/${i}`);
} }
setIsAvailableOffline(false); setIsAvailableOffline(false);
setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() }); setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() });
@@ -107,12 +134,15 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) {
} }
} catch (error) { } catch (error) {
console.error("Erreur lors du téléchargement:", 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() }); setBookStatus(book.id, { status: "error", progress: 0, timestamp: Date.now() });
toast({ toast({
title: "Erreur", title: "Erreur",
description: "Une erreur est survenue lors du téléchargement", description: "Une erreur est survenue lors du téléchargement",
variant: "destructive", variant: "destructive",
}); });
}
} finally { } finally {
setIsLoading(false); setIsLoading(false);
setDownloadProgress(0); setDownloadProgress(0);
@@ -152,7 +182,7 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) {
try { try {
const cache = await caches.open("stripstream-books"); const cache = await caches.open("stripstream-books");
// On vérifie que toutes les pages sont dans le cache // On vérifie que toutes les pages sont dans le cache
const bookPages = await cache.match(`/api/komga/books/${book.id}/pages`); const bookPages = await cache.match(`/api/komga/images/books/${book.id}/pages`);
if (!bookPages) { if (!bookPages) {
setIsAvailableOffline(false); setIsAvailableOffline(false);
setBookStatus(book.id, { status: "idle", progress: 0, timestamp: Date.now() }); setBookStatus(book.id, { status: "idle", progress: 0, timestamp: Date.now() });
@@ -162,7 +192,7 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) {
// Vérifie que toutes les pages sont dans le cache // Vérifie que toutes les pages sont dans le cache
let allPagesAvailable = true; let allPagesAvailable = true;
for (let i = 1; i <= book.media.pagesCount; i++) { for (let i = 1; i <= book.media.pagesCount; i++) {
const page = await cache.match(`/api/komga/books/${book.id}/pages/${i}`); const page = await cache.match(`/api/komga/images/books/${book.id}/pages/${i}`);
if (!page) { if (!page) {
allPagesAvailable = false; allPagesAvailable = false;
break; break;
@@ -200,9 +230,9 @@ export function BookOfflineButton({ book, className }: BookOfflineButtonProps) {
if (isAvailableOffline) { if (isAvailableOffline) {
setBookStatus(book.id, { status: "idle", progress: 0, timestamp: Date.now() }); setBookStatus(book.id, { status: "idle", progress: 0, timestamp: Date.now() });
// Supprime le livre du cache // Supprime le livre du cache
await cache.delete(`/api/komga/books/${book.id}/pages`); await cache.delete(`/api/komga/images/books/${book.id}/pages`);
for (let i = 1; i <= book.media.pagesCount; i++) { for (let i = 1; i <= book.media.pagesCount; i++) {
await cache.delete(`/api/komga/books/${book.id}/pages/${i}`); await cache.delete(`/api/komga/images/books/${book.id}/pages/${i}`);
const progress = (i / book.media.pagesCount) * 100; const progress = (i / book.media.pagesCount) * 100;
setDownloadProgress(progress); setDownloadProgress(progress);
} }