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 8fb273e32e
21 changed files with 1191 additions and 39 deletions

View File

@@ -11,7 +11,7 @@ import { JobTimelineCard } from "./components/JobTimelineCard";
import { JobProgressCard, IndexStatsCard, ThumbnailStatsCard } from "./components/JobProgressCard";
import { MetadataBatchReportCard, MetadataBatchResultsCard, MetadataRefreshReportCard, MetadataRefreshChangesCard } from "./components/MetadataReportCards";
import { ReadingStatusMatchReportCard, ReadingStatusMatchResultsCard, ReadingStatusPushReportCard, ReadingStatusPushResultsCard } from "./components/ReadingStatusReportCards";
import { DownloadDetectionReportCard, DownloadDetectionResultsCard } from "./components/DownloadDetectionCards";
import { DownloadDetectionReportCard, DownloadDetectionResultsCard, DownloadDetectionErrorsCard } from "./components/DownloadDetectionCards";
import { JobErrorsCard } from "./components/JobErrorsCard";
interface JobDetailPageProps {
@@ -148,10 +148,12 @@ export default async function JobDetailPage({ params }: JobDetailPageProps) {
let downloadDetectionReport: DownloadDetectionReportDto | null = null;
let downloadDetectionResults: DownloadDetectionResultDto[] = [];
let downloadDetectionErrors: DownloadDetectionResultDto[] = [];
if (isDownloadDetection) {
[downloadDetectionReport, downloadDetectionResults] = await Promise.all([
[downloadDetectionReport, downloadDetectionResults, downloadDetectionErrors] = await Promise.all([
getDownloadDetectionReport(id).catch(() => null),
getDownloadDetectionResults(id, "found").catch(() => []),
getDownloadDetectionResults(id, "error").catch(() => []),
]);
}
@@ -270,6 +272,7 @@ export default async function JobDetailPage({ params }: JobDetailPageProps) {
{/* Download detection */}
{isDownloadDetection && downloadDetectionReport && <DownloadDetectionReportCard report={downloadDetectionReport} t={t} />}
{isDownloadDetection && <DownloadDetectionErrorsCard results={downloadDetectionErrors} t={t} />}
{isDownloadDetection && <DownloadDetectionResultsCard results={downloadDetectionResults} libraryId={job.library_id} t={t} />}
{/* Metadata batch results */}