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:
2025-12-22 02:20:43 +01:00
parent abc6207296
commit 1cec6be6d7
49 changed files with 7580 additions and 7224 deletions

View File

@@ -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),