feat(api): generate API models and hooks for public shop configuration and commerce entities
- Added generated API hooks and models for public shop configuration, including listing and retrieving configurations. - Introduced models for commerce categories, discount codes, orders, product images, and products with pagination and search parameters. - Ensured all generated files are structured for easy integration with React Query.
This commit is contained in:
235
backend/thirdparty/downloader/views.py
vendored
235
backend/thirdparty/downloader/views.py
vendored
@@ -5,6 +5,7 @@ import yt_dlp
|
||||
import tempfile
|
||||
import os
|
||||
import shutil
|
||||
import mimetypes
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.views import APIView
|
||||
@@ -21,28 +22,17 @@ from django.utils import timezone
|
||||
from django.db.models.functions import TruncDay, TruncHour
|
||||
from .models import DownloaderRecord
|
||||
|
||||
# Allowed container formats for output/remux
|
||||
FORMAT_CHOICES = ("mp4", "mkv", "webm", "flv", "mov", "avi", "ogg")
|
||||
# Common container formats - user can provide any extension supported by ffmpeg
|
||||
FORMAT_HELP = (
|
||||
"Choose container format: "
|
||||
"Container format for the output file. Common formats: "
|
||||
"mp4 (H.264 + AAC, most compatible), "
|
||||
"mkv (flexible, lossless container), "
|
||||
"webm (VP9/AV1 + Opus), "
|
||||
"flv (legacy), mov (Apple-friendly), "
|
||||
"avi (older), ogg (mostly obsolete)."
|
||||
"avi (older), ogg, m4a (audio only), mp3 (audio only). "
|
||||
"The extension will be validated by ffmpeg during conversion."
|
||||
)
|
||||
|
||||
# Minimal mime map by extension
|
||||
MIME_BY_EXT = {
|
||||
"mp4": "video/mp4",
|
||||
"mkv": "video/x-matroska",
|
||||
"webm": "video/webm",
|
||||
"flv": "video/x-flv",
|
||||
"mov": "video/quicktime",
|
||||
"avi": "video/x-msvideo",
|
||||
"ogg": "video/ogg",
|
||||
}
|
||||
|
||||
class Downloader(APIView):
|
||||
permission_classes = [AllowAny]
|
||||
authentication_classes = []
|
||||
@@ -154,28 +144,92 @@ class Downloader(APIView):
|
||||
@extend_schema(
|
||||
tags=["downloader", "public"],
|
||||
summary="Download video from URL",
|
||||
description="""
|
||||
Download video with optional quality constraints and container format conversion.
|
||||
|
||||
**Quality Parameters (optional):**
|
||||
- If not specified, yt-dlp will automatically select the best available quality.
|
||||
- `video_quality`: Maximum video height in pixels (e.g., 1080, 720, 480).
|
||||
- `audio_quality`: Maximum audio bitrate in kbps (e.g., 320, 192, 128).
|
||||
|
||||
**Format/Extension:**
|
||||
- Any format supported by ffmpeg (mp4, mkv, webm, avi, mov, flv, m4a, mp3, etc.).
|
||||
- Defaults to 'mp4' if not specified.
|
||||
- The conversion is handled automatically by ffmpeg in the background.
|
||||
|
||||
**Advanced Options:**
|
||||
- `subtitles`: Download subtitles (language codes like 'en,cs' or 'all')
|
||||
- `embed_subtitles`: Embed subtitles into video file
|
||||
- `embed_thumbnail`: Embed thumbnail as cover art
|
||||
- `extract_audio`: Extract audio only (ignores video quality)
|
||||
- `start_time`: Trim start (format: HH:MM:SS or seconds)
|
||||
- `end_time`: Trim end (format: HH:MM:SS or seconds)
|
||||
- `playlist_items`: Download specific playlist items (e.g., '1-5,8,10')
|
||||
- `cookies`: Browser cookies for age-restricted content (Netscape format)
|
||||
""",
|
||||
request=inline_serializer(
|
||||
name="DownloadRequest",
|
||||
fields={
|
||||
"url": serializers.URLField(help_text="Video URL to download"),
|
||||
"ext": serializers.ChoiceField(
|
||||
choices=FORMAT_CHOICES,
|
||||
"url": serializers.URLField(help_text="Video URL to download from supported platforms"),
|
||||
"ext": serializers.CharField(
|
||||
required=False,
|
||||
default="mp4",
|
||||
help_text=FORMAT_HELP,
|
||||
),
|
||||
"format": serializers.ChoiceField(
|
||||
choices=FORMAT_CHOICES,
|
||||
required=False,
|
||||
help_text="Alias of 'ext' (deprecated)."
|
||||
),
|
||||
"video_quality": serializers.IntegerField(
|
||||
required=True,
|
||||
help_text="Target max video height (e.g. 1080)."
|
||||
required=False,
|
||||
allow_null=True,
|
||||
help_text="Optional: Target max video height in pixels (e.g. 1080, 720). If omitted, best quality is selected."
|
||||
),
|
||||
"audio_quality": serializers.IntegerField(
|
||||
required=True,
|
||||
help_text="Target max audio bitrate in kbps (e.g. 160)."
|
||||
required=False,
|
||||
allow_null=True,
|
||||
help_text="Optional: Target max audio bitrate in kbps (e.g. 320, 192, 128). If omitted, best quality is selected."
|
||||
),
|
||||
"subtitles": serializers.CharField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
help_text="Language codes (e.g., 'en', 'cs', 'en,cs') or 'all' for all available subtitles"
|
||||
),
|
||||
"embed_subtitles": serializers.BooleanField(
|
||||
required=False,
|
||||
default=False,
|
||||
help_text="Embed subtitles into the video file (requires mkv or mp4 container)"
|
||||
),
|
||||
"embed_thumbnail": serializers.BooleanField(
|
||||
required=False,
|
||||
default=False,
|
||||
help_text="Embed thumbnail as cover art in the file"
|
||||
),
|
||||
"extract_audio": serializers.BooleanField(
|
||||
required=False,
|
||||
default=False,
|
||||
help_text="Extract audio only, ignoring video quality settings"
|
||||
),
|
||||
"start_time": serializers.CharField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
help_text="Start time for trimming (format: HH:MM:SS or seconds as integer)"
|
||||
),
|
||||
"end_time": serializers.CharField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
help_text="End time for trimming (format: HH:MM:SS or seconds as integer)"
|
||||
),
|
||||
"playlist_items": serializers.CharField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
help_text="Playlist items to download (e.g., '1-5,8,10' or '1,2,3')"
|
||||
),
|
||||
"cookies": serializers.CharField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
help_text="Browser cookies in Netscape format for age-restricted content. Export from browser extensions like 'Get cookies.txt'"
|
||||
),
|
||||
},
|
||||
),
|
||||
@@ -185,38 +239,65 @@ class Downloader(APIView):
|
||||
name="DownloadErrorResponse",
|
||||
fields={
|
||||
"error": serializers.CharField(),
|
||||
"allowed": serializers.ListField(child=serializers.CharField(), required=False),
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
def post(self, request):
|
||||
url = request.data.get("url")
|
||||
# Accept ext or legacy format param
|
||||
ext = (request.data.get("ext") or request.data.get("format") or "mp4").lower()
|
||||
# Accept ext parameter, default to mp4
|
||||
ext = request.data.get("ext", "mp4")
|
||||
|
||||
try:
|
||||
video_quality = int(request.data.get("video_quality")) # height, e.g., 1080
|
||||
audio_quality = int(request.data.get("audio_quality")) # abr kbps, e.g., 160
|
||||
except Exception:
|
||||
return Response({"error": "Invalid quality parameters, not integers!"}, status=400)
|
||||
# Optional quality parameters - only parse if provided
|
||||
video_quality = None
|
||||
audio_quality = None
|
||||
|
||||
if request.data.get("video_quality"):
|
||||
try:
|
||||
video_quality = int(request.data.get("video_quality"))
|
||||
except (ValueError, TypeError):
|
||||
return Response({"error": "Invalid video_quality parameter, must be an integer!"}, status=400)
|
||||
|
||||
if request.data.get("audio_quality"):
|
||||
try:
|
||||
audio_quality = int(request.data.get("audio_quality"))
|
||||
except (ValueError, TypeError):
|
||||
return Response({"error": "Invalid audio_quality parameter, must be an integer!"}, status=400)
|
||||
|
||||
# Advanced options
|
||||
subtitles = request.data.get("subtitles")
|
||||
embed_subtitles = request.data.get("embed_subtitles", False)
|
||||
embed_thumbnail = request.data.get("embed_thumbnail", False)
|
||||
extract_audio = request.data.get("extract_audio", False)
|
||||
start_time = request.data.get("start_time")
|
||||
end_time = request.data.get("end_time")
|
||||
playlist_items = request.data.get("playlist_items")
|
||||
cookies = request.data.get("cookies")
|
||||
|
||||
if not url:
|
||||
return Response({"error": "URL is required"}, status=400)
|
||||
if ext not in FORMAT_CHOICES:
|
||||
return Response({"error": f"Unsupported extension '{ext}'", "allowed": FORMAT_CHOICES}, status=400)
|
||||
if not ext or not isinstance(ext, str):
|
||||
return Response({"error": "Extension must be a valid string"}, status=400)
|
||||
|
||||
# Ensure base tmp dir exists
|
||||
os.makedirs(settings.DOWNLOADER_TMP_DIR, exist_ok=True)
|
||||
tmpdir = tempfile.mkdtemp(prefix="downloader_", dir=settings.DOWNLOADER_TMP_DIR)
|
||||
outtmpl = os.path.join(tmpdir, "download.%(ext)s")
|
||||
|
||||
# Build a format selector using requested quality caps
|
||||
# Example: "bv[height<=1080]+ba[abr<=160]/b"
|
||||
video_part = f"bv[height<={video_quality}]" if video_quality else "bv*"
|
||||
audio_part = f"ba[abr<={audio_quality}]" if audio_quality else "ba"
|
||||
|
||||
format_selector = f"{video_part}+{audio_part}/b"
|
||||
# Build a format selector using optional quality caps
|
||||
# If quality params are not provided, use best quality
|
||||
if video_quality is not None and audio_quality is not None:
|
||||
# Both specified: "bv[height<=1080]+ba[abr<=160]/b"
|
||||
format_selector = f"bv[height<={video_quality}]+ba[abr<={audio_quality}]/b"
|
||||
elif video_quality is not None:
|
||||
# Only video quality specified: "bv[height<=1080]+ba/b"
|
||||
format_selector = f"bv[height<={video_quality}]+ba/b"
|
||||
elif audio_quality is not None:
|
||||
# Only audio quality specified: "bv+ba[abr<=160]/b"
|
||||
format_selector = f"bv+ba[abr<={audio_quality}]/b"
|
||||
else:
|
||||
# No quality constraints, let yt-dlp choose best
|
||||
format_selector = "b/bv+ba"
|
||||
|
||||
|
||||
ydl_options = {
|
||||
@@ -226,11 +307,69 @@ class Downloader(APIView):
|
||||
"quiet": True,
|
||||
"max_filesize": settings.DOWNLOADER_MAX_SIZE_BYTES,
|
||||
"socket_timeout": settings.DOWNLOADER_TIMEOUT,
|
||||
# remux to container without re-encoding where possible
|
||||
"postprocessors": [
|
||||
{"key": "FFmpegVideoRemuxer", "preferedformat": ext}
|
||||
],
|
||||
"postprocessors": [],
|
||||
}
|
||||
|
||||
# Handle cookies for age-restricted content
|
||||
if cookies:
|
||||
# Save cookies to a temporary file
|
||||
cookie_file = os.path.join(tmpdir, "cookies.txt")
|
||||
try:
|
||||
with open(cookie_file, "w") as f:
|
||||
f.write(cookies)
|
||||
ydl_options["cookiefile"] = cookie_file
|
||||
except Exception as e:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
return Response({"error": f"Invalid cookies format: {str(e)}"}, status=400)
|
||||
|
||||
# Subtitles
|
||||
if subtitles:
|
||||
if subtitles.lower() == "all":
|
||||
ydl_options["writesubtitles"] = True
|
||||
ydl_options["writeautomaticsub"] = True
|
||||
ydl_options["subtitleslangs"] = ["all"]
|
||||
else:
|
||||
ydl_options["writesubtitles"] = True
|
||||
ydl_options["subtitleslangs"] = [lang.strip() for lang in subtitles.split(",")]
|
||||
|
||||
# Embed subtitles (only for mkv/mp4)
|
||||
if embed_subtitles and subtitles:
|
||||
if ext in ["mkv", "mp4"]:
|
||||
ydl_options["postprocessors"].append({"key": "FFmpegEmbedSubtitle"})
|
||||
else:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
return Response({"error": "Subtitle embedding requires mkv or mp4 format"}, status=400)
|
||||
|
||||
# Embed thumbnail
|
||||
if embed_thumbnail:
|
||||
ydl_options["writethumbnail"] = True
|
||||
ydl_options["postprocessors"].append({"key": "EmbedThumbnail"})
|
||||
|
||||
# Extract audio only
|
||||
if extract_audio:
|
||||
ydl_options["postprocessors"].append({
|
||||
"key": "FFmpegExtractAudio",
|
||||
"preferredcodec": ext if ext in ["mp3", "m4a", "opus", "vorbis", "wav"] else "mp3",
|
||||
})
|
||||
|
||||
# Download sections (trim)
|
||||
if start_time or end_time:
|
||||
download_ranges = {}
|
||||
if start_time:
|
||||
download_ranges["start_time"] = start_time
|
||||
if end_time:
|
||||
download_ranges["end_time"] = end_time
|
||||
ydl_options["download_ranges"] = lambda info_dict, ydl: [download_ranges]
|
||||
|
||||
# Playlist items
|
||||
if playlist_items:
|
||||
ydl_options["playlist_items"] = playlist_items
|
||||
|
||||
# Add remux postprocessor if not extracting audio
|
||||
if not extract_audio:
|
||||
ydl_options["postprocessors"].append(
|
||||
{"key": "FFmpegVideoRemuxer", "preferedformat": ext}
|
||||
)
|
||||
|
||||
file_path = ""
|
||||
try:
|
||||
@@ -268,7 +407,7 @@ class Downloader(APIView):
|
||||
|
||||
safe_title = slugify(info.get("title") or "video")
|
||||
filename = f"{safe_title}.{ext}"
|
||||
content_type = MIME_BY_EXT.get(ext, "application/octet-stream")
|
||||
content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
streaming_content=stream_and_cleanup(file_path, tmpdir),
|
||||
|
||||
Reference in New Issue
Block a user