import os import subprocess import tempfile from datetime import time from pathlib import Path from django.core.management.base import BaseCommand from django.core.files import File from django.db import transaction from apps.video.models import Video from apps.podcast.models import Podcast class Command(BaseCommand): help = 'Convert all videos to podcasts by extracting audio and copying metadata' def add_arguments(self, parser): parser.add_argument( '--skip-existing', action='store_true', help='Skip podcasts that already exist with the same slug' ) parser.add_argument( '--dry-run', action='store_true', help='Show what would be done without actually converting' ) parser.add_argument( '--limit', type=int, default=None, help='Limit number of videos to process (for testing)' ) def handle(self, *args, **options): 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 conversions will be performed')) # Get all videos videos = Video.objects.filter(status=True).order_by('id') if limit: videos = videos[:limit] total_videos = videos.count() if total_videos == 0: self.stdout.write(self.style.ERROR('No videos found in database.')) return self.stdout.write(f'\nFound {total_videos} videos to convert') self.stdout.write(self.style.WARNING('This process will take time as it extracts audio from each video...\n')) processed_count = 0 skipped_count = 0 failed_count = 0 for video in videos: try: # Check if podcast already exists with same slug if skip_existing and Podcast.objects.filter(slug=video.slug).exists(): self.stdout.write(self.style.WARNING(f'Skipping {video.slug}: Already exists')) skipped_count += 1 continue if self.dry_run: self.stdout.write(f'[DRY RUN] Would convert: {video.title}') processed_count += 1 continue # Process the video success = self.convert_video_to_podcast(video) if success: processed_count += 1 else: failed_count += 1 except Exception as e: self.stdout.write(self.style.ERROR(f'✗ Error processing {video.slug}: {str(e)}')) failed_count += 1 self.stdout.write(self.style.SUCCESS(f'\n✓ Conversion complete!')) self.stdout.write(f' Processed: {processed_count}') self.stdout.write(f' Skipped: {skipped_count}') self.stdout.write(f' Failed: {failed_count}') def convert_video_to_podcast(self, video): """Convert a single video to podcast by extracting audio""" self.stdout.write(f'\nProcessing: {video.title}') # Check if video has a file if not video.video_file: self.stdout.write(self.style.WARNING(f' ⚠ No video file found, skipping')) return False temp_dir = None try: # Create temporary directory temp_dir = tempfile.mkdtemp() # Extract audio from video audio_path = self.extract_audio(video, temp_dir) if not audio_path: return False # Ensure unique slug slug = video.slug counter = 1 while Podcast.objects.filter(slug=slug).exists(): slug = f"{video.slug}-{counter}" counter += 1 if slug != video.slug: self.stdout.write(self.style.WARNING(f' ⚠ Slug conflict, using: {slug}')) # Create Podcast object with transaction.atomic(): podcast = Podcast( title=video.title, slug=slug, description=video.description, audio_time=video.video_time, # Same duration status=True, view_count=0, # Start fresh download_count=0 ) # Copy thumbnail if exists if video.thumbnail: try: # Read the video's thumbnail video.thumbnail.open('rb') thumbnail_content = video.thumbnail.read() video.thumbnail.close() # Create a temporary file for the thumbnail from django.core.files.base import ContentFile podcast.thumbnail.save( f'{slug}_thumb.jpg', ContentFile(thumbnail_content), save=False ) self.stdout.write(f' ✓ Thumbnail copied') except Exception as e: self.stdout.write(self.style.WARNING(f' ⚠ Could not copy thumbnail: {str(e)}')) # Save audio file with open(audio_path, 'rb') as audio_file: podcast.audio_file.save( f'{slug}.mp3', File(audio_file), save=False ) podcast.save() self.stdout.write(self.style.SUCCESS(f'✓ Saved podcast: {podcast.title} (slug: {slug})')) return True except Exception as e: self.stdout.write(self.style.ERROR(f'✗ Error: {str(e)}')) return False finally: # Cleanup temporary files if temp_dir and os.path.exists(temp_dir): import shutil shutil.rmtree(temp_dir, ignore_errors=True) def extract_audio(self, video, temp_dir): """Extract audio from video file using ffmpeg""" try: self.stdout.write(f' Extracting audio...') # Get the video file path video_path = video.video_file.path if not os.path.exists(video_path): self.stdout.write(self.style.ERROR(f' ✗ Video file not found at: {video_path}')) return None # Output audio path audio_path = os.path.join(temp_dir, f'{video.slug}.mp3') # Extract audio using ffmpeg # -vn: no video # -acodec libmp3lame: use MP3 codec # -q:a 2: quality (0-9, where 0 is best) cmd = [ 'ffmpeg', '-i', video_path, '-vn', # No video '-acodec', 'libmp3lame', '-q:a', '2', # High quality audio_path, '-y' # Overwrite output file ] self.stdout.write(f' Running ffmpeg...') result = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=600 # 10 minutes timeout ) if result.returncode == 0 and os.path.exists(audio_path): audio_size = os.path.getsize(audio_path) / (1024 * 1024) # Size in MB self.stdout.write(self.style.SUCCESS(f' ✓ Audio extracted: {audio_size:.2f} MB')) return audio_path else: error_msg = result.stderr.decode('utf-8', errors='ignore') self.stdout.write(self.style.ERROR(f' ✗ Audio extraction failed')) if error_msg: self.stdout.write(f' Error details: {error_msg[:200]}') return None except subprocess.TimeoutExpired: self.stdout.write(self.style.ERROR(f' ✗ Timeout during audio extraction')) return None except Exception as e: self.stdout.write(self.style.ERROR(f' ✗ Error: {str(e)}')) return None