feat: refresh metadata ciblé par série après import et dans la modale

- Après import torrent, refresh automatique des métadonnées uniquement
  sur la série importée (via refresh_link) au lieu d'un job complet
- Nouvel endpoint POST /metadata/refresh-link/:id pour rafraîchir un
  seul lien metadata approuvé
- Bouton "Rafraîchir" dans la modale metadata (état linked) avec
  spinner et confirmation visuelle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 23:05:06 +01:00
parent 072d6870fe
commit ca17d02116
7 changed files with 120 additions and 6 deletions

View File

@@ -0,0 +1,13 @@
import { NextRequest, NextResponse } from "next/server";
import { apiFetch } from "@/lib/api";
export async function POST(_request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id } = await params;
const data = await apiFetch(`/metadata/refresh-link/${id}`, { method: "POST" });
return NextResponse.json(data);
} catch (error) {
const message = error instanceof Error ? error.message : "Failed to refresh metadata";
return NextResponse.json({ error: message }, { status: 500 });
}
}

View File

@@ -55,6 +55,8 @@ export function MetadataSearchModal({
const [missing, setMissing] = useState<MissingBooksDto | null>(initialMissing);
const [showMissingList, setShowMissingList] = useState(false);
const [syncReport, setSyncReport] = useState<SyncReport | null>(null);
const [refreshing, setRefreshing] = useState(false);
const [refreshDone, setRefreshDone] = useState(false);
// Provider selector: empty string = library default
const [searchProvider, setSearchProvider] = useState("");
@@ -655,6 +657,37 @@ export function MetadataSearchModal({
>
{t("metadata.searchAgain")}
</button>
<button
type="button"
disabled={refreshing}
onClick={async () => {
if (!linkId) return;
setRefreshing(true);
setRefreshDone(false);
try {
const resp = await fetch(`/api/metadata/refresh-link/${linkId}`, { method: "POST" });
if (resp.ok) {
setRefreshDone(true);
setTimeout(() => setRefreshDone(false), 3000);
}
} finally {
setRefreshing(false);
}
}}
className={`p-2.5 rounded-lg border text-sm font-medium transition-colors ${
refreshDone
? "border-success/30 bg-success/5 text-success"
: "border-primary/30 bg-primary/5 text-primary hover:bg-primary/10"
}`}
>
{refreshing ? (
<Icon name="spinner" size="sm" className="animate-spin" />
) : refreshDone ? (
<Icon name="check" size="sm" />
) : (
t("metadata.refreshLink")
)}
</button>
<button
type="button"
onClick={handleUnlink}