import os import json import random import subprocess import tempfile from datetime import datetime, time from pathlib import Path import requests from django.core.management.base import BaseCommand from django.core.files import File from django.db import transaction from django.utils.text import slugify from apps.video.models import Video class Command(BaseCommand): help = 'Download videos from video_link.json and save them to Video model' # Russian titles related to prophets, Quran, and Hadith RUSSIAN_TITLES = [ "Жизнь Пророка Мухаммада (да благословит его Аллах)", "История пророка Ибрахима", "Коран и его значение в жизни мусульман", "Хадисы Пророка о милосердии", "Пророк Иса в исламской традиции", "Коранические истории о пророках", "Хадисы о важности знаний", "Жизнь и миссия пророка Мусы", "Толкование Корана: суры о вере", "Пророк Нух и его призыв к единобожию", "Хадисы о праведности и благочестии", "Коранические принципы справедливости", "История пророка Юсуфа", "Хадисы о терпении и благодарности", "Пророк Сулейман и его мудрость", "Коран о морали и нравственности", "Жизнь пророка Закарии", "Хадисы о семье и родителях", "Пророк Давуд и его псалмы", "Коранические учения о добре и зле", "Хадисы о щедрости и милосердии", "История пророка Салиха", "Коран о единобожии и вере", "Пророк Худ и его народ", "Хадисы о праведных поступках", "Коранические истории о терпении", "Жизнь пророка Яхьи", "Хадисы о молитве и поклонении", "Пророк Лут и его призыв", "Коран о милости Аллаха", "Хадисы о скромности и смирении", "История пророка Шуайба", "Коранические заповеди о честности", "Пророк Идрис и его вознесение", "Хадисы о братстве в исламе", "Коран о судном дне", "Жизнь пророка Исмаила", "Хадисы о постоянстве в вере", "Пророк Ильяс и его чудеса", "Коранические притчи и их мудрость", "Хадисы о покаянии и прощении", "История пророка Айюба", "Коран о защите слабых и угнетенных", "Пророк Зу-ль-Кифль и его терпение", "Хадисы о правдивости и искренности", "Коранические учения о справедливом суде", "Жизнь пророка Харуна", "Хадисы о стремлении к знаниям", "Пророк Юнус и его покаяние", "Коран о величии Аллаха", "Имам Хусейн и его жертва", "Имам Али и его мудрость", "Имам Хасан и путь мира", "Фатима аз-Захра и ее святость", "Имам Махди и ожидание", "Ашура и значение Кербелы", "Учения Ахль аль-Байт", "Двенадцать имамов в шиизме", "Имам Риза и его наследие", ] def add_arguments(self, parser): parser.add_argument( '--json-file', type=str, default='video_link.json', help='Path to video_link.json file' ) parser.add_argument( '--skip-existing', action='store_true', help='Skip videos that already exist with the same slug' ) parser.add_argument( '--dry-run', action='store_true', help='Show what would be done without actually downloading' ) parser.add_argument( '--limit', type=int, default=None, help='Limit number of videos to process (for testing)' ) def handle(self, *args, **options): json_file = options['json_file'] skip_existing = options['skip_existing'] self.dry_run = options.get('dry_run', False) limit = options.get('limit') if self.dry_run: self.stdout.write(self.style.WARNING('DRY RUN MODE - No actual downloads or saves will be performed')) # Read JSON file if not os.path.isabs(json_file): # If relative path, make it relative to project root from django.conf import settings json_file = os.path.join(settings.BASE_DIR, json_file) if not os.path.exists(json_file): self.stdout.write(self.style.ERROR(f'File not found: {json_file}')) return with open(json_file, 'r', encoding='utf-8') as f: data = json.load(f) videos_list = data.get('videos', []) youtube_links = data.get('youtube_links', []) processed_count = 0 # Process videos with slugs for video_data in videos_list: if limit and processed_count >= limit: self.stdout.write(self.style.WARNING(f'Reached limit of {limit} videos')) break slug = video_data.get('slug') video_url = video_data.get('video') if not video_url: self.stdout.write(self.style.WARNING(f'Skipping {slug}: No video URL')) continue if skip_existing and Video.objects.filter(slug=slug).exists(): self.stdout.write(self.style.WARNING(f'Skipping {slug}: Already exists')) continue self.process_video(video_url, slug) processed_count += 1 # Process youtube_links (direct links without slugs) for idx, video_url in enumerate(youtube_links, start=1): if limit and processed_count >= limit: self.stdout.write(self.style.WARNING(f'Reached limit of {limit} videos')) break # Generate slug from URL filename filename = os.path.basename(video_url) slug = slugify(os.path.splitext(filename)[0])[:50] if skip_existing and Video.objects.filter(slug=slug).exists(): self.stdout.write(self.style.WARNING(f'Skipping {slug}: Already exists')) continue self.process_video(video_url, slug) processed_count += 1 self.stdout.write(self.style.SUCCESS(f'Processed {processed_count} videos successfully!')) def process_video(self, video_url, slug): """Process a single video: download, extract thumbnail, save to database""" self.stdout.write(f'\nProcessing: {slug}') self.stdout.write(f' URL: {video_url}') if self.dry_run: title = random.choice(self.RUSSIAN_TITLES) self.stdout.write(f' [DRY RUN] Would save as: {title}') return temp_dir = None try: # Create temporary directory temp_dir = tempfile.mkdtemp() # Download video video_path = self.download_video(video_url, temp_dir, slug) if not video_path: return # Extract thumbnail thumbnail_path = self.extract_thumbnail(video_path, temp_dir, slug) # Get video duration duration = self.get_video_duration(video_path) # Generate random title title = random.choice(self.RUSSIAN_TITLES) # Ensure unique title by appending counter if needed base_title = title counter = 1 while Video.objects.filter(title=title).exists(): title = f"{base_title} ({counter})" counter += 1 # Ensure unique slug by appending counter if needed final_slug = slug slug_counter = 1 while Video.objects.filter(slug=final_slug).exists(): final_slug = f"{slug}-{slug_counter}" slug_counter += 1 if final_slug != slug: self.stdout.write(self.style.WARNING(f' ⚠ Slug conflict, using: {final_slug}')) # Create Video object with transaction.atomic(): video = Video( title=title, slug=final_slug, video_type=Video.VedioTypeChoices.VIDEO_FILE, video_time=duration, status=True, ) # Save video file with open(video_path, 'rb') as video_file: video.video_file.save( f'{slug}.mp4', File(video_file), save=False ) # Save thumbnail if extracted if thumbnail_path and os.path.exists(thumbnail_path): with open(thumbnail_path, 'rb') as thumb_file: video.thumbnail.save( f'{slug}_thumb.jpg', File(thumb_file), save=False ) video.save() self.stdout.write(self.style.SUCCESS(f'✓ Saved: {title} (slug: {final_slug})')) except Exception as e: self.stdout.write(self.style.ERROR(f'✗ Error processing {slug}: {str(e)}')) finally: # Cleanup temporary files if temp_dir and os.path.exists(temp_dir): import shutil shutil.rmtree(temp_dir, ignore_errors=True) def download_video(self, video_url, temp_dir, slug): """Download video to temporary directory""" try: self.stdout.write(f' Downloading video...') video_path = os.path.join(temp_dir, f'{slug}.mp4') response = requests.get(video_url, stream=True, timeout=300) response.raise_for_status() total_size = int(response.headers.get('content-length', 0)) downloaded = 0 last_percent = 0 with open(video_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded += len(chunk) if total_size > 0: percent = int((downloaded / total_size) * 100) # Print progress every 10% if percent >= last_percent + 10: self.stdout.write(f' Progress: {percent}%') last_percent = percent self.stdout.write(f' ✓ Downloaded: {downloaded / (1024*1024):.2f} MB') return video_path except Exception as e: self.stdout.write(self.style.ERROR(f' ✗ Download failed: {str(e)}')) return None def extract_thumbnail(self, video_path, temp_dir, slug): """Extract thumbnail from video using ffmpeg""" try: self.stdout.write(f' Extracting thumbnail...') thumbnail_path = os.path.join(temp_dir, f'{slug}_thumb.jpg') # Extract frame at 1 second cmd = [ 'ffmpeg', '-i', video_path, '-ss', '00:00:01', '-vframes', '1', '-q:v', '2', thumbnail_path, '-y' ] result = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=60 ) if result.returncode == 0 and os.path.exists(thumbnail_path): self.stdout.write(f' ✓ Thumbnail extracted') return thumbnail_path else: self.stdout.write(self.style.WARNING(f' ⚠ Thumbnail extraction failed')) return None except Exception as e: self.stdout.write(self.style.WARNING(f' ⚠ Thumbnail error: {str(e)}')) return None def get_video_duration(self, video_path): """Get video duration using ffprobe""" try: cmd = [ 'ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', video_path ] result = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30 ) if result.returncode == 0: duration_seconds = float(result.stdout.decode().strip()) hours = int(duration_seconds // 3600) minutes = int((duration_seconds % 3600) // 60) seconds = int(duration_seconds % 60) self.stdout.write(f' ✓ Duration: {hours:02d}:{minutes:02d}:{seconds:02d}') return time(hour=hours, minute=minutes, second=seconds) else: self.stdout.write(self.style.WARNING(f' ⚠ Could not determine duration, using default')) return time(hour=0, minute=0, second=0) except Exception as e: self.stdout.write(self.style.WARNING(f' ⚠ Duration error: {str(e)}, using default')) return time(hour=0, minute=0, second=0)