import { useMutation, useQueryClient } from "@tanstack/react-query"; import { ScanSearch } from "lucide-react"; import { useState } from "react"; import { Link } from "react-router-dom"; import { Button, Spinner } from "@/components/ui"; import { api, errorMessage } from "@/lib/api"; import { cn } from "@/lib/cn"; import { e621ErrorMessage, effectiveCredentials, iqdbSearch, RATING_LABELS, type E621IqdbResult, } from "@/lib/e621"; import type { MediaItem } from "@/lib/types"; import { useE621 } from "@/store/e621"; const ratingPill: Record = { s: "bg-ctp-green text-ctp-crust", q: "bg-ctp-peach text-ctp-crust", e: "bg-ctp-red text-ctp-crust", }; type Notice = { kind: "ok" | "warn" | "error"; text: string } | null; export function IqdbCard({ item, canLink, }: { item: MediaItem; canLink: boolean; }) { const queryClient = useQueryClient(); const credentials = useE621((state) => state.credentials); const [busy, setBusy] = useState(false); const [candidates, setCandidates] = useState(null); const [selected, setSelected] = useState(null); const [notice, setNotice] = useState(null); const isImage = item.kind === "image"; const configured = Boolean(credentials?.configured); async function search() { setBusy(true); setNotice(null); setSelected(null); setCandidates(null); try { const response = await fetch(item.raw_url); if (!response.ok) { throw new Error(`Could not load the file (HTTP ${response.status}).`); } const blob = await response.blob(); const results = await iqdbSearch( effectiveCredentials(credentials), blob, item.filename, ); setCandidates(results); if (results.length === 0) { setNotice({ kind: "warn", text: "No similar posts found on e621." }); } } catch (error) { setNotice({ kind: "error", text: e621ErrorMessage(error) }); } finally { setBusy(false); } } const linkMutation = useMutation({ mutationFn: (postId: number) => api( `/api/files/${item.j_id}/match/`, { method: "POST", json: { post_id: postId } }, ), onSuccess: (data) => { void queryClient.invalidateQueries({ queryKey: ["file", item.j_id] }); void queryClient.invalidateQueries({ queryKey: ["files"] }); void queryClient.invalidateQueries({ queryKey: ["tag-cloud"] }); setSelected(null); setNotice( data.md5_match ? { kind: "ok", text: `Linked to post #${data.e621_post_id} — MD5 matches.`, } : { kind: "warn", text: `Linked to post #${data.e621_post_id}, but its MD5 differs from this file.`, }, ); }, onError: (error) => setNotice({ kind: "error", text: errorMessage(error) }), }); return (

IQDB search

{candidates && candidates.length > 0 ? ( {candidates.length} candidates ) : null}

Reverse-search e621 with this file to find its original post.

{!configured ? (

Configure e621 credentials in Account to use IQDB.

) : !isImage ? (

IQDB works on images only.

) : null} {notice ? (

{notice.text}

) : null} {candidates && candidates.length > 0 ? (
{candidates.map((candidate) => { const exact = candidate.md5 !== null && candidate.md5 === item.md5; return ( ); })}
) : null} {selected ? (

Post #{selected.post_id}

{selected.md5 === item.md5 ? ( MD5 match ) : null}

{selected.rating ? (RATING_LABELS[selected.rating] ?? selected.rating) : "Unknown rating"} {selected.score_total !== null ? ` · ▲ ${selected.score_total}` : ""} {selected.fav_count !== null ? ` · ${selected.fav_count} favs` : ""} {selected.width && selected.height ? ` · ${selected.width}×${selected.height}` : ""}

{selected.tags_preview.length > 0 ? (
{selected.tags_preview.map((tag) => ( {tag} ))}
) : null}
View post

{canLink ? "Linking stores this post's metadata on the item." : "Only the uploader or staff can link posts."}

) : null} {candidates && candidates.length > 0 && !selected ? (

Select a candidate first, then confirm the link.

) : null}
); }