from pathlib import Path from django.conf import settings from django.db.models import Min from rest_framework import mixins, status, viewsets from rest_framework.decorators import action from rest_framework.parsers import FormParser, MultiPartParser from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from . import services from .models import MediaItem from .serializers import MediaItemSerializer class MediaItemViewSet( mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet, ): serializer_class = MediaItemSerializer lookup_field = "md5" http_method_names = ["get", "patch", "head", "options"] def get_queryset(self): queryset = MediaItem.objects.prefetch_related("locations").annotate( name=Min("locations__rel_path") ) search = self.request.query_params.get("search", "").strip() if search: 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) ordering = self.request.query_params.get("ordering", "").strip() allowed = {"name", "-name", "size", "-size", "created_at", "-created_at"} queryset = queryset.order_by(ordering if ordering in allowed else "-created_at") return queryset.distinct() @action(detail=True, methods=["get"]) def raw(self, request, md5=None): item = self.get_object() location = item.locations.first() if location is None: return Response( {"detail": "No file for this item."}, status=status.HTTP_404_NOT_FOUND, ) return services.serve_file( request, location.path, download=request.query_params.get("download") == "1" ) @action(detail=True, methods=["get"]) def thumbnail(self, request, md5=None): item = self.get_object() location = item.locations.first() if location is None: return Response( {"detail": "No file for this item."}, status=status.HTTP_404_NOT_FOUND, ) path = Path(location.path) if path.suffix.lower() in services.VIDEO_EXTENSIONS: thumbnail = services.generate_video_thumbnail(item.md5, path) if thumbnail is None: return Response( {"detail": "Thumbnail unavailable."}, status=status.HTTP_404_NOT_FOUND, ) return services.serve_file(request, thumbnail) return services.serve_file(request, path) class UploadView(APIView): permission_classes = [IsAuthenticated] parser_classes = [MultiPartParser, FormParser] def post(self, request): upload = request.FILES.get("file") if upload is None: return Response( {"detail": "A file is required."}, status=status.HTTP_400_BAD_REQUEST ) folder = Path(settings.WATCHED_FOLDER) destination = services.unique_destination(folder, upload.name) with open(destination, "wb") as output: for chunk in upload.chunks(): output.write(chunk) try: item, _, _ = services.index_file(destination, folder) except Exception as exc: # noqa: BLE001 - report indexing failures destination.unlink(missing_ok=True) return Response( {"detail": f"Could not index upload: {exc}"}, status=status.HTTP_400_BAD_REQUEST, ) update_fields = [] rating = request.data.get("rating") or "" if rating in {"s", "q", "e"} and item.rating != rating: item.rating = rating update_fields.append("rating") tags = services.parse_tags(request.data.get("tags")) if tags is not None: item.tags = tags update_fields.append("tags") notes = request.data.get("notes") if notes is not None: item.notes = str(notes) update_fields.append("notes") if update_fields: item.save(update_fields=update_fields + ["updated_at"]) return Response( MediaItemSerializer(item, context={"request": request}).data, status=status.HTTP_201_CREATED, )