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
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
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).")
|
||||
)
|
||||
Reference in New Issue
Block a user