You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

227 lines
8.3 KiB

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