Files
J621/backend/apps/library/management/commands/compute_visual_hashes.py
T
JakeBreath cd490b0a23 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
2026-09-17 12:49:10 -05:00

44 lines
1.4 KiB
Python

from django.core.management.base import BaseCommand
from django.db.models import Q
from apps.library.models import MediaItem
from apps.library.services import HASH_FIELDS, ensure_visual_hashes
class Command(BaseCommand):
help = "Compute perceptual hashes for library items missing them."
def add_arguments(self, parser):
parser.add_argument(
"--force",
action="store_true",
help="Recompute hashes even when they already exist.",
)
def handle(self, *args, **options):
queryset = MediaItem.objects.prefetch_related("locations").order_by("id")
if not options["force"]:
missing = Q()
for field in HASH_FIELDS:
missing |= Q(**{field: ""})
queryset = queryset.filter(missing)
total = queryset.count()
if total == 0:
self.stdout.write(self.style.SUCCESS("All items already have hashes."))
return
self.stdout.write(f"Computing hashes for {total} item(s) ...")
done = 0
for item in queryset.iterator(chunk_size=100):
if options["force"]:
item.ahash = item.dhash = item.phash = item.whash = ""
ensure_visual_hashes(item)
done += 1
if done % 100 == 0:
self.stdout.write(f"Processed {done} ...")
self.stdout.write(
self.style.SUCCESS(f"Done. Processed {done} item(s).")
)