Wait for all uploads, then batch MD5 -> visual -> IQDB with bulk links
Batching must not start while files are still being uploaded, and the MD5 phase must move a whole chunk at once instead of one resolve per file: - the upload queue drains completely first (failed uploads included) before any matching starts; - phase 1 asks e621 for every md5 (75 per posts.json request), builds the md5 -> post map from the response, and sends the matches to the new POST /api/uploads/link-bulk/ action, so a whole 75-file chunk moves into Indexed in a single board update; - link-bulk indexes the staged file directly when the post's MD5 matches (identical bytes), so there is no per-file download round trip; - phase 2 runs local visual similarity for whatever stayed pending, phase 3 the IQDB queue. Verified end to end with real e621 files: one md5 query for the batch, one link-bulk call, both matching files flipped to Indexed together, then the visual and IQDB phases. 23 library tests green (link-bulk, visual phase, deferred visual matching).
This commit is contained in:
@@ -132,13 +132,17 @@ class StagedUploadWorkflowTests(TestCase):
|
||||
)
|
||||
return client
|
||||
|
||||
def make_temp(self, user, label, status=TempUpload.STATUS_PENDING):
|
||||
def make_temp(
|
||||
self, user, label, status=TempUpload.STATUS_PENDING, payload=None
|
||||
):
|
||||
return TempUpload.objects.create(
|
||||
user=user,
|
||||
file=SimpleUploadedFile(f"{label}.png", TINY_PNG, content_type="image/png"),
|
||||
file=SimpleUploadedFile(
|
||||
f"{label}.png", payload or TINY_PNG, content_type="image/png"
|
||||
),
|
||||
original_filename=f"{label}.png",
|
||||
md5=hashlib.md5(label.encode()).hexdigest(),
|
||||
size=len(TINY_PNG),
|
||||
size=len(payload or TINY_PNG),
|
||||
status=status,
|
||||
)
|
||||
|
||||
@@ -261,6 +265,57 @@ class StagedUploadWorkflowTests(TestCase):
|
||||
response = client.post(f"/api/uploads/{temp.id}/visual-match/")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_link_bulk_moves_a_batch_with_one_call(self):
|
||||
client = self.api_client(self.uploader)
|
||||
first = self.make_temp(self.uploader, "bulk-link-1")
|
||||
second = self.make_temp(
|
||||
self.uploader, "bulk-link-2", payload=similar_png((10, 200, 30))
|
||||
)
|
||||
response = jpost(
|
||||
client,
|
||||
"/api/uploads/link-bulk/",
|
||||
{
|
||||
"links": [
|
||||
{
|
||||
"temp_id": str(first.id),
|
||||
"post": {
|
||||
"id": 900001,
|
||||
"rating": "s",
|
||||
"file": {
|
||||
"md5": first.md5,
|
||||
# Identical MD5: the staged file is indexed
|
||||
# without downloading the URL.
|
||||
"url": "https://static1.e621.net/data/fake-1.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"temp_id": str(second.id),
|
||||
"post": {
|
||||
"id": 900002,
|
||||
"rating": "q",
|
||||
"file": {
|
||||
"md5": second.md5,
|
||||
"url": "https://static1.e621.net/data/fake-2.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
body = response.json()
|
||||
self.assertEqual(body["errors"], [])
|
||||
self.assertEqual(len(body["updated"]), 2)
|
||||
for temp, post_id in ((first, 900001), (second, 900002)):
|
||||
temp.refresh_from_db()
|
||||
self.assertEqual(temp.status, TempUpload.STATUS_COMPLETED)
|
||||
self.assertEqual(temp.library_item_id is not None, True)
|
||||
self.assertEqual(temp.e621_post_id, post_id)
|
||||
self.assertEqual(temp.resolution, TempUpload.RESOLUTION_AUTO_MD5)
|
||||
self.assertEqual(temp.library_item.e621_post_id, post_id)
|
||||
self.assertEqual(MediaItem.objects.count(), 2)
|
||||
|
||||
|
||||
class IqdbRecordingTests(TestCase):
|
||||
"""The modal needs to tell "checked, no match" from "never checked"."""
|
||||
|
||||
Reference in New Issue
Block a user