Re-sign visual-match thumbnails on every detail fetch
Match rows stored a signed URL minted when the scan ran, so it aged out (or used the pre-stable signing scheme) and the modal showed broken tiles even after legacy signatures were fixed. Rows now carry item_id/j_id and the detail serializer mints a fresh thumbnail URL per request; matches whose item no longer exists are dropped.
This commit is contained in:
@@ -150,6 +150,7 @@ class TempUploadSerializer(serializers.ModelSerializer):
|
|||||||
iqdb_checked = serializers.SerializerMethodField()
|
iqdb_checked = serializers.SerializerMethodField()
|
||||||
processing = serializers.SerializerMethodField()
|
processing = serializers.SerializerMethodField()
|
||||||
similar_count = serializers.SerializerMethodField()
|
similar_count = serializers.SerializerMethodField()
|
||||||
|
visual_matches = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TempUpload
|
model = TempUpload
|
||||||
@@ -242,6 +243,57 @@ class TempUploadSerializer(serializers.ModelSerializer):
|
|||||||
def get_similar_count(self, obj):
|
def get_similar_count(self, obj):
|
||||||
return len(obj.iqdb_data or []) + len(obj.visual_matches or [])
|
return len(obj.iqdb_data or []) + len(obj.visual_matches or [])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _visual_item_id(entry):
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
return None
|
||||||
|
item_id = entry.get("item_id")
|
||||||
|
if item_id is None:
|
||||||
|
j_id = str(entry.get("j_id") or "")
|
||||||
|
if j_id.upper().startswith("J-"):
|
||||||
|
j_id = j_id[2:]
|
||||||
|
item_id = j_id if j_id.isdigit() else None
|
||||||
|
try:
|
||||||
|
return int(item_id)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_visual_matches(self, obj):
|
||||||
|
"""Rebuild match rows with fresh signed thumbnail URLs.
|
||||||
|
|
||||||
|
Storing the signed URL meant it aged out (or came from an older
|
||||||
|
signing scheme) and the "Already in your library" grid showed broken
|
||||||
|
tiles. The stored rows only carry the item reference now.
|
||||||
|
"""
|
||||||
|
entries = obj.visual_matches or []
|
||||||
|
if not entries:
|
||||||
|
return entries
|
||||||
|
wanted = {}
|
||||||
|
for entry in entries:
|
||||||
|
item_id = self._visual_item_id(entry)
|
||||||
|
if item_id is not None:
|
||||||
|
wanted[item_id] = None
|
||||||
|
items = MediaItem.objects.in_bulk(list(wanted))
|
||||||
|
user = self._request_user()
|
||||||
|
request = self.context.get("request")
|
||||||
|
matches = []
|
||||||
|
for entry in entries:
|
||||||
|
item_id = self._visual_item_id(entry)
|
||||||
|
item = items.get(item_id) if item_id is not None else None
|
||||||
|
if item is None:
|
||||||
|
continue
|
||||||
|
matches.append(
|
||||||
|
{
|
||||||
|
"j_id": f"J-{item.id}",
|
||||||
|
"filename": entry.get("filename") or item.md5,
|
||||||
|
"similarity": entry.get("similarity"),
|
||||||
|
"thumbnail_url": signed_media_url(
|
||||||
|
item, user, "thumbnail", request=request
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
class TempUploadListSerializer(TempUploadSerializer):
|
class TempUploadListSerializer(TempUploadSerializer):
|
||||||
"""Compact staged-upload row for the board and the status polling.
|
"""Compact staged-upload row for the board and the status polling.
|
||||||
|
|||||||
@@ -287,6 +287,37 @@ class StagedUploadWorkflowTests(TestCase):
|
|||||||
self.assertEqual(body["visual_matches"], [])
|
self.assertEqual(body["visual_matches"], [])
|
||||||
self.assertEqual(body["status"], TempUpload.STATUS_PENDING)
|
self.assertEqual(body["status"], TempUpload.STATUS_PENDING)
|
||||||
|
|
||||||
|
def test_detail_resigns_stored_visual_match_urls(self):
|
||||||
|
"""Stored matches carry only the item reference; URLs are re-minted.
|
||||||
|
|
||||||
|
Embedding the signed URL meant it expired (or used an older signing
|
||||||
|
scheme) and the modal showed alt text instead of thumbnails.
|
||||||
|
"""
|
||||||
|
client = self.api_client(self.uploader)
|
||||||
|
self.seed_library_item(client, "seed-resign")
|
||||||
|
item = MediaItem.objects.get()
|
||||||
|
temp = self.make_temp(self.uploader, "resign")
|
||||||
|
TempUpload.objects.filter(pk=temp.pk).update(
|
||||||
|
visual_matches=[
|
||||||
|
{
|
||||||
|
"item_id": item.id,
|
||||||
|
"j_id": f"J-{item.id}",
|
||||||
|
"filename": "seed-resign.png",
|
||||||
|
"similarity": 96.5,
|
||||||
|
"thumbnail_url": "/api/files/J-x/thumbnail/?sig=stale",
|
||||||
|
},
|
||||||
|
{"item_id": 999999, "j_id": "J-999999", "filename": "gone.png"},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
body = client.get(f"/api/uploads/{temp.id}/").json()
|
||||||
|
self.assertEqual(len(body["visual_matches"]), 1)
|
||||||
|
match = body["visual_matches"][0]
|
||||||
|
self.assertEqual(match["j_id"], f"J-{item.id}")
|
||||||
|
self.assertEqual(match["similarity"], 96.5)
|
||||||
|
self.assertNotIn("stale", match["thumbnail_url"])
|
||||||
|
self.assertIn("/thumbnail/", match["thumbnail_url"])
|
||||||
|
self.assertIn(f"v={item.md5}", match["thumbnail_url"])
|
||||||
|
|
||||||
def test_visual_match_phase_rejects_completed_uploads(self):
|
def test_visual_match_phase_rejects_completed_uploads(self):
|
||||||
client = self.api_client(self.uploader)
|
client = self.api_client(self.uploader)
|
||||||
temp = self.make_temp(
|
temp = self.make_temp(
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ def match_hashes(hashes, index, limit=10, user=None, request=None):
|
|||||||
location = item.locations.first()
|
location = item.locations.first()
|
||||||
matches.append(
|
matches.append(
|
||||||
{
|
{
|
||||||
|
"item_id": item.id,
|
||||||
"j_id": f"J-{item.id}",
|
"j_id": f"J-{item.id}",
|
||||||
"filename": Path(location.rel_path).name if location else item.md5,
|
"filename": Path(location.rel_path).name if location else item.md5,
|
||||||
"similarity": round(similarity * 100, 1),
|
"similarity": round(similarity * 100, 1),
|
||||||
|
|||||||
Reference in New Issue
Block a user