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
36 lines
1.1 KiB
Python
36 lines
1.1 KiB
Python
from django.contrib.auth.models import AbstractUser
|
|
from django.db import models
|
|
|
|
|
|
class User(AbstractUser):
|
|
"""Project user with optional e621 API credentials and an app role."""
|
|
|
|
ROLE_USER = "user"
|
|
ROLE_UPLOADER = "uploader"
|
|
ROLE_STAFF = "staff"
|
|
ROLE_CHOICES = [
|
|
(ROLE_USER, "User"),
|
|
(ROLE_UPLOADER, "Uploader"),
|
|
(ROLE_STAFF, "Staff"),
|
|
]
|
|
|
|
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default=ROLE_USER)
|
|
avatar = models.ForeignKey(
|
|
"library.MediaItem",
|
|
null=True,
|
|
blank=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name="+",
|
|
)
|
|
e621_username = models.CharField(max_length=100, blank=True, default="")
|
|
e621_api_key = models.CharField(max_length=100, blank=True, default="")
|
|
e621_base_url = models.CharField(max_length=200, default="https://e621.net")
|
|
|
|
@property
|
|
def e621_configured(self):
|
|
return bool(self.e621_username and self.e621_api_key)
|
|
|
|
@property
|
|
def can_upload(self):
|
|
return self.is_superuser or self.role in {self.ROLE_UPLOADER, self.ROLE_STAFF}
|