Upload pipeline: staging, MD5 auto-match, IQDB, three-column board

Backend:
- TempUpload model: staged files (pending / visual_match / completed /
  error) with resolution, e621 payload, custom metadata and IQDB data
- Files land in a temp folder and only move into the watched library
  folder once resolved; duplicates resolve immediately without a copy
- Endpoints: stage (multipart), list, retrieve, temp file, IQDB save,
  resolve (link to post or custom metadata), discard/dismiss
- cleanup_temp_uploads command for old staged files
- Replaces the old direct-to-library upload endpoint

Frontend:
- Upload page is now a three-column board (Pending & Unmatched /
  Visual Similarity Detected / Auto-uploaded & Indexed)
- After upload: MD5s are batch-checked against e621 and matches
  auto-complete with full post metadata; remaining files run through
  IQDB and move to the similarity column when candidates exist
- Metadata modal with IQDB candidates, post-ID linking and custom
  tags/rating/notes; discard and dismiss actions
- e621 client gains fetchPostsByMd5 and iqdbSearch helpers

Roadmap updated with the completed upload items.
This commit is contained in:
2026-09-17 11:12:03 -05:00
parent 60e1113231
commit d0e2901c92
13 changed files with 1189 additions and 118 deletions
+10 -3
View File
@@ -1,4 +1,4 @@
import type { MediaItem } from "@/lib/types";
import type { TempUpload } from "@/lib/types";
const TOKEN_KEY = "j621.token";
@@ -6,6 +6,13 @@ export function getToken(): string | null {
return window.localStorage.getItem(TOKEN_KEY);
}
export function authHeaders(extra?: HeadersInit): Headers {
const headers = new Headers(extra);
const token = getToken();
if (token) headers.set("Authorization", `Token ${token}`);
return headers;
}
export function setToken(token: string | null): void {
if (token) {
window.localStorage.setItem(TOKEN_KEY, token);
@@ -82,7 +89,7 @@ export function uploadFile(
file: File,
fields: Record<string, string> = {},
onProgress?: (percent: number) => void,
): Promise<MediaItem> {
): Promise<TempUpload> {
return new Promise((resolve, reject) => {
const form = new FormData();
form.append("file", file);
@@ -107,7 +114,7 @@ export function uploadFile(
data = null;
}
if (request.status >= 200 && request.status < 300) {
resolve(data as MediaItem);
resolve(data as TempUpload);
} else {
reject(new ApiError(request.status, data));
}