This commit is contained in:
2025-10-29 00:58:37 +01:00
parent 73da41b514
commit dd9d076bd2
33 changed files with 1172 additions and 385 deletions

View File

@@ -9,6 +9,7 @@ from django.http import StreamingHttpResponse, JsonResponse
from django.utils.text import slugify
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.db.utils import OperationalError, ProgrammingError
# docs + schema helpers
from rest_framework import serializers
@@ -26,6 +27,7 @@ import math
import json
import tempfile
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import quote as urlquote
from .models import DownloaderModel
from .serializers import DownloaderLogSerializer
@@ -141,9 +143,30 @@ def _client_meta(request) -> Tuple[Optional[Any], Optional[str], Optional[str]]:
user = getattr(request, "user", None)
return user, ip, ua
# Safe logger: swallow DB errors if table is missing/not migrated yet
def _log_safely(*, info, requested_format, status: str, url: str, user, ip_address: str, user_agent: str, error_message: str | None = None):
try:
DownloaderModel.from_ydl_info(
info=info,
requested_format=requested_format,
status=status,
url=url,
user=user,
ip_address=ip_address,
user_agent=user_agent,
error_message=error_message,
)
except (OperationalError, ProgrammingError):
# migrations not applied or table missing ignore
pass
except Exception:
# never break the request on logging failures
pass
class DownloaderFormatsView(APIView):
"""Probe media URL and return available formats with estimated sizes and limit flags."""
permission_classes = [AllowAny]
authentication_classes = []
@extend_schema(
tags=["downloader"],
@@ -224,7 +247,7 @@ class DownloaderFormatsView(APIView):
except Exception as e:
# log probe error
user, ip, ua = _client_meta(request)
DownloaderModel.from_ydl_info(
_log_safely(
info={"webpage_url": url},
requested_format=None,
status="probe_error",
@@ -258,7 +281,7 @@ class DownloaderFormatsView(APIView):
# Log probe
user, ip, ua = _client_meta(request)
DownloaderModel.from_ydl_info(
_log_safely(
info=info,
requested_format=None,
status="probe_ok",
@@ -280,6 +303,7 @@ class DownloaderFormatsView(APIView):
class DownloaderFileView(APIView):
"""Download selected format if under max size, then stream the file back."""
permission_classes = [AllowAny]
authentication_classes = []
@extend_schema(
tags=["downloader"],
@@ -288,7 +312,7 @@ class DownloaderFileView(APIView):
description="Downloads with a strict max filesize guard and streams as application/octet-stream.",
request=DownloadRequestSchema,
responses={
200: OpenApiResponse(response=OpenApiTypes.BINARY, media_type="application/octet-stream"),
200: OpenApiTypes.BINARY, # was OpenApiResponse(..., media_type="application/octet-stream")
400: OpenApiResponse(response=ErrorResponseSchema),
413: OpenApiResponse(response=ErrorResponseSchema),
500: OpenApiResponse(response=ErrorResponseSchema),
@@ -334,7 +358,7 @@ class DownloaderFileView(APIView):
info = ydl.extract_info(url, download=False)
except Exception as e:
user, ip, ua = _client_meta(request)
DownloaderModel.from_ydl_info(
_log_safely(
info={"webpage_url": url},
requested_format=fmt_id,
status="precheck_error",
@@ -358,7 +382,7 @@ class DownloaderFileView(APIView):
est_size = _estimate_size_bytes(selected, duration)
if est_size is not None and est_size > max_bytes:
user, ip, ua = _client_meta(request)
DownloaderModel.from_ydl_info(
_log_safely(
info=selected,
requested_format=fmt_id,
status="blocked_by_size",
@@ -400,7 +424,7 @@ class DownloaderFileView(APIView):
filepath = result.get("requested_downloads", [{}])[0].get("filepath") or result.get("_filename")
except Exception as e:
user, ip, ua = _client_meta(request)
DownloaderModel.from_ydl_info(
_log_safely(
info=selected,
requested_format=fmt_id,
status="download_error",
@@ -425,7 +449,7 @@ class DownloaderFileView(APIView):
try:
selected_info = dict(selected)
selected_info["filesize"] = os.path.getsize(filepath)
DownloaderModel.from_ydl_info(
_log_safely(
info=selected_info,
requested_format=fmt_id,
status="success",
@@ -451,7 +475,13 @@ class DownloaderFileView(APIView):
pass
resp = StreamingHttpResponse(file_generator(filepath), content_type="application/octet-stream")
resp["Content-Disposition"] = f'attachment; filename="{safe_name}"'
# Include both plain and RFC 5987 encoded filename
resp["Content-Disposition"] = (
f'attachment; filename="{safe_name}"; filename*=UTF-8\'\'{urlquote(safe_name)}'
)
# Expose headers so the browser can read them via XHR/fetch
resp["X-Filename"] = safe_name
resp["Access-Control-Expose-Headers"] = "Content-Disposition, X-Filename, Content-Length, Content-Type"
try:
resp["Content-Length"] = str(os.path.getsize(filepath))
except Exception:
@@ -521,19 +551,3 @@ class DownloaderStatsView(APIView):
"top_acodec": top_acodec,
"audio_vs_video": audio_vs_video,
})
# Minimal placeholder so existing URL doesn't break; prefer using automatic logs above.
class DownloaderLogView(APIView):
permission_classes = [AllowAny]
@extend_schema(
tags=["downloader"],
operation_id="downloader_log_helper",
summary="Deprecated helper",
description="Use /api/downloader/formats/ then /api/downloader/download/.",
responses={200: inline_serializer(name="LogHelper", fields={"detail": serializers.CharField()})},
)
def post(self, request):
"""POST to the deprecated log helper endpoint."""
return Response({"detail": "Use /api/downloader/formats/ then /api/downloader/download/."}, status=200)