# ---------------------- Inline serializers for documentation only ---------------------- # Using inline_serializer to avoid creating new files. import yt_dlp import tempfile import os import shutil from rest_framework import serializers from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated, AllowAny from drf_spectacular.utils import extend_schema, inline_serializer from drf_spectacular.types import OpenApiTypes from django.conf import settings from django.http import StreamingHttpResponse from django.utils.text import slugify # NEW: aggregations and timeseries helpers from django.db import models 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") FORMAT_HELP = ( "Choose container format: " "mp4 (H.264 + AAC, most compatible), " "mkv (flexible, lossless container), " "webm (VP9/AV1 + Opus), " "flv (legacy), mov (Apple-friendly), " "avi (older), ogg (mostly obsolete)." ) # 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 = [] @extend_schema( tags=["downloader"], summary="Get video info from URL", parameters=[ inline_serializer( name="VideoInfoParams", fields={ "url": serializers.URLField(help_text="Video URL to analyze"), }, ) ], responses={ 200: inline_serializer( name="VideoInfoResponse", fields={ "title": serializers.CharField(), "duration": serializers.IntegerField(allow_null=True), "thumbnail": serializers.URLField(allow_null=True), "video_resolutions": serializers.ListField(child=serializers.CharField()), "audio_resolutions": serializers.ListField(child=serializers.CharField()), }, ), 400: inline_serializer( name="ErrorResponse", fields={"error": serializers.CharField()}, ), }, ) def get(self, request): url = request.data.get("url") or request.query_params.get("url") if not url: return Response({"error": "URL is required"}, status=400) ydl_options = { "quiet": True, } try: with yt_dlp.YoutubeDL(ydl_options) as ydl: info = ydl.extract_info(url, download=False) except Exception: return Response({"error": "Failed to retrieve video info"}, status=400) formats = info.get("formats", []) or [] # Video: collect unique heights and sort desc heights = { int(f.get("height")) for f in formats if f.get("vcodec") != "none" and isinstance(f.get("height"), int) } video_resolutions = [f"{h}p" for h in sorted(heights, reverse=True)] # Audio: collect unique bitrates (abr kbps), fallback to tbr when abr missing bitrates = set() for f in formats: if f.get("acodec") != "none" and f.get("vcodec") == "none": abr = f.get("abr") tbr = f.get("tbr") val = None if isinstance(abr, (int, float)): val = int(abr) elif isinstance(tbr, (int, float)): val = int(tbr) if val and val > 0: bitrates.add(val) audio_resolutions = [f"{b}kbps" for b in sorted(bitrates, reverse=True)] return Response( { "title": info.get("title"), "duration": info.get("duration"), "thumbnail": info.get("thumbnail"), "video_resolutions": video_resolutions, "audio_resolutions": audio_resolutions, }, status=200, ) @extend_schema( tags=["downloader"], summary="Download video from URL", request=inline_serializer( name="DownloadRequest", fields={ "url": serializers.URLField(help_text="Video URL to download"), "ext": serializers.ChoiceField( choices=FORMAT_CHOICES, 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)." ), "audio_quality": serializers.IntegerField( required=True, help_text="Target max audio bitrate in kbps (e.g. 160)." ), }, ), responses={ 200: OpenApiTypes.BINARY, 400: inline_serializer( 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() 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) 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) # 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" ydl_options = { "format": format_selector, # select by requested quality "merge_output_format": ext, # container "outtmpl": outtmpl, # temp dir "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} ], } file_path = "" try: with yt_dlp.YoutubeDL(ydl_options) as ydl: info = ydl.extract_info(url, download=True) base = ydl.prepare_filename(info) file_path = base if base.endswith(f".{ext}") else os.path.splitext(base)[0] + f".{ext}" # Stats before streaming duration = int((info or {}).get("duration") or 0) size = os.path.getsize(file_path) if os.path.exists(file_path) else 0 DownloaderRecord.objects.create( url=url, format=ext, length_of_media=duration, file_size=size, ) # Streaming generator that deletes file & temp dir after send (or on abort) def stream_and_cleanup(path: str, temp_dir: str, chunk_size: int = 8192): try: with open(path, "rb") as f: while True: chunk = f.read(chunk_size) if not chunk: break yield chunk finally: try: if os.path.exists(path): os.remove(path) finally: shutil.rmtree(temp_dir, ignore_errors=True) safe_title = slugify(info.get("title") or "video") filename = f"{safe_title}.{ext}" content_type = MIME_BY_EXT.get(ext, "application/octet-stream") response = StreamingHttpResponse( streaming_content=stream_and_cleanup(file_path, tmpdir), content_type=content_type, ) if size: response["Content-Length"] = str(size) response["Content-Disposition"] = f'attachment; filename="{filename}"' return response except Exception as e: shutil.rmtree(tmpdir, ignore_errors=True) return Response({"error": str(e)}, status=400) # ---------------- STATS FOR GRAPHS ---------------- from .serializers import DownloaderStatsSerializer from django.db.models import Count, Avg, Sum class DownloaderStats(APIView): """ Vrací agregované statistiky z tabulky DownloaderRecord. """ authentication_classes = [] permission_classes = [AllowAny] @extend_schema( tags=["downloader"], summary="Get aggregated downloader statistics", responses={200: DownloaderStatsSerializer}, ) def get(self, request): # agregace číselných polí agg = DownloaderRecord.objects.aggregate( total_downloads=Count("id"), avg_length_of_media=Avg("length_of_media"), avg_file_size=Avg("file_size"), total_length_of_media=Sum("length_of_media"), total_file_size=Sum("file_size"), ) # zjištění nejčastějšího formátu most_common = ( DownloaderRecord.objects.values("format") .annotate(count=Count("id")) .order_by("-count") .first() ) agg["most_common_format"] = most_common["format"] if most_common else None serializer = DownloaderStatsSerializer(agg) return Response(serializer.data)