feat: section disponibles au téléchargement + fix nommage import
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 43s

- Endpoint GET /download-detection/latest-found : résultats "found" du
  dernier job de détection par bibliothèque
- Section dans la page Téléchargements avec les releases disponibles
  groupées par bibliothèque, bouton qBittorrent intégré
- Fix nommage import : exclut les volumes importés de la recherche de
  référence (évite le cercle vicieux vol 8 → ref vol 8 → même nom)
- Fix extraction volumes : gère "Tome.007" (point après préfixe) en
  plus de "Tome 007" dans extract_volumes_from_title
- Fallback disque pour la référence de nommage quand la DB ne matche pas
- Logging détaillé du processus d'import pour debug

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 22:38:31 +01:00
parent 888db484fb
commit 32078c715a
10 changed files with 303 additions and 15 deletions

View File

@@ -2,8 +2,9 @@
import { useState, useEffect, useCallback } from "react";
import { createPortal } from "react-dom";
import { TorrentDownloadDto } from "@/lib/api";
import { Card, CardContent, Button, Icon } from "@/app/components/ui";
import { TorrentDownloadDto, LatestFoundPerLibraryDto } from "@/lib/api";
import { Card, CardContent, CardHeader, CardTitle, Button, Icon } from "@/app/components/ui";
import { QbittorrentProvider, QbittorrentDownloadButton } from "@/app/components/QbittorrentDownloadButton";
import { useTranslation } from "@/lib/i18n/context";
import type { TranslationKey } from "@/lib/i18n/fr";
@@ -62,9 +63,10 @@ function formatEta(seconds: number): string {
interface DownloadsPageProps {
initialDownloads: TorrentDownloadDto[];
initialLatestFound: LatestFoundPerLibraryDto[];
}
export function DownloadsPage({ initialDownloads }: DownloadsPageProps) {
export function DownloadsPage({ initialDownloads, initialLatestFound }: DownloadsPageProps) {
const { t } = useTranslation();
const [downloads, setDownloads] = useState<TorrentDownloadDto[]>(initialDownloads);
const [filter, setFilter] = useState<string>("all");
@@ -154,6 +156,23 @@ export function DownloadsPage({ initialDownloads }: DownloadsPageProps) {
))}
</div>
)}
{/* Available downloads from latest detection */}
{initialLatestFound.length > 0 && (
<QbittorrentProvider>
<div className="mt-10">
<h2 className="text-xl font-bold text-foreground mb-4 flex items-center gap-2">
<Icon name="search" size="lg" />
{t("downloads.availableTitle")}
</h2>
<div className="space-y-6">
{initialLatestFound.map(lib => (
<AvailableLibraryCard key={lib.library_id} lib={lib} />
))}
</div>
</div>
</QbittorrentProvider>
)}
</>
);
}
@@ -314,3 +333,77 @@ function DownloadCard({ dl, onDeleted }: { dl: TorrentDownloadDto; onDeleted: ()
</Card>
);
}
function AvailableLibraryCard({ lib }: { lib: LatestFoundPerLibraryDto }) {
const { t } = useTranslation();
const [collapsed, setCollapsed] = useState(true);
const displayResults = collapsed ? lib.results.slice(0, 5) : lib.results;
return (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base">{lib.library_name}</CardTitle>
<span className="text-xs text-muted-foreground">
{t("downloads.detectedSeries", { count: lib.results.length })} {formatDate(lib.job_date)}
</span>
</div>
</CardHeader>
<CardContent className="space-y-2">
{displayResults.map(r => (
<div key={r.id} className="rounded-lg border border-border/40 bg-background/60 p-3">
<div className="flex items-center justify-between gap-2 mb-1.5">
<span className="font-semibold text-sm text-foreground truncate">{r.series_name}</span>
<span className="text-[10px] px-1.5 py-0.5 rounded-full font-medium whitespace-nowrap bg-warning/20 text-warning shrink-0">
{r.missing_count} {t("downloads.missing")}
</span>
</div>
{r.available_releases && r.available_releases.length > 0 && (
<div className="space-y-1">
{r.available_releases.map((release, idx) => (
<div key={idx} className="flex items-center gap-2 py-1 pl-2 rounded bg-muted/30">
<div className="flex-1 min-w-0">
<p className="text-xs font-mono text-foreground truncate" title={release.title}>{release.title}</p>
<div className="flex items-center gap-3 mt-0.5 flex-wrap">
{release.indexer && <span className="text-[10px] text-muted-foreground">{release.indexer}</span>}
{release.seeders != null && (
<span className="text-[10px] text-success font-medium">{release.seeders} seeders</span>
)}
<span className="text-[10px] text-muted-foreground">{(release.size / 1024 / 1024).toFixed(0)} MB</span>
<div className="flex items-center gap-1">
{release.matched_missing_volumes.map(vol => (
<span key={vol} className="text-[10px] px-1.5 py-0.5 rounded-full bg-success/20 text-success font-medium">T.{vol}</span>
))}
</div>
</div>
</div>
{release.download_url && (
<QbittorrentDownloadButton
downloadUrl={release.download_url}
releaseId={`${r.id}-${idx}`}
libraryId={lib.library_id}
seriesName={r.series_name}
expectedVolumes={release.matched_missing_volumes}
/>
)}
</div>
))}
</div>
)}
</div>
))}
{lib.results.length > 5 && (
<button
type="button"
onClick={() => setCollapsed(c => !c)}
className="text-xs text-primary hover:underline w-full text-center py-1"
>
{collapsed
? t("downloads.showMore", { count: lib.results.length - 5 })
: t("downloads.showLess")}
</button>
)}
</CardContent>
</Card>
);
}