Migration DB (0070 + 0071): - Backup automatique de book_reading_progress avant migration - Crée table series (fusion de series_metadata) avec UUID PK - Ajoute series_id FK à books, external_metadata_links, anilist_series_links, available_downloads, download_detection_results - Supprime les colonnes TEXT legacy et la table series_metadata Backend API + Indexer: - Toutes les queries SQL migrées vers series_id FK + JOIN series - Routes /series/:name → /series/:series_id (UUID) - Nouvel endpoint GET /series/by-name/:name pour lookup par nom - match_title_volumes() factorisé entre prowlarr.rs et download_detection.rs - Fix scheduler.rs: settings → app_settings - OpenAPI mis à jour avec les nouveaux endpoints Frontend: - Routes /libraries/[id]/series/[name] → /series/[seriesId] - Tous les composants (Edit, Delete, MarkRead, Prowlarr, Metadata, ReadingStatus) utilisent seriesId - compressVolumes() pour afficher T1→3 au lieu de T1 T2 T3 - Titre release en entier (plus de truncate) dans available downloads Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
131 lines
6.3 KiB
TypeScript
131 lines
6.3 KiB
TypeScript
import Link from "next/link";
|
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, StatBox } from "@/app/components/ui";
|
|
import { QbittorrentProvider, QbittorrentDownloadButton } from "@/app/components/QbittorrentDownloadButton";
|
|
import type { DownloadDetectionReportDto, DownloadDetectionResultDto } from "@/lib/api";
|
|
import type { TranslateFunction } from "@/lib/i18n/dictionaries";
|
|
import { compressVolumes } from "@/lib/volumeRanges";
|
|
|
|
export function DownloadDetectionReportCard({ report, t }: { report: DownloadDetectionReportDto; t: TranslateFunction }) {
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>{t("jobDetail.downloadDetectionReport")}</CardTitle>
|
|
<CardDescription>{t("jobDetail.seriesAnalyzed", { count: String(report.total_series) })}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
|
<StatBox value={report.found} label={t("jobDetail.downloadFound")} variant="success" />
|
|
<StatBox value={report.not_found} label={t("jobDetail.downloadNotFound")} />
|
|
<StatBox value={report.no_missing} label={t("jobDetail.downloadNoMissing")} variant="primary" />
|
|
<StatBox value={report.no_metadata} label={t("jobDetail.downloadNoMetadata")} />
|
|
<StatBox value={report.errors} label={t("jobDetail.errors")} variant={report.errors > 0 ? "error" : "default"} />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
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, qbConfigured, t }: {
|
|
results: DownloadDetectionResultDto[];
|
|
libraryId: string | null;
|
|
qbConfigured?: boolean;
|
|
t: TranslateFunction;
|
|
}) {
|
|
if (results.length === 0) return null;
|
|
|
|
return (
|
|
<QbittorrentProvider initialConfigured={qbConfigured}>
|
|
<Card className="lg:col-span-2">
|
|
<CardHeader>
|
|
<CardTitle>{t("jobDetail.downloadAvailableReleases")}</CardTitle>
|
|
<CardDescription>{t("jobDetail.downloadAvailableReleasesDesc", { count: String(results.length) })}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4 max-h-[700px] overflow-y-auto">
|
|
{results.map((r) => (
|
|
<div key={r.id} className="rounded-lg border border-success/20 bg-success/5 p-3">
|
|
<div className="flex items-center justify-between gap-2 mb-2">
|
|
{libraryId ? (
|
|
<Link
|
|
href={`/libraries/${libraryId}/series/${r.series_id ?? encodeURIComponent(r.series_name)}`}
|
|
className="font-semibold text-sm text-primary hover:underline truncate"
|
|
>
|
|
{r.series_name}
|
|
</Link>
|
|
) : (
|
|
<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">
|
|
{t("jobDetail.downloadMissingCount", { count: String(r.missing_count) })}
|
|
</span>
|
|
</div>
|
|
{r.available_releases && r.available_releases.length > 0 && (
|
|
<div className="space-y-1.5">
|
|
{r.available_releases.map((release, idx) => (
|
|
<div key={idx} className="flex items-center gap-2 p-2 rounded bg-background/60 border border-border/40">
|
|
<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-1 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} {t("prowlarr.columnSeeders").toLowerCase()}</span>
|
|
)}
|
|
<span className="text-[10px] text-muted-foreground">
|
|
{(release.size / 1024 / 1024).toFixed(0)} MB
|
|
</span>
|
|
<div className="flex flex-wrap items-center gap-1">
|
|
{compressVolumes(release.matched_missing_volumes).map((range) => (
|
|
<span key={range} className="text-[10px] px-1.5 py-0.5 rounded-full bg-success/20 text-success font-medium">
|
|
{range}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{release.download_url && (
|
|
<QbittorrentDownloadButton
|
|
downloadUrl={release.download_url}
|
|
releaseId={`${r.id}-${idx}`}
|
|
libraryId={libraryId ?? undefined}
|
|
seriesName={r.series_name}
|
|
expectedVolumes={release.matched_missing_volumes}
|
|
allVolumes={release.all_volumes}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
</QbittorrentProvider>
|
|
);
|
|
}
|