gukgjzkgjhgjh
This commit is contained in:
70
backend/thirdparty/downloader/migrations/0002_alter_downloaderrecord_options_and_more.py
vendored
Normal file
70
backend/thirdparty/downloader/migrations/0002_alter_downloaderrecord_options_and_more.py
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
# Generated by Django 5.2.7 on 2026-04-19 21:51
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('downloader', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='downloaderrecord',
|
||||
options={'ordering': ['-download_time']},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='downloaderrecord',
|
||||
name='error_message',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='downloaderrecord',
|
||||
name='is_audio_only',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='downloaderrecord',
|
||||
name='platform',
|
||||
field=models.CharField(blank=True, default='', help_text='e.g. youtube, tiktok, vimeo', max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='downloaderrecord',
|
||||
name='processing_time',
|
||||
field=models.FloatField(blank=True, help_text='Server-side processing time in seconds', null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='downloaderrecord',
|
||||
name='success',
|
||||
field=models.BooleanField(default=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='downloaderrecord',
|
||||
name='title',
|
||||
field=models.CharField(blank=True, default='', max_length=500),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='downloaderrecord',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='downloads', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='downloaderrecord',
|
||||
name='video_quality',
|
||||
field=models.IntegerField(blank=True, help_text='Video height in pixels (e.g. 1080). Null for audio-only.', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='downloaderrecord',
|
||||
name='file_size',
|
||||
field=models.BigIntegerField(blank=True, help_text='File size in bytes', null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='downloaderrecord',
|
||||
name='length_of_media',
|
||||
field=models.IntegerField(blank=True, help_text='Length of media in seconds', null=True),
|
||||
),
|
||||
]
|
||||
39
backend/thirdparty/downloader/models.py
vendored
39
backend/thirdparty/downloader/models.py
vendored
@@ -2,14 +2,43 @@ from django.db import models
|
||||
from django.conf import settings
|
||||
from vontor_cz.models import SoftDeleteModel
|
||||
|
||||
# 7áznamy pro donwloader, co lidé nejvíc stahujou a v jakém formátu
|
||||
|
||||
class DownloaderRecord(SoftDeleteModel):
|
||||
# --- Source ---
|
||||
url = models.URLField()
|
||||
download_time = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
title = models.CharField(max_length=500, blank=True, default='')
|
||||
platform = models.CharField(max_length=100, blank=True, default='', help_text="e.g. youtube, tiktok, vimeo")
|
||||
|
||||
# --- User (optional — anonymous downloads allowed) ---
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='downloads',
|
||||
)
|
||||
|
||||
# --- Media info ---
|
||||
format = models.CharField(max_length=50)
|
||||
video_quality = models.IntegerField(null=True, blank=True, help_text="Video height in pixels (e.g. 1080). Null for audio-only.")
|
||||
is_audio_only = models.BooleanField(default=False)
|
||||
length_of_media = models.IntegerField(null=True, blank=True, help_text="Length of media in seconds")
|
||||
file_size = models.BigIntegerField(null=True, blank=True, help_text="File size in bytes")
|
||||
|
||||
length_of_media = models.IntegerField(help_text="Length of media in seconds")
|
||||
file_size = models.BigIntegerField(help_text="File size in bytes")
|
||||
# --- Performance ---
|
||||
processing_time = models.FloatField(null=True, blank=True, help_text="Server-side processing time in seconds")
|
||||
|
||||
# --- Status ---
|
||||
success = models.BooleanField(default=True)
|
||||
error_message = models.TextField(blank=True, default='')
|
||||
|
||||
# --- Timing ---
|
||||
download_time = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-download_time']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.platform or 'unknown'} | {self.title or self.url} | {self.download_time:%Y-%m-%d}"
|
||||
|
||||
|
||||
|
||||
105
backend/thirdparty/downloader/serializers.py
vendored
105
backend/thirdparty/downloader/serializers.py
vendored
@@ -1,9 +1,110 @@
|
||||
from rest_framework import serializers
|
||||
from django.db.models import Count, Avg, Sum, Q
|
||||
from django.db.models.functions import TruncDay, TruncHour
|
||||
|
||||
|
||||
class DownloaderRecordSerializer(serializers.Serializer):
|
||||
"""Single download history entry."""
|
||||
id = serializers.IntegerField()
|
||||
url = serializers.URLField()
|
||||
title = serializers.CharField()
|
||||
platform = serializers.CharField()
|
||||
format = serializers.CharField()
|
||||
video_quality = serializers.IntegerField(allow_null=True)
|
||||
is_audio_only = serializers.BooleanField()
|
||||
length_of_media = serializers.IntegerField(allow_null=True)
|
||||
file_size = serializers.IntegerField(allow_null=True)
|
||||
processing_time = serializers.FloatField(allow_null=True)
|
||||
success = serializers.BooleanField()
|
||||
error_message = serializers.CharField()
|
||||
download_time = serializers.DateTimeField()
|
||||
|
||||
|
||||
class PlatformCountSerializer(serializers.Serializer):
|
||||
platform = serializers.CharField()
|
||||
count = serializers.IntegerField()
|
||||
|
||||
|
||||
class QualityCountSerializer(serializers.Serializer):
|
||||
video_quality = serializers.IntegerField(allow_null=True)
|
||||
count = serializers.IntegerField()
|
||||
|
||||
|
||||
class TimeseriesPointSerializer(serializers.Serializer):
|
||||
period = serializers.DateTimeField()
|
||||
count = serializers.IntegerField()
|
||||
|
||||
|
||||
class TopUrlSerializer(serializers.Serializer):
|
||||
url = serializers.URLField()
|
||||
title = serializers.CharField()
|
||||
count = serializers.IntegerField()
|
||||
|
||||
|
||||
class DownloaderStatsSerializer(serializers.Serializer):
|
||||
# Totals
|
||||
total_downloads = serializers.IntegerField()
|
||||
successful_downloads = serializers.IntegerField()
|
||||
failed_downloads = serializers.IntegerField()
|
||||
success_rate = serializers.FloatField(help_text="Percentage 0-100")
|
||||
|
||||
# Media metrics
|
||||
avg_length_of_media = serializers.FloatField(allow_null=True)
|
||||
avg_file_size = serializers.FloatField(allow_null=True)
|
||||
total_length_of_media = serializers.IntegerField(allow_null=True)
|
||||
avg_file_size = serializers.FloatField(allow_null=True)
|
||||
total_file_size = serializers.IntegerField(allow_null=True)
|
||||
most_common_format = serializers.CharField(allow_null=True)
|
||||
avg_processing_time = serializers.FloatField(allow_null=True)
|
||||
|
||||
# Format / quality breakdown
|
||||
most_common_format = serializers.CharField(allow_null=True)
|
||||
audio_only_count = serializers.IntegerField()
|
||||
video_count = serializers.IntegerField()
|
||||
downloads_by_platform = PlatformCountSerializer(many=True)
|
||||
downloads_by_quality = QualityCountSerializer(many=True)
|
||||
|
||||
# Top content
|
||||
most_downloaded_urls = TopUrlSerializer(many=True)
|
||||
|
||||
# Timeseries
|
||||
downloads_per_day = TimeseriesPointSerializer(many=True)
|
||||
downloads_per_hour = TimeseriesPointSerializer(many=True)
|
||||
|
||||
def to_representation(self, qs):
|
||||
agg = qs.aggregate(
|
||||
total_downloads=Count('id'),
|
||||
successful_downloads=Count('id', filter=Q(success=True)),
|
||||
failed_downloads=Count('id', filter=Q(success=False)),
|
||||
avg_length_of_media=Avg('length_of_media'),
|
||||
total_length_of_media=Sum('length_of_media'),
|
||||
avg_file_size=Avg('file_size'),
|
||||
total_file_size=Sum('file_size'),
|
||||
avg_processing_time=Avg('processing_time'),
|
||||
audio_only_count=Count('id', filter=Q(is_audio_only=True)),
|
||||
video_count=Count('id', filter=Q(is_audio_only=False)),
|
||||
)
|
||||
|
||||
total = agg['total_downloads'] or 0
|
||||
agg['success_rate'] = round(agg['successful_downloads'] / total * 100, 2) if total else 0.0
|
||||
|
||||
most_common = qs.values('format').annotate(count=Count('id')).order_by('-count').first()
|
||||
agg['most_common_format'] = most_common['format'] if most_common else None
|
||||
|
||||
agg['downloads_by_platform'] = list(
|
||||
qs.values('platform').annotate(count=Count('id')).order_by('-count')
|
||||
)
|
||||
agg['downloads_by_quality'] = list(
|
||||
qs.values('video_quality').annotate(count=Count('id')).order_by('-count')
|
||||
)
|
||||
agg['most_downloaded_urls'] = list(
|
||||
qs.values('url', 'title').annotate(count=Count('id')).order_by('-count')[:10]
|
||||
)
|
||||
agg['downloads_per_day'] = list(
|
||||
qs.annotate(period=TruncDay('download_time'))
|
||||
.values('period').annotate(count=Count('id')).order_by('period')
|
||||
)
|
||||
agg['downloads_per_hour'] = list(
|
||||
qs.annotate(period=TruncHour('download_time'))
|
||||
.values('period').annotate(count=Count('id')).order_by('period')
|
||||
)
|
||||
|
||||
return super().to_representation(agg)
|
||||
|
||||
66
backend/thirdparty/downloader/views.py
vendored
66
backend/thirdparty/downloader/views.py
vendored
@@ -9,6 +9,7 @@ import mimetypes
|
||||
import base64
|
||||
import urllib.request
|
||||
import zipfile
|
||||
import time
|
||||
import requests
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -450,6 +451,11 @@ class Downloader(APIView):
|
||||
{"key": "FFmpegVideoRemuxer", "preferedformat": ext}
|
||||
)
|
||||
|
||||
_platform = (info.get('extractor') or urlparse(url).netloc).lower().replace('www.', '').split(':')[0]
|
||||
_user = request.user if request.user.is_authenticated else None
|
||||
_title = info.get('title', '')
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
if is_playlist:
|
||||
# Handle playlist - create ZIP file
|
||||
@@ -490,9 +496,16 @@ class Downloader(APIView):
|
||||
|
||||
DownloaderRecord.objects.create(
|
||||
url=url,
|
||||
format="zip",
|
||||
length_of_media=total_duration,
|
||||
file_size=total_size,
|
||||
title=_title,
|
||||
platform=_platform,
|
||||
user=_user,
|
||||
format='zip',
|
||||
is_audio_only=bool(extract_audio),
|
||||
video_quality=video_quality,
|
||||
length_of_media=total_duration or None,
|
||||
file_size=total_size or None,
|
||||
processing_time=round(time.time() - start_time, 3),
|
||||
success=True,
|
||||
)
|
||||
|
||||
# Streaming response for ZIP
|
||||
@@ -535,9 +548,16 @@ class Downloader(APIView):
|
||||
|
||||
DownloaderRecord.objects.create(
|
||||
url=url,
|
||||
title=_title,
|
||||
platform=_platform,
|
||||
user=_user,
|
||||
format=ext,
|
||||
length_of_media=duration,
|
||||
file_size=size,
|
||||
is_audio_only=bool(extract_audio),
|
||||
video_quality=video_quality,
|
||||
length_of_media=duration or None,
|
||||
file_size=size or None,
|
||||
processing_time=round(time.time() - start_time, 3),
|
||||
success=True,
|
||||
)
|
||||
|
||||
# Streaming generator that deletes file & temp dir after send
|
||||
@@ -572,6 +592,18 @@ class Downloader(APIView):
|
||||
|
||||
except Exception as e:
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
DownloaderRecord.objects.create(
|
||||
url=url,
|
||||
title=_title,
|
||||
platform=_platform,
|
||||
user=_user,
|
||||
format=ext if not is_playlist else 'zip',
|
||||
is_audio_only=bool(extract_audio),
|
||||
video_quality=video_quality,
|
||||
processing_time=round(time.time() - start_time, 3),
|
||||
success=False,
|
||||
error_message=str(e),
|
||||
)
|
||||
return Response({"error": str(e)}, status=400)
|
||||
|
||||
|
||||
@@ -581,7 +613,6 @@ class Downloader(APIView):
|
||||
# ---------------- STATS FOR GRAPHS ----------------
|
||||
|
||||
from .serializers import DownloaderStatsSerializer
|
||||
from django.db.models import Count, Avg, Sum
|
||||
|
||||
class DownloaderStats(APIView):
|
||||
"""
|
||||
@@ -589,30 +620,11 @@ class DownloaderStats(APIView):
|
||||
"""
|
||||
authentication_classes = []
|
||||
permission_classes = [AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
tags=["downloader", "public"],
|
||||
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)
|
||||
return Response(DownloaderStatsSerializer(DownloaderRecord.objects.all()).data)
|
||||
Reference in New Issue
Block a user