diff --git a/videoarchiver/ffmpeg/ffmpeg_manager.py b/videoarchiver/ffmpeg/ffmpeg_manager.py index a36f3b6..e719cb4 100644 --- a/videoarchiver/ffmpeg/ffmpeg_manager.py +++ b/videoarchiver/ffmpeg/ffmpeg_manager.py @@ -31,9 +31,12 @@ class FFmpegManager: base_dir=self.base_dir ) - # Get or download FFmpeg - self.ffmpeg_path = self._initialize_ffmpeg() + # Get or download FFmpeg and FFprobe + binaries = self._initialize_binaries() + self.ffmpeg_path = binaries["ffmpeg"] + self.ffprobe_path = binaries["ffprobe"] logger.info(f"Using FFmpeg from: {self.ffmpeg_path}") + logger.info(f"Using FFprobe from: {self.ffprobe_path}") # Initialize components self.gpu_detector = GPUDetector(self.ffmpeg_path) @@ -48,35 +51,40 @@ class FFmpegManager: self._verify_ffmpeg() logger.info("FFmpeg manager initialized successfully") - def _initialize_ffmpeg(self) -> Path: - """Initialize FFmpeg binary with proper error handling""" + def _initialize_binaries(self) -> Dict[str, Path]: + """Initialize FFmpeg and FFprobe binaries with proper error handling""" try: - # Verify existing FFmpeg if it exists - if self.downloader.ffmpeg_path.exists(): + # Verify existing binaries if they exist + if self.downloader.ffmpeg_path.exists() and self.downloader.ffprobe_path.exists(): logger.info(f"Found existing FFmpeg: {self.downloader.ffmpeg_path}") + logger.info(f"Found existing FFprobe: {self.downloader.ffprobe_path}") if self.downloader.verify(): - return self.downloader.ffmpeg_path + return { + "ffmpeg": self.downloader.ffmpeg_path, + "ffprobe": self.downloader.ffprobe_path + } else: - logger.warning("Existing FFmpeg is not functional, downloading new copy") + logger.warning("Existing binaries are not functional, downloading new copies") - # Download and verify FFmpeg - logger.info("Downloading FFmpeg...") - ffmpeg_path = self.downloader.download() + # Download and verify binaries + logger.info("Downloading FFmpeg and FFprobe...") + binaries = self.downloader.download() if not self.downloader.verify(): - raise FFmpegError("Downloaded FFmpeg binary is not functional") + raise FFmpegError("Downloaded binaries are not functional") # Set executable permissions try: if platform.system() != "Windows": - os.chmod(str(ffmpeg_path), 0o755) + os.chmod(str(binaries["ffmpeg"]), 0o755) + os.chmod(str(binaries["ffprobe"]), 0o755) except Exception as e: - logger.error(f"Failed to set FFmpeg permissions: {e}") + logger.error(f"Failed to set binary permissions: {e}") - return ffmpeg_path + return binaries except Exception as e: - logger.error(f"Failed to initialize FFmpeg: {e}") - raise FFmpegError(f"Failed to initialize FFmpeg: {e}") + logger.error(f"Failed to initialize binaries: {e}") + raise FFmpegError(f"Failed to initialize binaries: {e}") def _verify_ffmpeg(self) -> None: """Verify FFmpeg functionality with comprehensive checks""" @@ -94,6 +102,19 @@ class FFmpegManager: raise FFmpegError("FFmpeg version check failed") logger.info(f"FFmpeg version: {result.stdout.split()[2]}") + # Check FFprobe version + probe_cmd = [str(self.ffprobe_path), "-version"] + result = subprocess.run( + probe_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=10 + ) + if result.returncode != 0: + raise FFmpegError("FFprobe version check failed") + logger.info(f"FFprobe version: {result.stdout.split()[2]}") + # Check FFmpeg capabilities caps_cmd = [str(self.ffmpeg_path), "-hide_banner", "-encoders"] result = subprocess.run( @@ -174,11 +195,19 @@ class FFmpegManager: raise FFmpegError("FFmpeg is not available") return str(self.ffmpeg_path) + def get_ffprobe_path(self) -> str: + """Get path to FFprobe binary""" + if not self.ffprobe_path.exists(): + raise FFmpegError("FFprobe is not available") + return str(self.ffprobe_path) + def force_download(self) -> bool: """Force re-download of FFmpeg binary""" try: logger.info("Force downloading FFmpeg...") - self.ffmpeg_path = self.downloader.download() + binaries = self.downloader.download() + self.ffmpeg_path = binaries["ffmpeg"] + self.ffprobe_path = binaries["ffprobe"] return self.downloader.verify() except Exception as e: logger.error(f"Failed to force download FFmpeg: {e}") diff --git a/videoarchiver/ffmpeg/video_analyzer.py b/videoarchiver/ffmpeg/video_analyzer.py index 3bd0f6e..d2c219b 100644 --- a/videoarchiver/ffmpeg/video_analyzer.py +++ b/videoarchiver/ffmpeg/video_analyzer.py @@ -9,7 +9,6 @@ from contextlib import contextmanager import tempfile import shutil import json -import re logger = logging.getLogger("VideoArchiver") @@ -47,14 +46,7 @@ class VideoAnalyzer: logger.info(f"Initialized VideoAnalyzer with FFmpeg: {self.ffmpeg_path}, FFprobe: {self.ffprobe_path}") def analyze_video(self, input_path: str) -> Dict[str, Any]: - """Analyze video content for optimal encoding settings - - Args: - input_path: Path to input video file - - Returns: - Dict containing video analysis results - """ + """Analyze video content for optimal encoding settings""" try: if not os.path.exists(input_path): logger.error(f"Input file not found: {input_path}") diff --git a/videoarchiver/utils/video_downloader.py b/videoarchiver/utils/video_downloader.py index 8834640..c0fabd0 100644 --- a/videoarchiver/utils/video_downloader.py +++ b/videoarchiver/utils/video_downloader.py @@ -7,6 +7,8 @@ import asyncio import ffmpeg import yt_dlp import shutil +import subprocess +import json from concurrent.futures import ThreadPoolExecutor from typing import Dict, List, Optional, Tuple from pathlib import Path @@ -152,21 +154,50 @@ class VideoDownloader: def _verify_video_file(self, file_path: str) -> bool: """Verify video file integrity""" try: - probe = ffmpeg.probe(file_path) + # Use ffprobe from FFmpegManager + ffprobe_path = str(self.ffmpeg_mgr.get_ffprobe_path()) + logger.debug(f"Using ffprobe from: {ffprobe_path}") + + cmd = [ + ffprobe_path, + "-v", "quiet", + "-print_format", "json", + "-show_format", + "-show_streams", + file_path + ] + + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=30 + ) + + if result.returncode != 0: + raise VideoVerificationError(f"FFprobe failed: {result.stderr}") + + probe = json.loads(result.stdout) + # Check if file has video stream video_streams = [s for s in probe["streams"] if s["codec_type"] == "video"] if not video_streams: raise VideoVerificationError("No video streams found") + # Check if duration is valid duration = float(probe["format"].get("duration", 0)) if duration <= 0: raise VideoVerificationError("Invalid video duration") + # Check if file is readable with open(file_path, "rb") as f: f.seek(0, 2) # Seek to end if f.tell() == 0: raise VideoVerificationError("Empty file") + return True + except Exception as e: logger.error(f"Error verifying video file {file_path}: {e}") return False