Duplicates, delete & storage, users page with J-ID avatars
Backend: - Perceptual hashes (aHash/dHash/pHash/wHash via imagehash, no imgdd) stored on items, computed on upload/download and by the new compute_visual_hashes command - Duplicates API: exact duplicates (multi-location items), visual matches for one item, union-find similarity groups with pagination - Delete API with ownership/staff checks, per-item and per-copy deletion, watched-folder path validation; storage overview and temp cleanup; file list accepts j_ids batches - Staged uploads are flagged visual_match with their library matches (threshold via VISUAL_MATCH_THRESHOLD) - Staff users API: list with upload counts, set role and avatar by J-ID; User.avatar FK with signed avatar URLs - Download threads close their DB connection and stale tasks are reaped, keeping behaviour Gunicorn-friendly Frontend: - /duplicates: exact duplicate groups with per-copy delete, visual similarity controls, search similar to a J-ID, paginated groups with selection, bulk delete and dismiss - /delete: storage cards, delete by J-ID with preview grid, temp cleanup - /users: staff directory with role selects and avatar J-ID inputs - Nav + command palette entries; top-bar avatar; upload cards and the metadata modal show library visual matches
This commit is contained in:
@@ -137,9 +137,11 @@ function TempCard({
|
||||
No preview
|
||||
</div>
|
||||
)}
|
||||
{temp.status === "visual_match" && temp.iqdb_data?.length ? (
|
||||
{temp.status === "visual_match" &&
|
||||
(temp.iqdb_data?.length ?? 0) + (temp.visual_matches?.length ?? 0) > 0 ? (
|
||||
<span className="absolute left-2 top-2 rounded-full bg-ctp-peach/90 px-1.5 py-0.5 font-mono text-[10px] font-medium text-ctp-crust">
|
||||
{temp.iqdb_data.length} similar
|
||||
{(temp.iqdb_data?.length ?? 0) + (temp.visual_matches?.length ?? 0)}{" "}
|
||||
similar
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -282,6 +284,13 @@ function MetadataModal({
|
||||
});
|
||||
}
|
||||
|
||||
function discardUpload() {
|
||||
void api(`/api/uploads/${temp.temp_id}/`, { method: "DELETE" }).then(() => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["uploads"] });
|
||||
onClose();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-[70] flex items-start justify-center overflow-y-auto bg-ctp-crust/70 px-4 py-10 backdrop-blur-sm"
|
||||
@@ -364,6 +373,54 @@ function MetadataModal({
|
||||
|
||||
{tab === "link" ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
{temp.visual_matches && temp.visual_matches.length > 0 ? (
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-ctp-overlay1">
|
||||
Already in your library
|
||||
</p>
|
||||
<div className="mt-2 flex flex-col gap-2">
|
||||
{temp.visual_matches.map((match) => (
|
||||
<div
|
||||
key={match.j_id}
|
||||
className="flex items-center gap-3 rounded-md border border-ctp-surface0 bg-ctp-surface0/40 p-2"
|
||||
>
|
||||
<img
|
||||
src={`/api/files/${match.j_id}/thumbnail/`}
|
||||
alt={match.j_id}
|
||||
loading="lazy"
|
||||
className="h-12 w-12 shrink-0 rounded object-cover"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<Link
|
||||
to={`/detail/${match.j_id}`}
|
||||
className="font-mono text-[11px] text-ctp-blue hover:underline"
|
||||
>
|
||||
{match.j_id}
|
||||
</Link>
|
||||
<span className="block truncate font-mono text-[11px] text-ctp-subtext0" title={match.filename}>
|
||||
{match.filename}
|
||||
</span>
|
||||
</span>
|
||||
<span className="shrink-0 font-mono text-[11px] text-ctp-peach">
|
||||
{match.similarity}%
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-[11px] text-ctp-overlay0">
|
||||
This upload looks like it is already in the library —
|
||||
you can discard it below.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={discardUpload}
|
||||
className="mt-2 rounded-md px-1.5 py-0.5 font-mono text-[10px] text-ctp-red transition hover:bg-ctp-red/15"
|
||||
>
|
||||
discard this upload
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{temp.iqdb_data && temp.iqdb_data.length > 0 ? (
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-ctp-overlay1">
|
||||
|
||||
Reference in New Issue
Block a user