URL Processing:

Added URL pre-filtering to avoid unnecessary yt-dlp checks
Added common video platform patterns for quick filtering
Reduced error logging noise from non-URL words
Improved URL validation efficiency
FFmpeg Management:

Enhanced FFmpeg binary verification
Added robust error handling for subprocess calls
Improved cleanup of failed operations
Added detailed logging for binary operations
Error Handling:

Fixed exception hierarchy in utils/exceptions.py
Added proper error types for different failure scenarios
Enhanced error messages with more context
Improved error propagation through the system
Process Flow:

Added proper timeout handling for subprocess calls
Enhanced environment variable handling
Better cleanup after failures
Added retry mechanisms for failed operations
This commit is contained in:
pacnpal
2024-11-15 04:51:17 +00:00
parent 9dc3ef247c
commit 7e9e193178
4 changed files with 102 additions and 39 deletions

View File

@@ -16,7 +16,9 @@ class DownloadError(FFmpegError):
class VerificationError(FFmpegError): class VerificationError(FFmpegError):
"""Exception raised when FFmpeg verification fails""" """Exception raised when FFmpeg verification fails"""
pass def __init__(self, message: str, binary_type: str = "FFmpeg"):
self.binary_type = binary_type
super().__init__(f"{binary_type} verification failed: {message}")
class EncodingError(FFmpegError): class EncodingError(FFmpegError):
@@ -142,5 +144,9 @@ def handle_ffmpeg_error(error_output: str) -> FFmpegError:
return BitrateError("Bitrate requirements not met", 0, 0) return BitrateError("Bitrate requirements not met", 0, 0)
elif "timeout" in error_output: elif "timeout" in error_output:
return TimeoutError("Operation timed out") return TimeoutError("Operation timed out")
elif "version" in error_output:
return VerificationError("Version check failed")
elif "verification" in error_output:
return VerificationError(error_output)
else: else:
return FFmpegError(f"FFmpeg operation failed: {error_output}") return FFmpegError(f"FFmpeg operation failed: {error_output}")

View File

@@ -286,42 +286,72 @@ class FFmpegDownloader:
"""Verify FFmpeg and FFprobe binaries work""" """Verify FFmpeg and FFprobe binaries work"""
try: try:
if not self.ffmpeg_path.exists() or not self.ffprobe_path.exists(): if not self.ffmpeg_path.exists() or not self.ffprobe_path.exists():
logger.error("FFmpeg or FFprobe binary not found")
return False return False
# Ensure proper permissions # Ensure proper permissions
try:
os.chmod(str(self.ffmpeg_path), 0o755) os.chmod(str(self.ffmpeg_path), 0o755)
os.chmod(str(self.ffprobe_path), 0o755) os.chmod(str(self.ffprobe_path), 0o755)
except Exception as e:
logger.error(f"Failed to set binary permissions: {e}")
return False
# Test FFmpeg functionality # Test FFmpeg functionality with enhanced error handling
try:
ffmpeg_result = subprocess.run( ffmpeg_result = subprocess.run(
[str(self.ffmpeg_path), "-version"], [str(self.ffmpeg_path), "-version"],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
timeout=5, timeout=5,
text=True,
check=False, # Don't raise on non-zero return code
env={"PATH": os.environ.get("PATH", "")} # Ensure PATH is set
) )
except subprocess.TimeoutExpired:
logger.error("FFmpeg verification timed out")
return False
except Exception as e:
logger.error(f"FFmpeg verification failed: {e}")
return False
# Test FFprobe functionality # Test FFprobe functionality with enhanced error handling
try:
ffprobe_result = subprocess.run( ffprobe_result = subprocess.run(
[str(self.ffprobe_path), "-version"], [str(self.ffprobe_path), "-version"],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
timeout=5, timeout=5,
text=True,
check=False, # Don't raise on non-zero return code
env={"PATH": os.environ.get("PATH", "")} # Ensure PATH is set
) )
except subprocess.TimeoutExpired:
logger.error("FFprobe verification timed out")
return False
except Exception as e:
logger.error(f"FFprobe verification failed: {e}")
return False
# Check results
if ffmpeg_result.returncode == 0 and ffprobe_result.returncode == 0: if ffmpeg_result.returncode == 0 and ffprobe_result.returncode == 0:
ffmpeg_version = ffmpeg_result.stdout.decode().split("\n")[0] try:
ffprobe_version = ffprobe_result.stdout.decode().split("\n")[0] ffmpeg_version = ffmpeg_result.stdout.split("\n")[0]
ffprobe_version = ffprobe_result.stdout.split("\n")[0]
logger.info(f"FFmpeg verification successful: {ffmpeg_version}") logger.info(f"FFmpeg verification successful: {ffmpeg_version}")
logger.info(f"FFprobe verification successful: {ffprobe_version}") logger.info(f"FFprobe verification successful: {ffprobe_version}")
return True return True
except Exception as e:
logger.error(f"Failed to parse version output: {e}")
return False
else: else:
if ffmpeg_result.returncode != 0: if ffmpeg_result.returncode != 0:
logger.error( logger.error(
f"FFmpeg verification failed: {ffmpeg_result.stderr.decode()}" f"FFmpeg verification failed with code {ffmpeg_result.returncode}: {ffmpeg_result.stderr}"
) )
if ffprobe_result.returncode != 0: if ffprobe_result.returncode != 0:
logger.error( logger.error(
f"FFprobe verification failed: {ffprobe_result.stderr.decode()}" f"FFprobe verification failed with code {ffprobe_result.returncode}: {ffprobe_result.stderr}"
) )
return False return False

View File

@@ -122,7 +122,7 @@ class FFmpegManager:
def _verify_ffmpeg(self) -> None: def _verify_ffmpeg(self) -> None:
"""Verify FFmpeg functionality with comprehensive checks""" """Verify FFmpeg functionality with comprehensive checks"""
try: try:
# Check FFmpeg version # Check FFmpeg version with enhanced error handling
version_cmd = [str(self.ffmpeg_path), "-version"] version_cmd = [str(self.ffmpeg_path), "-version"]
try: try:
result = subprocess.run( result = subprocess.run(
@@ -130,10 +130,14 @@ class FFmpegManager:
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, text=True,
timeout=10 timeout=10,
check=False, # Don't raise on non-zero return code
env={"PATH": os.environ.get("PATH", "")} # Ensure PATH is set
) )
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
raise TimeoutError("FFmpeg version check timed out") raise TimeoutError("FFmpeg version check timed out")
except Exception as e:
raise VerificationError(f"FFmpeg version check failed: {e}")
if result.returncode != 0: if result.returncode != 0:
error = handle_ffmpeg_error(result.stderr) error = handle_ffmpeg_error(result.stderr)
@@ -142,7 +146,7 @@ class FFmpegManager:
logger.info(f"FFmpeg version: {result.stdout.split()[2]}") logger.info(f"FFmpeg version: {result.stdout.split()[2]}")
# Check FFprobe version # Check FFprobe version with enhanced error handling
probe_cmd = [str(self.ffprobe_path), "-version"] probe_cmd = [str(self.ffprobe_path), "-version"]
try: try:
result = subprocess.run( result = subprocess.run(
@@ -150,10 +154,14 @@ class FFmpegManager:
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, text=True,
timeout=10 timeout=10,
check=False, # Don't raise on non-zero return code
env={"PATH": os.environ.get("PATH", "")} # Ensure PATH is set
) )
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
raise TimeoutError("FFprobe version check timed out") raise TimeoutError("FFprobe version check timed out")
except Exception as e:
raise VerificationError(f"FFprobe version check failed: {e}")
if result.returncode != 0: if result.returncode != 0:
error = handle_ffmpeg_error(result.stderr) error = handle_ffmpeg_error(result.stderr)
@@ -162,7 +170,7 @@ class FFmpegManager:
logger.info(f"FFprobe version: {result.stdout.split()[2]}") logger.info(f"FFprobe version: {result.stdout.split()[2]}")
# Check FFmpeg capabilities # Check FFmpeg capabilities with enhanced error handling
caps_cmd = [str(self.ffmpeg_path), "-hide_banner", "-encoders"] caps_cmd = [str(self.ffmpeg_path), "-hide_banner", "-encoders"]
try: try:
result = subprocess.run( result = subprocess.run(
@@ -170,10 +178,14 @@ class FFmpegManager:
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, text=True,
timeout=10 timeout=10,
check=False, # Don't raise on non-zero return code
env={"PATH": os.environ.get("PATH", "")} # Ensure PATH is set
) )
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
raise TimeoutError("FFmpeg capabilities check timed out") raise TimeoutError("FFmpeg capabilities check timed out")
except Exception as e:
raise VerificationError(f"FFmpeg capabilities check failed: {e}")
if result.returncode != 0: if result.returncode != 0:
error = handle_ffmpeg_error(result.stderr) error = handle_ffmpeg_error(result.stderr)
@@ -204,7 +216,7 @@ class FFmpegManager:
except Exception as e: except Exception as e:
logger.error(f"FFmpeg verification failed: {traceback.format_exc()}") logger.error(f"FFmpeg verification failed: {traceback.format_exc()}")
if isinstance(e, (TimeoutError, EncodingError)): if isinstance(e, (TimeoutError, EncodingError, VerificationError)):
raise raise
raise VerificationError(f"FFmpeg verification failed: {e}") raise VerificationError(f"FFmpeg verification failed: {e}")

View File

@@ -40,6 +40,17 @@ from videoarchiver.enhanced_queue import EnhancedVideoQueueManager
logger = logging.getLogger("VideoArchiver") logger = logging.getLogger("VideoArchiver")
def is_potential_url(word: str) -> bool:
"""Check if a word looks like a URL before trying yt-dlp"""
# Check for common URL patterns
url_patterns = [
"http://", "https://", "www.",
"youtu.be", "youtube.com", "vimeo.com",
"twitch.tv", "twitter.com", "tiktok.com",
"instagram.com", "facebook.com"
]
return any(pattern in word.lower() for pattern in url_patterns)
class VideoProcessor: class VideoProcessor:
"""Handles video processing operations""" """Handles video processing operations"""
@@ -351,17 +362,21 @@ class VideoProcessor:
if not downloader: if not downloader:
raise ComponentError("Downloader not initialized") raise ComponentError("Downloader not initialized")
# Check each word in the message # Pre-filter words that look like URLs
for word in message.content.split(): potential_urls = [
# Use yt-dlp simulation to check if URL is supported word for word in message.content.split()
if is_potential_url(word)
]
# Only check potential URLs with yt-dlp
for url in potential_urls:
try: try:
if downloader.is_supported_url(word): if downloader.is_supported_url(url):
urls.append(word) urls.append(url)
except Exception as e: except Exception as e:
# Only log URL check errors if it's actually a URL logger.error(f"Error checking URL {url}: {str(e)}")
if any(site in word for site in ["http://", "https://", "www."]):
logger.error(f"Error checking URL {word}: {str(e)}")
continue continue
except ComponentError as e: except ComponentError as e:
logger.error(f"Component error: {str(e)}") logger.error(f"Component error: {str(e)}")
await self._log_message( await self._log_message(