Library search upgrades: tag search, tag cloud, status filter
Backend: - MediaItem gains search_tags (custom + e621 tags, lowercase) and has_custom_data, maintained on save with a data migration backfill - File list search accepts search_type=filename|tags|both (tag search is word-AND across the flattened tag text) and status=matched|custom| unknown filters - New /api/tags/cloud/ endpoint (cached 2 min per guest/auth, invalidated on item changes and deletions) returning the most-used tags, honouring guest visibility Frontend: - Library sidebar: Filename/Tags/Both selector, status pill toggles (persisted), and a clickable tag cloud that runs a tag search - Roadmap updated
This commit is contained in:
@@ -8,11 +8,11 @@ from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Count, Q
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .models import MediaItem, MediaLocation
|
||||
from .models import TAG_CLOUD_CACHE_KEYS, MediaItem, MediaLocation
|
||||
from .permissions import CanUpload
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -299,6 +299,7 @@ class DeleteFilesView(APIView):
|
||||
item.delete()
|
||||
|
||||
cache.delete(STORAGE_CACHE_KEY)
|
||||
cache.delete_many(TAG_CLOUD_CACHE_KEYS)
|
||||
|
||||
for location in MediaLocation.objects.filter(
|
||||
id__in=location_ids
|
||||
@@ -338,6 +339,56 @@ class ClearTempView(APIView):
|
||||
return Response({"removed": removed})
|
||||
|
||||
|
||||
class TagCloudView(APIView):
|
||||
"""Most-used tags across the library (custom + e621 tags)."""
|
||||
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
cache_key = (
|
||||
"j621.library.tag_cloud.auth"
|
||||
if request.user.is_authenticated
|
||||
else "j621.library.tag_cloud.guest"
|
||||
)
|
||||
cached = cache.get(cache_key)
|
||||
if cached is not None:
|
||||
return Response(cached)
|
||||
|
||||
queryset = MediaItem.objects.all()
|
||||
if not request.user.is_authenticated:
|
||||
queryset = queryset.filter(hidden_from_guests=False)
|
||||
|
||||
counts = {}
|
||||
for item in queryset.only("tags", "e621_data").iterator(chunk_size=500):
|
||||
names = set()
|
||||
for tag in item.tags or []:
|
||||
name = str(tag).strip().lower()
|
||||
if name:
|
||||
names.add(name)
|
||||
data = item.e621_data or {}
|
||||
categories = data.get("tags") if isinstance(data, dict) else None
|
||||
if isinstance(categories, dict):
|
||||
for values in categories.values():
|
||||
if isinstance(values, list):
|
||||
names.update(
|
||||
str(tag).strip().lower()
|
||||
for tag in values
|
||||
if str(tag).strip()
|
||||
)
|
||||
for name in names:
|
||||
counts[name] = counts.get(name, 0) + 1
|
||||
|
||||
ranked = sorted(counts.items(), key=lambda entry: (-entry[1], entry[0]))
|
||||
payload = {
|
||||
"count": len(counts),
|
||||
"tags": [
|
||||
{"tag": tag, "count": count} for tag, count in ranked[:120]
|
||||
],
|
||||
}
|
||||
cache.set(cache_key, payload, 120)
|
||||
return Response(payload)
|
||||
|
||||
|
||||
class StorageView(APIView):
|
||||
"""Disk usage for the watched folder, media root and temp uploads."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user