Fix hidden library items not rendering in the browser
Items flagged hidden_from_guests (blacklisted tags) returned 404 for <img> requests since tags cannot send the auth header. The API now exposes signed raw_url/thumbnail_url fields (mirroring upload previews and avatars), and the SPA uses them in the gallery, detail view, duplicates and delete screens, and upload visual matches.
This commit is contained in:
@@ -5,7 +5,12 @@ from django.core import signing
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import MediaItem, MediaLocation, TempUpload, DownloadTask
|
||||
from .services import MEDIA_FILE_SALT, UPLOAD_FILE_SALT, VIDEO_EXTENSIONS
|
||||
from .services import (
|
||||
MEDIA_FILE_SALT,
|
||||
UPLOAD_FILE_SALT,
|
||||
VIDEO_EXTENSIONS,
|
||||
signed_media_url,
|
||||
)
|
||||
|
||||
|
||||
class MediaLocationSerializer(serializers.ModelSerializer):
|
||||
@@ -22,6 +27,8 @@ class MediaItemSerializer(serializers.ModelSerializer):
|
||||
locations = MediaLocationSerializer(many=True, read_only=True)
|
||||
j_id = serializers.SerializerMethodField()
|
||||
display_rating = serializers.SerializerMethodField()
|
||||
raw_url = serializers.SerializerMethodField()
|
||||
thumbnail_url = serializers.SerializerMethodField()
|
||||
filename = serializers.SerializerMethodField()
|
||||
extension = serializers.SerializerMethodField()
|
||||
kind = serializers.SerializerMethodField()
|
||||
@@ -42,6 +49,8 @@ class MediaItemSerializer(serializers.ModelSerializer):
|
||||
"filename",
|
||||
"extension",
|
||||
"kind",
|
||||
"raw_url",
|
||||
"thumbnail_url",
|
||||
"uploaded_by",
|
||||
"uploaded_by_id",
|
||||
"e621_post_id",
|
||||
@@ -59,6 +68,8 @@ class MediaItemSerializer(serializers.ModelSerializer):
|
||||
"filename",
|
||||
"extension",
|
||||
"kind",
|
||||
"raw_url",
|
||||
"thumbnail_url",
|
||||
"uploaded_by",
|
||||
"uploaded_by_id",
|
||||
"e621_post_id",
|
||||
@@ -68,9 +79,22 @@ class MediaItemSerializer(serializers.ModelSerializer):
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def _request_user(self):
|
||||
request = self.context.get("request")
|
||||
user = getattr(request, "user", None)
|
||||
if user is None or not getattr(user, "is_authenticated", False):
|
||||
return None
|
||||
return user
|
||||
|
||||
def get_j_id(self, obj):
|
||||
return f"J-{obj.id}"
|
||||
|
||||
def get_raw_url(self, obj):
|
||||
return signed_media_url(obj, self._request_user(), "raw")
|
||||
|
||||
def get_thumbnail_url(self, obj):
|
||||
return signed_media_url(obj, self._request_user(), "thumbnail")
|
||||
|
||||
def get_display_rating(self, obj):
|
||||
if obj.rating:
|
||||
return obj.rating
|
||||
|
||||
@@ -10,6 +10,7 @@ from pathlib import Path
|
||||
|
||||
import imagehash
|
||||
from django.conf import settings
|
||||
from django.core import signing
|
||||
from django.http import FileResponse, Http404, HttpResponse
|
||||
from django.utils.text import get_valid_filename
|
||||
from PIL import Image
|
||||
@@ -72,6 +73,18 @@ def index_file(path, folder):
|
||||
return item, created_item, location, created_location
|
||||
|
||||
|
||||
def signed_media_url(item, user, action="raw"):
|
||||
"""Media URL that <img>/<video> tags can load for a signed-in user."""
|
||||
base = f"/api/files/J-{item.id}/{action}/"
|
||||
if user is None or not getattr(user, "is_authenticated", False):
|
||||
return base
|
||||
signature = signing.dumps(
|
||||
{"item": item.id, "user": user.id, "action": action},
|
||||
salt=MEDIA_FILE_SALT,
|
||||
)
|
||||
return f"{base}?sig={signature}"
|
||||
|
||||
|
||||
def rename_location_to_j_id(item, location):
|
||||
"""Name a freshly indexed copy J-<id>.<ext> inside its own folder."""
|
||||
path = Path(location.path)
|
||||
|
||||
@@ -14,6 +14,7 @@ from rest_framework.views import APIView
|
||||
|
||||
from .models import TAG_CLOUD_CACHE_KEYS, MediaItem, MediaLocation
|
||||
from .permissions import CanUpload
|
||||
from .services import signed_media_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -85,9 +86,10 @@ def display_rating(item):
|
||||
return rating if rating in {"s", "q", "e"} else ""
|
||||
|
||||
|
||||
def item_brief(item):
|
||||
def item_brief(item, request=None):
|
||||
locations = list(item.locations.all())
|
||||
location = locations[0] if locations else None
|
||||
user = getattr(request, "user", None)
|
||||
return {
|
||||
"j_id": f"J-{item.id}",
|
||||
"md5": item.md5,
|
||||
@@ -97,6 +99,7 @@ def item_brief(item):
|
||||
"location_count": len(locations),
|
||||
"uploaded_by": item.uploaded_by.username if item.uploaded_by else None,
|
||||
"e621_post_id": item.e621_post_id,
|
||||
"thumbnail_url": signed_media_url(item, user, "thumbnail"),
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +131,7 @@ class ExactDuplicatesView(APIView):
|
||||
)
|
||||
groups = []
|
||||
for item in items:
|
||||
brief = item_brief(item)
|
||||
brief = item_brief(item, request)
|
||||
brief["locations"] = [
|
||||
{"id": location.id, "rel_path": location.rel_path}
|
||||
for location in item.locations.all()
|
||||
@@ -159,14 +162,14 @@ class VisualMatchesView(APIView):
|
||||
similarity = similarity_between(target, item, algorithms, threshold)
|
||||
if similarity is None:
|
||||
continue
|
||||
brief = item_brief(item)
|
||||
brief = item_brief(item, request)
|
||||
brief["similarity"] = round(similarity * 100, 1)
|
||||
matches.append(brief)
|
||||
matches.sort(key=lambda entry: entry["similarity"], reverse=True)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"target": item_brief(target),
|
||||
"target": item_brief(target, request),
|
||||
"threshold": round(threshold * 100, 1),
|
||||
"algorithms": algorithms,
|
||||
"count": len(matches),
|
||||
@@ -236,7 +239,10 @@ class VisualGroupsView(APIView):
|
||||
"threshold": round(threshold * 100, 1),
|
||||
"algorithms": algorithms,
|
||||
"groups": [
|
||||
{"size": len(members), "members": [item_brief(item) for item in members]}
|
||||
{
|
||||
"size": len(members),
|
||||
"members": [item_brief(item, request) for item in members],
|
||||
}
|
||||
for members in page_groups
|
||||
],
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ from .tools import HASH_FIELDS, hashes_similarity
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def find_library_matches(path, limit=10):
|
||||
def find_library_matches(path, limit=10, user=None):
|
||||
"""Library items visually similar to a staged file."""
|
||||
hashes = services.compute_visual_hashes(path)
|
||||
if not hashes:
|
||||
@@ -56,6 +56,7 @@ def find_library_matches(path, limit=10):
|
||||
"j_id": f"J-{item.id}",
|
||||
"filename": Path(location.rel_path).name if location else item.md5,
|
||||
"similarity": round(similarity * 100, 1),
|
||||
"thumbnail_url": services.signed_media_url(item, user, "thumbnail"),
|
||||
}
|
||||
)
|
||||
matches.sort(key=lambda entry: entry["similarity"], reverse=True)
|
||||
@@ -176,7 +177,7 @@ class TempUploadViewSet(
|
||||
temp.library_item = existing
|
||||
temp.file.delete(save=False)
|
||||
else:
|
||||
matches = find_library_matches(temp.file.path)
|
||||
matches = find_library_matches(temp.file.path, user=request.user)
|
||||
if matches:
|
||||
temp.visual_matches = matches
|
||||
temp.status = TempUpload.STATUS_VISUAL_MATCH
|
||||
|
||||
Reference in New Issue
Block a user