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
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
|