import re from pathlib import Path from urllib.parse import urlparse from django.conf import settings from django.core import signing from django.db.models import Min, Q from django.http import Http404, StreamingHttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone from django.utils.text import get_valid_filename from rest_framework import mixins, status, viewsets from rest_framework.decorators import action from rest_framework.permissions import AllowAny, IsAuthenticatedOrReadOnly from rest_framework.response import Response from rest_framework.views import APIView from . import e621, matching, services from .downloads import reap_stale_downloads, start_download_task from .matching import reap_stale_match_tasks, start_match_task from .models import DownloadTask, MatchTask, MediaItem from .permissions import CanUpload, IsUploaderOrStaffOrReadOnly from .serializers import ( DownloadTaskSerializer, MatchTaskSerializer, MediaItemSerializer, ) LIST_ORDERINGS = {"name", "-name", "size", "-size", "created_at", "-created_at"} MD5_RE = re.compile(r"[0-9a-fA-F]{32}") class MediaItemViewSet( mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet, ): serializer_class = MediaItemSerializer permission_classes = [IsAuthenticatedOrReadOnly, IsUploaderOrStaffOrReadOnly] http_method_names = ["get", "post", "patch", "head", "options"] def get_queryset(self): queryset = MediaItem.objects.prefetch_related("locations").annotate( name=Min("locations__rel_path") ) if not self.request.user.is_authenticated: queryset = queryset.filter(hidden_from_guests=False) j_ids = self.request.query_params.get("j_ids", "").strip() if j_ids: numeric_ids = [] for value in j_ids.split(","): text = value.strip() number = text[2:] if text.upper().startswith("J-") else text if number.isdigit(): numeric_ids.append(int(number)) queryset = queryset.filter(pk__in=numeric_ids) search = self.request.query_params.get("search", "").strip() search_type = self.request.query_params.get("search_type", "filename").strip() if search: words = [word for word in search.lower().split() if word] if search_type == "tags": for word in words: queryset = queryset.filter(search_tags__icontains=word) elif search_type == "both": query = Q(locations__rel_path__icontains=search) tag_query = Q() for word in words: tag_query &= Q(search_tags__icontains=word) queryset = queryset.filter(query | tag_query) else: queryset = queryset.filter(locations__rel_path__icontains=search) ratings = [ value for value in self.request.query_params.get("rating", "").split(",") if value in {"s", "q", "e"} ] if ratings: queryset = queryset.filter(rating__in=ratings) statuses = [] for value in self.request.query_params.getlist("status"): statuses.extend( part.strip() for part in value.split(",") if part.strip() ) if statuses: status_query = Q() for status_value in set(statuses): if status_value == "custom": status_query |= Q(has_custom_data=True) elif status_value == "matched": status_query |= Q(e621_match_status=MediaItem.E621_MATCHED) elif status_value == "not_found": status_query |= Q(e621_match_status=MediaItem.E621_NOT_FOUND) elif status_value == "deleted": status_query |= Q(e621_match_status=MediaItem.E621_DELETED) elif status_value == "unknown": status_query |= Q( has_custom_data=False, e621_match_status=MediaItem.E621_UNKNOWN, ) queryset = queryset.filter(status_query) ordering = self.request.query_params.get("ordering", "").strip() queryset = queryset.order_by( ordering if ordering in LIST_ORDERINGS else "-created_at" ) return queryset.distinct() def get_object(self): """Resolve J-, a bare numeric id, or an MD5 fingerprint.""" value = str(self.kwargs.get("pk", "")).strip() queryset = self.get_queryset() if MD5_RE.fullmatch(value): obj = get_object_or_404(queryset, md5=value.lower()) else: numeric = value[2:] if value.upper().startswith("J-") else value if not numeric.isdigit(): raise Http404 obj = get_object_or_404(queryset, pk=int(numeric)) self.check_object_permissions(self.request, obj) return obj def _signed_media_item(self, request, action_name): """Signed media URLs let /