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
+35 -1
View File
@@ -2,7 +2,7 @@ import os
from rest_framework import serializers
from .models import MediaItem, MediaLocation
from .models import MediaItem, MediaLocation, TempUpload
from .services import VIDEO_EXTENSIONS
@@ -92,3 +92,37 @@ class MediaItemSerializer(serializers.ModelSerializer):
def get_kind(self, obj):
return "video" if "." + self.get_extension(obj) in VIDEO_EXTENSIONS else "image"
class TempUploadSerializer(serializers.ModelSerializer):
temp_id = serializers.UUIDField(source="id", read_only=True)
library_j_id = serializers.SerializerMethodField()
file_url = serializers.SerializerMethodField()
class Meta:
model = TempUpload
fields = [
"temp_id",
"original_filename",
"md5",
"size",
"status",
"resolution",
"e621_post_id",
"e621_data",
"custom_rating",
"custom_tags",
"custom_notes",
"iqdb_data",
"library_j_id",
"file_url",
"created_at",
"updated_at",
]
read_only_fields = fields
def get_library_j_id(self, obj):
return f"J-{obj.library_item_id}" if obj.library_item_id else None
def get_file_url(self, obj):
return f"/api/uploads/{obj.id}/file/" if obj.file else None