feat: gestion des téléchargements qBittorrent avec import automatique

- Nouvelle table `torrent_downloads` pour suivre les téléchargements gérés
- API : endpoint POST /torrent-downloads/notify (webhook optionnel) et GET /torrent-downloads
- Poller background toutes les 30s qui interroge qBittorrent pour détecter
  les torrents terminés — aucune config "run external program" nécessaire
- Import automatique : déplacement des fichiers vers la série cible,
  renommage selon le pattern existant (détection de la largeur des digits),
  support packs multi-volumes, scan job déclenché après import
- Page /downloads dans le backoffice : filtres, auto-refresh, carte par download
- Toggle auto-import intégré dans la card qBittorrent des settings
- Erreurs de détection download affichées dans le détail des jobs
- Volume /downloads monté dans docker-compose

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 14:43:10 +01:00
parent a2de2e1601
commit 4bb142d1dd
21 changed files with 1197 additions and 39 deletions

View File

@@ -24,6 +24,30 @@ export function DownloadDetectionReportCard({ report, t }: { report: DownloadDet
);
}
export function DownloadDetectionErrorsCard({ results, t }: {
results: DownloadDetectionResultDto[];
t: TranslateFunction;
}) {
if (results.length === 0) return null;
return (
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle>{t("jobDetail.downloadErrors")}</CardTitle>
<CardDescription>{t("jobDetail.downloadErrorsDesc", { count: String(results.length) })}</CardDescription>
</CardHeader>
<CardContent className="space-y-2 max-h-80 overflow-y-auto">
{results.map((r) => (
<div key={r.id} className="p-3 bg-destructive/10 rounded-lg border border-destructive/20">
<p className="text-sm font-semibold text-destructive mb-1">{r.series_name}</p>
<p className="text-sm text-destructive/80">{r.error_message ?? "Erreur inconnue"}</p>
</div>
))}
</CardContent>
</Card>
);
}
export function DownloadDetectionResultsCard({ results, libraryId, t }: {
results: DownloadDetectionResultDto[];
libraryId: string | null;