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
44 lines
1.4 KiB
Python
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).")
|
|
)
|