|
@@ -394,6 +394,50 @@ def download_video(video_id, output_path, video_title="Unknown"):
|
|
|
logger.info(f"Starting download: {video_title} (ID: {video_id})")
|
|
logger.info(f"Starting download: {video_title} (ID: {video_id})")
|
|
|
logger.debug(f"Download path: {output_path}")
|
|
logger.debug(f"Download path: {output_path}")
|
|
|
|
|
|
|
|
|
|
+ # First, check what format would be downloaded to determine compatibility
|
|
|
|
|
+ logger.debug(f"Checking video format compatibility for: {video_title}")
|
|
|
|
|
+
|
|
|
|
|
+ simulate_command = [
|
|
|
|
|
+ "yt-dlp", "--simulate", "--print", "%(format_id)s|%(vcodec)s|%(height)s|%(ext)s",
|
|
|
|
|
+ "-f", "(bestvideo[height>=2160][vcodec~='^(avc|h264|hevc|h265)']+bestaudio)/(bestvideo[height>=2160]+bestaudio)/(bestvideo[height>=1440]+bestaudio)/(bestvideo[height>=1080]+bestaudio)/best",
|
|
|
|
|
+ f"https://www.youtube.com/watch?v={video_id}"
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ will_be_compatible = True
|
|
|
|
|
+ predicted_codec = "unknown"
|
|
|
|
|
+ predicted_height = 0
|
|
|
|
|
+ temp_filename = None
|
|
|
|
|
+ final_filename = None
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ sim_result = subprocess.run(simulate_command, capture_output=True, text=True, timeout=30)
|
|
|
|
|
+ if sim_result.returncode == 0 and sim_result.stdout.strip():
|
|
|
|
|
+ format_info = sim_result.stdout.strip().split('|')
|
|
|
|
|
+ if len(format_info) >= 3:
|
|
|
|
|
+ predicted_codec = format_info[1] if format_info[1] != 'none' else 'unknown'
|
|
|
|
|
+ predicted_height = int(format_info[2]) if format_info[2].isdigit() else 0
|
|
|
|
|
+
|
|
|
|
|
+ # Check if the predicted codec is Apple TV incompatible
|
|
|
|
|
+ if any(incompatible in predicted_codec.lower() for incompatible in ['vp9', 'av01', 'av1']):
|
|
|
|
|
+ will_be_compatible = False
|
|
|
|
|
+ # Create temporary filename for incompatible formats
|
|
|
|
|
+ sanitized_title = sanitize_filename(video_title)
|
|
|
|
|
+ temp_filename = f"{sanitized_title}.incompatible"
|
|
|
|
|
+ final_filename = f"{sanitized_title}"
|
|
|
|
|
+ logger.info(f"⚠️ Incompatible codec detected: {predicted_codec} - downloading as temporary file")
|
|
|
|
|
+ logger.info(f" Temporary: {temp_filename}")
|
|
|
|
|
+ logger.info(f" Final: {final_filename}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.info(f"✅ Compatible codec detected: {predicted_codec}")
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.debug(f"Could not simulate download format: {e}")
|
|
|
|
|
+
|
|
|
|
|
+ # Set output filename based on compatibility prediction
|
|
|
|
|
+ if temp_filename:
|
|
|
|
|
+ output_template = os.path.join(output_path, f"{temp_filename}.%(ext)s")
|
|
|
|
|
+ else:
|
|
|
|
|
+ output_template = os.path.join(output_path, "%(title)s.%(ext)s")
|
|
|
|
|
+
|
|
|
# List available formats for debugging (only in debug mode)
|
|
# List available formats for debugging (only in debug mode)
|
|
|
if logger.isEnabledFor(logging.DEBUG):
|
|
if logger.isEnabledFor(logging.DEBUG):
|
|
|
list_available_formats(video_id)
|
|
list_available_formats(video_id)
|
|
@@ -417,7 +461,7 @@ def download_video(video_id, output_path, video_title="Unknown"):
|
|
|
"--verbose" if logger.isEnabledFor(logging.DEBUG) else "--no-warnings",
|
|
"--verbose" if logger.isEnabledFor(logging.DEBUG) else "--no-warnings",
|
|
|
"--write-info-json", # Write metadata for analysis
|
|
"--write-info-json", # Write metadata for analysis
|
|
|
f"https://www.youtube.com/watch?v={video_id}",
|
|
f"https://www.youtube.com/watch?v={video_id}",
|
|
|
- "-o", os.path.join(output_path, "%(title)s.%(ext)s")
|
|
|
|
|
|
|
+ "-o", output_template
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
logger.debug(f"yt-dlp command: {' '.join(command)}")
|
|
logger.debug(f"yt-dlp command: {' '.join(command)}")
|
|
@@ -425,15 +469,73 @@ def download_video(video_id, output_path, video_title="Unknown"):
|
|
|
try:
|
|
try:
|
|
|
start_time = time.time()
|
|
start_time = time.time()
|
|
|
logging.info(f"Starting download with command: {' '.join(command)}")
|
|
logging.info(f"Starting download with command: {' '.join(command)}")
|
|
|
- result = subprocess.run(command, check=True, capture_output=True, text=True)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ # Add progress hook for yt-dlp progress tracking
|
|
|
|
|
+ command.extend(["--progress-template", "download:%(progress._percent_str)s %(progress._eta_str)s %(progress._speed_str)s"])
|
|
|
|
|
+
|
|
|
|
|
+ # Start download with progress monitoring
|
|
|
|
|
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, universal_newlines=True)
|
|
|
|
|
+
|
|
|
|
|
+ current_progress = 0
|
|
|
|
|
+ while True:
|
|
|
|
|
+ output = process.stdout.readline()
|
|
|
|
|
+ if output == '' and process.poll() is not None:
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ if output:
|
|
|
|
|
+ output = output.strip()
|
|
|
|
|
+ logging.debug(f"yt-dlp output: {output}")
|
|
|
|
|
+
|
|
|
|
|
+ # Parse progress from yt-dlp output
|
|
|
|
|
+ if "%" in output and "ETA" in output:
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Extract percentage from progress line
|
|
|
|
|
+ parts = output.split()
|
|
|
|
|
+ for part in parts:
|
|
|
|
|
+ if "%" in part:
|
|
|
|
|
+ progress_str = part.replace("%", "")
|
|
|
|
|
+ if progress_str.replace(".", "").isdigit():
|
|
|
|
|
+ new_progress = float(progress_str)
|
|
|
|
|
+ if abs(new_progress - current_progress) >= 5: # Update every 5%
|
|
|
|
|
+ current_progress = new_progress
|
|
|
|
|
+
|
|
|
|
|
+ # Update Home Assistant with current progress
|
|
|
|
|
+ update_homeassistant_stats(
|
|
|
|
|
+ download_queue=download_stats['total_videos'] - download_stats['completed_videos'],
|
|
|
|
|
+ conversion_queue=conversion_queue.qsize(),
|
|
|
|
|
+ download_progress=round((download_stats['completed_videos'] / max(download_stats['total_videos'], 1)) * 100, 1),
|
|
|
|
|
+ active_downloads=download_stats['active_downloads'],
|
|
|
|
|
+ active_conversions=conversion_stats['active_conversions'],
|
|
|
|
|
+ current_download_progress=current_progress,
|
|
|
|
|
+ current_download_file=video_title[:50]
|
|
|
|
|
+ )
|
|
|
|
|
+ logger.info(f"Download progress: {current_progress:.1f}% - {video_title}")
|
|
|
|
|
+ break
|
|
|
|
|
+ except (ValueError, IndexError):
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ # Wait for process to complete
|
|
|
|
|
+ process.wait()
|
|
|
end_time = time.time()
|
|
end_time = time.time()
|
|
|
|
|
|
|
|
|
|
+ if process.returncode != 0:
|
|
|
|
|
+ stderr_output = process.stderr.read()
|
|
|
|
|
+ logging.error(f"yt-dlp failed with return code {process.returncode}: {stderr_output}")
|
|
|
|
|
+ raise subprocess.CalledProcessError(process.returncode, command, stderr=stderr_output)
|
|
|
|
|
+
|
|
|
download_time = end_time - start_time
|
|
download_time = end_time - start_time
|
|
|
logger.info(f"Download completed in {download_time:.1f}s: {video_title}")
|
|
logger.info(f"Download completed in {download_time:.1f}s: {video_title}")
|
|
|
- logging.info(f"yt-dlp stdout: {result.stdout}")
|
|
|
|
|
- if result.stderr:
|
|
|
|
|
- logging.warning(f"yt-dlp stderr: {result.stderr}")
|
|
|
|
|
- logger.debug(f"yt-dlp output: {result.stdout[-500:]}") # Log last 500 chars
|
|
|
|
|
|
|
+
|
|
|
|
|
+ # Clear current download progress
|
|
|
|
|
+ update_homeassistant_stats(
|
|
|
|
|
+ download_queue=download_stats['total_videos'] - download_stats['completed_videos'],
|
|
|
|
|
+ conversion_queue=conversion_queue.qsize(),
|
|
|
|
|
+ download_progress=round((download_stats['completed_videos'] / max(download_stats['total_videos'], 1)) * 100, 1),
|
|
|
|
|
+ active_downloads=download_stats['active_downloads'],
|
|
|
|
|
+ active_conversions=conversion_stats['active_conversions'],
|
|
|
|
|
+ current_download_progress=100,
|
|
|
|
|
+ current_download_file=""
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
# Analyze what was actually downloaded and check Apple TV compatibility
|
|
# Analyze what was actually downloaded and check Apple TV compatibility
|
|
|
try:
|
|
try:
|
|
@@ -477,29 +579,55 @@ def download_video(video_id, output_path, video_title="Unknown"):
|
|
|
file_size_mb = file_size / (1024 * 1024)
|
|
file_size_mb = file_size / (1024 * 1024)
|
|
|
logger.info(f"Downloaded file size: {file_size_mb:.1f} MB")
|
|
logger.info(f"Downloaded file size: {file_size_mb:.1f} MB")
|
|
|
|
|
|
|
|
|
|
+ # Check if this is a temporary incompatible file
|
|
|
|
|
+ is_temp_file = ".incompatible." in file
|
|
|
|
|
+
|
|
|
# Check Apple TV compatibility and queue for conversion if needed
|
|
# Check Apple TV compatibility and queue for conversion if needed
|
|
|
needs_conversion = False
|
|
needs_conversion = False
|
|
|
conversion_reason = ""
|
|
conversion_reason = ""
|
|
|
|
|
+ target_filename = None
|
|
|
|
|
|
|
|
- # Check if file needs conversion for Apple TV compatibility
|
|
|
|
|
- if codec.lower() in ['vp9', 'av01', 'av1']:
|
|
|
|
|
|
|
+ if is_temp_file:
|
|
|
|
|
+ # This was downloaded as incompatible - definitely needs conversion
|
|
|
needs_conversion = True
|
|
needs_conversion = True
|
|
|
- conversion_reason = f"Incompatible codec ({codec}) for Apple TV"
|
|
|
|
|
- elif not file.endswith('.mp4'):
|
|
|
|
|
- needs_conversion = True
|
|
|
|
|
- conversion_reason = f"Incompatible container ({file.split('.')[-1]}) for Apple TV"
|
|
|
|
|
- elif height >= 2160 and codec.lower() not in ['h264', 'avc1', 'hevc', 'h265']:
|
|
|
|
|
- needs_conversion = True
|
|
|
|
|
- conversion_reason = f"4K video with non-Apple TV codec ({codec})"
|
|
|
|
|
|
|
+ conversion_reason = f"Pre-identified incompatible format ({codec})"
|
|
|
|
|
+ # Set target filename (remove .incompatible from the name but keep extension)
|
|
|
|
|
+ base_name, ext = os.path.splitext(file)
|
|
|
|
|
+ if ".incompatible" in base_name:
|
|
|
|
|
+ clean_base = base_name.replace(".incompatible", "")
|
|
|
|
|
+ target_filename = f"{clean_base}{ext}"
|
|
|
|
|
+ else:
|
|
|
|
|
+ target_filename = file
|
|
|
|
|
+ target_path = os.path.join(output_path, target_filename)
|
|
|
|
|
+ logger.info(f"🔄 Temporary incompatible file detected - will convert to: {target_filename}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ # Regular compatibility check for files downloaded with standard names
|
|
|
|
|
+ if any(incompatible in codec.lower() for incompatible in ['vp9', 'av01', 'av1']):
|
|
|
|
|
+ needs_conversion = True
|
|
|
|
|
+ conversion_reason = f"Incompatible codec ({codec}) for Apple TV"
|
|
|
|
|
+ elif not file.endswith('.mp4'):
|
|
|
|
|
+ needs_conversion = True
|
|
|
|
|
+ conversion_reason = f"Incompatible container ({file.split('.')[-1]}) for Apple TV"
|
|
|
|
|
+ elif height >= 2160 and not any(compatible in codec.lower() for compatible in ['h264', 'avc1', 'hevc', 'h265']):
|
|
|
|
|
+ needs_conversion = True
|
|
|
|
|
+ conversion_reason = f"4K video with non-Apple TV codec ({codec})"
|
|
|
|
|
|
|
|
if needs_conversion:
|
|
if needs_conversion:
|
|
|
logger.info(f"Queuing for conversion: {conversion_reason}")
|
|
logger.info(f"Queuing for conversion: {conversion_reason}")
|
|
|
# Determine priority based on video resolution
|
|
# Determine priority based on video resolution
|
|
|
priority = 2 if height >= 2160 else 1 # 4K gets higher priority
|
|
priority = 2 if height >= 2160 else 1 # 4K gets higher priority
|
|
|
- add_to_conversion_queue(file_path, priority)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ # Add to conversion queue with target filename if specified
|
|
|
|
|
+ if target_filename:
|
|
|
|
|
+ add_to_conversion_queue_with_target(file_path, target_path, priority)
|
|
|
|
|
+ logger.info(f"📋 Queued: {file} → {target_filename}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ add_to_conversion_queue(file_path, priority)
|
|
|
|
|
+ logger.info(f"📋 Queued: {file}")
|
|
|
|
|
+
|
|
|
print_queue_status()
|
|
print_queue_status()
|
|
|
else:
|
|
else:
|
|
|
- logger.info(f"File is Apple TV compatible: {codec} in MP4 container")
|
|
|
|
|
|
|
+ logger.info(f"✅ File is Apple TV compatible: {codec} in MP4 container")
|
|
|
|
|
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
logger.debug(f"Could not analyze downloaded file {file}: {e}")
|
|
logger.debug(f"Could not analyze downloaded file {file}: {e}")
|
|
@@ -555,6 +683,24 @@ def add_to_conversion_queue(input_file, priority=1):
|
|
|
logger.error(f"Error adding to conversion queue: {e}")
|
|
logger.error(f"Error adding to conversion queue: {e}")
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
|
|
+def add_to_conversion_queue_with_target(input_file, target_file, priority=1):
|
|
|
|
|
+ """Add a video file to the conversion queue with a specific target filename."""
|
|
|
|
|
+ logger = logging.getLogger(__name__)
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Higher number = higher priority (4K gets priority 2, others get 1)
|
|
|
|
|
+ # Store as tuple: (priority, input_file, target_file)
|
|
|
|
|
+ conversion_queue.put((priority, input_file, target_file), timeout=1)
|
|
|
|
|
+ logger.info(f"Added to conversion queue: {os.path.basename(input_file)} → {os.path.basename(target_file)} (priority: {priority})")
|
|
|
|
|
+ logger.debug(f"Queue size after addition: {conversion_queue.qsize()}")
|
|
|
|
|
+ return True
|
|
|
|
|
+ except queue.Full:
|
|
|
|
|
+ logger.warning(f"Conversion queue is full! Skipping: {os.path.basename(input_file)}")
|
|
|
|
|
+ return False
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.error(f"Error adding to conversion queue: {e}")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
def get_queue_status():
|
|
def get_queue_status():
|
|
|
"""Get current conversion queue status."""
|
|
"""Get current conversion queue status."""
|
|
|
queue_size = conversion_queue.qsize()
|
|
queue_size = conversion_queue.qsize()
|
|
@@ -579,7 +725,18 @@ def process_conversion_queue():
|
|
|
while True:
|
|
while True:
|
|
|
try:
|
|
try:
|
|
|
# Get item from queue (blocks until available)
|
|
# Get item from queue (blocks until available)
|
|
|
- priority, input_file = conversion_queue.get(timeout=30)
|
|
|
|
|
|
|
+ queue_item = conversion_queue.get(timeout=30)
|
|
|
|
|
+
|
|
|
|
|
+ # Handle both old format (priority, input_file) and new format (priority, input_file, target_file)
|
|
|
|
|
+ if len(queue_item) == 2:
|
|
|
|
|
+ priority, input_file = queue_item
|
|
|
|
|
+ target_file = None
|
|
|
|
|
+ elif len(queue_item) == 3:
|
|
|
|
|
+ priority, input_file, target_file = queue_item
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.error(f"Invalid queue item format: {queue_item}")
|
|
|
|
|
+ conversion_queue.task_done()
|
|
|
|
|
+ continue
|
|
|
|
|
|
|
|
# Check if file still exists
|
|
# Check if file still exists
|
|
|
if not os.path.exists(input_file):
|
|
if not os.path.exists(input_file):
|
|
@@ -587,10 +744,14 @@ def process_conversion_queue():
|
|
|
conversion_queue.task_done()
|
|
conversion_queue.task_done()
|
|
|
continue
|
|
continue
|
|
|
|
|
|
|
|
- logger.info(f"Starting conversion (priority {priority}): {os.path.basename(input_file)}")
|
|
|
|
|
-
|
|
|
|
|
- # Perform the conversion
|
|
|
|
|
- success = convert_to_apple_tv_compatible(input_file)
|
|
|
|
|
|
|
+ if target_file:
|
|
|
|
|
+ logger.info(f"Starting conversion (priority {priority}): {os.path.basename(input_file)} → {os.path.basename(target_file)}")
|
|
|
|
|
+ # Perform the conversion with target filename
|
|
|
|
|
+ success = convert_to_apple_tv_compatible_with_target(input_file, target_file)
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.info(f"Starting conversion (priority {priority}): {os.path.basename(input_file)}")
|
|
|
|
|
+ # Perform the conversion with standard naming
|
|
|
|
|
+ success = convert_to_apple_tv_compatible(input_file)
|
|
|
|
|
|
|
|
if success:
|
|
if success:
|
|
|
logger.info(f"Conversion completed successfully: {os.path.basename(input_file)}")
|
|
logger.info(f"Conversion completed successfully: {os.path.basename(input_file)}")
|
|
@@ -611,8 +772,50 @@ def process_conversion_queue():
|
|
|
except:
|
|
except:
|
|
|
pass
|
|
pass
|
|
|
|
|
|
|
|
|
|
+def sanitize_filename(filename):
|
|
|
|
|
+ """Sanitize a filename to remove invalid characters."""
|
|
|
|
|
+ import re
|
|
|
|
|
+ # Remove invalid filename characters
|
|
|
|
|
+ sanitized = re.sub(r'[<>:"/\\|?*]', '_', filename)
|
|
|
|
|
+ # Remove extra spaces and limit length
|
|
|
|
|
+ sanitized = ' '.join(sanitized.split())[:200]
|
|
|
|
|
+ return sanitized
|
|
|
|
|
+
|
|
|
|
|
+def test_vaapi_support():
|
|
|
|
|
+ """Test if Intel VAAPI hardware acceleration is available."""
|
|
|
|
|
+ logger = logging.getLogger(__name__)
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Test H.264 VAAPI encoding
|
|
|
|
|
+ test_cmd = [FFMPEG_PATH, "-hide_banner", "-f", "lavfi", "-i", "testsrc=duration=1:size=320x240:rate=1",
|
|
|
|
|
+ "-vaapi_device", "/dev/dri/renderD128", "-vf", "format=nv12,hwupload",
|
|
|
|
|
+ "-c:v", "h264_vaapi", "-frames:v", "1", "-f", "null", "-"]
|
|
|
|
|
+ result = subprocess.run(test_cmd, capture_output=True, text=True, timeout=10)
|
|
|
|
|
+ h264_vaapi_available = result.returncode == 0
|
|
|
|
|
+
|
|
|
|
|
+ # Test HEVC VAAPI encoding (optional, may not work on all Intel GPUs)
|
|
|
|
|
+ test_cmd = [FFMPEG_PATH, "-hide_banner", "-f", "lavfi", "-i", "testsrc=duration=1:size=320x240:rate=1",
|
|
|
|
|
+ "-vaapi_device", "/dev/dri/renderD128", "-vf", "format=nv12,hwupload",
|
|
|
|
|
+ "-c:v", "hevc_vaapi", "-frames:v", "1", "-f", "null", "-"]
|
|
|
|
|
+ result = subprocess.run(test_cmd, capture_output=True, text=True, timeout=10)
|
|
|
|
|
+ hevc_vaapi_available = result.returncode == 0
|
|
|
|
|
+
|
|
|
|
|
+ if h264_vaapi_available and hevc_vaapi_available:
|
|
|
|
|
+ logger.info("🚀 Intel VAAPI hardware acceleration available (H.264 + HEVC)")
|
|
|
|
|
+ return True, True
|
|
|
|
|
+ elif h264_vaapi_available:
|
|
|
|
|
+ logger.info("🚀 Intel VAAPI hardware acceleration available (H.264 only)")
|
|
|
|
|
+ return True, False
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.info("⚠️ Intel VAAPI hardware acceleration not available, falling back to software encoding")
|
|
|
|
|
+ return False, False
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.warning(f"Could not test VAAPI support: {e}")
|
|
|
|
|
+ return False, False
|
|
|
|
|
+
|
|
|
def convert_to_apple_tv_compatible(input_file):
|
|
def convert_to_apple_tv_compatible(input_file):
|
|
|
- """Convert a video to Apple TV 4K compatible format, optimized by resolution."""
|
|
|
|
|
|
|
+ """Convert a video to Apple TV 4K compatible format with Intel VAAPI hardware acceleration."""
|
|
|
global conversion_stats
|
|
global conversion_stats
|
|
|
logger = logging.getLogger(__name__)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
@@ -632,10 +835,13 @@ def convert_to_apple_tv_compatible(input_file):
|
|
|
conversion_stats['active_conversions'] += 1
|
|
conversion_stats['active_conversions'] += 1
|
|
|
conversion_stats['current_conversion'] = os.path.basename(input_file)
|
|
conversion_stats['current_conversion'] = os.path.basename(input_file)
|
|
|
|
|
|
|
|
- logger.info(f"Converting {os.path.basename(input_file)} to Apple TV compatible format...")
|
|
|
|
|
|
|
+ logger.info(f"Converting {os.path.basename(input_file)} to Apple TV compatible format with GPU acceleration...")
|
|
|
logger.debug(f"Input file: {input_file}")
|
|
logger.debug(f"Input file: {input_file}")
|
|
|
logger.debug(f"Output file: {mp4_file}")
|
|
logger.debug(f"Output file: {mp4_file}")
|
|
|
|
|
|
|
|
|
|
+ # Test VAAPI support
|
|
|
|
|
+ vaapi_h264_available, vaapi_hevc_available = test_vaapi_support()
|
|
|
|
|
+
|
|
|
# Update Home Assistant
|
|
# Update Home Assistant
|
|
|
update_homeassistant_stats(
|
|
update_homeassistant_stats(
|
|
|
conversion_queue=conversion_queue.qsize(),
|
|
conversion_queue=conversion_queue.qsize(),
|
|
@@ -669,44 +875,69 @@ def convert_to_apple_tv_compatible(input_file):
|
|
|
is_4k = False
|
|
is_4k = False
|
|
|
is_high_res = True
|
|
is_high_res = True
|
|
|
|
|
|
|
|
- # Apple TV 4K optimized encoding based on source quality:
|
|
|
|
|
- if is_4k:
|
|
|
|
|
- # 4K content: Use H.265 for better compression at high resolution
|
|
|
|
|
|
|
+ # Intel VAAPI optimized encoding based on source quality and hardware support:
|
|
|
|
|
+ if is_4k and vaapi_hevc_available:
|
|
|
|
|
+ # 4K content with VAAPI HEVC: Use hardware-accelerated H.265
|
|
|
codec_args = [
|
|
codec_args = [
|
|
|
- "-c:v", "libx265", # H.265 for 4K (better compression)
|
|
|
|
|
- "-preset", "medium", # Better compression for 4K
|
|
|
|
|
- "-crf", "20", # Higher quality for 4K content
|
|
|
|
|
- "-level", "5.1", # Level 5.1 supports 4K
|
|
|
|
|
- "-pix_fmt", "yuv420p", # Ensure compatibility
|
|
|
|
|
|
|
+ "-vaapi_device", "/dev/dri/renderD128", # VAAPI device
|
|
|
|
|
+ "-vf", "format=nv12,hwupload", # Upload to GPU memory
|
|
|
|
|
+ "-c:v", "hevc_vaapi", # Intel VAAPI HEVC encoder
|
|
|
|
|
+ "-qp", "20", # Quality parameter (lower = better)
|
|
|
|
|
+ "-rc_mode", "CQP", # Constant Quality mode
|
|
|
]
|
|
]
|
|
|
- logger.info("Using H.265 encoding for 4K content")
|
|
|
|
|
|
|
+ logger.info("🚀 Using Intel VAAPI HEVC hardware acceleration for 4K content")
|
|
|
|
|
+ elif (is_4k or is_high_res) and vaapi_h264_available:
|
|
|
|
|
+ # 4K/1080p content with VAAPI H.264: Use hardware-accelerated H.264
|
|
|
|
|
+ quality = "20" if is_4k else "22" # Slightly lower quality for 1080p
|
|
|
|
|
+ codec_args = [
|
|
|
|
|
+ "-vaapi_device", "/dev/dri/renderD128", # VAAPI device
|
|
|
|
|
+ "-vf", "format=nv12,hwupload", # Upload to GPU memory
|
|
|
|
|
+ "-c:v", "h264_vaapi", # Intel VAAPI H.264 encoder
|
|
|
|
|
+ "-qp", quality, # Quality parameter
|
|
|
|
|
+ "-rc_mode", "CQP", # Constant Quality mode
|
|
|
|
|
+ "-profile:v", "high", # H.264 High Profile
|
|
|
|
|
+ ]
|
|
|
|
|
+ quality_label = "4K" if is_4k else "1080p"
|
|
|
|
|
+ logger.info(f"🚀 Using Intel VAAPI H.264 hardware acceleration for {quality_label} content")
|
|
|
|
|
+ elif is_4k:
|
|
|
|
|
+ # 4K content without VAAPI: Fall back to software H.265
|
|
|
|
|
+ codec_args = [
|
|
|
|
|
+ "-c:v", "libx265", # Software H.265 for 4K
|
|
|
|
|
+ "-preset", "medium", # Better compression for 4K
|
|
|
|
|
+ "-crf", "20", # Higher quality for 4K content
|
|
|
|
|
+ "-level", "5.1", # Level 5.1 supports 4K
|
|
|
|
|
+ "-pix_fmt", "yuv420p", # Standard compatibility
|
|
|
|
|
+ ]
|
|
|
|
|
+ logger.info("⚠️ Using software H.265 encoding for 4K content (VAAPI not available)")
|
|
|
elif is_high_res:
|
|
elif is_high_res:
|
|
|
- # 1080p content: Use H.264 for maximum compatibility
|
|
|
|
|
|
|
+ # 1080p content without VAAPI: Use software H.264
|
|
|
codec_args = [
|
|
codec_args = [
|
|
|
- "-c:v", "libx264", # H.264 for 1080p (max compatibility)
|
|
|
|
|
- "-profile:v", "high", # High profile for better quality
|
|
|
|
|
- "-preset", "fast", # Faster encoding for 1080p
|
|
|
|
|
- "-crf", "21", # Good quality for 1080p
|
|
|
|
|
- "-level", "4.1", # Level 4.1 supports 1080p at high bitrates
|
|
|
|
|
|
|
+ "-c:v", "libx264", # Software H.264 for 1080p
|
|
|
|
|
+ "-profile:v", "high", # High profile for better quality
|
|
|
|
|
+ "-preset", "fast", # Faster encoding for 1080p
|
|
|
|
|
+ "-crf", "21", # Good quality for 1080p
|
|
|
|
|
+ "-level", "4.1", # Level 4.1 supports 1080p
|
|
|
]
|
|
]
|
|
|
- logger.info("Using H.264 encoding for 1080p content")
|
|
|
|
|
|
|
+ logger.info("⚠️ Using software H.264 encoding for 1080p content (VAAPI not available)")
|
|
|
else:
|
|
else:
|
|
|
- # Lower resolution: Use H.264 with faster settings
|
|
|
|
|
|
|
+ # Lower resolution: Use software H.264 with faster settings
|
|
|
codec_args = [
|
|
codec_args = [
|
|
|
- "-c:v", "libx264", # H.264 for lower res
|
|
|
|
|
- "-profile:v", "main", # Main profile for lower res
|
|
|
|
|
- "-preset", "fast", # Fast encoding
|
|
|
|
|
- "-crf", "23", # Standard quality
|
|
|
|
|
- "-level", "3.1", # Level 3.1 for lower res
|
|
|
|
|
|
|
+ "-c:v", "libx264", # Software H.264 for lower res
|
|
|
|
|
+ "-profile:v", "main", # Main profile for lower res
|
|
|
|
|
+ "-preset", "fast", # Fast encoding
|
|
|
|
|
+ "-crf", "23", # Standard quality
|
|
|
|
|
+ "-level", "3.1", # Level 3.1 for lower res
|
|
|
]
|
|
]
|
|
|
- logger.info("Using H.264 encoding for lower resolution content")
|
|
|
|
|
|
|
+ logger.info("Using software H.264 encoding for lower resolution content")
|
|
|
|
|
|
|
|
|
|
+ # Add progress reporting to FFmpeg command
|
|
|
command = [
|
|
command = [
|
|
|
FFMPEG_PATH, "-i", input_file,
|
|
FFMPEG_PATH, "-i", input_file,
|
|
|
*codec_args,
|
|
*codec_args,
|
|
|
"-c:a", "aac", # AAC audio codec
|
|
"-c:a", "aac", # AAC audio codec
|
|
|
"-b:a", "192k", # Higher audio bitrate for better quality
|
|
"-b:a", "192k", # Higher audio bitrate for better quality
|
|
|
"-movflags", "+faststart", # Optimize for streaming/quick playback
|
|
"-movflags", "+faststart", # Optimize for streaming/quick playback
|
|
|
|
|
+ "-progress", "pipe:1", # Output progress to stdout
|
|
|
"-y", # Overwrite output file if exists
|
|
"-y", # Overwrite output file if exists
|
|
|
mp4_file
|
|
mp4_file
|
|
|
]
|
|
]
|
|
@@ -715,13 +946,71 @@ def convert_to_apple_tv_compatible(input_file):
|
|
|
logger.debug(f"FFmpeg command: {' '.join(command)}")
|
|
logger.debug(f"FFmpeg command: {' '.join(command)}")
|
|
|
logger.info("Starting conversion... This may take a while for 4K content.")
|
|
logger.info("Starting conversion... This may take a while for 4K content.")
|
|
|
|
|
|
|
|
|
|
+ # Get total duration for progress calculation
|
|
|
|
|
+ duration_cmd = [FFPROBE_PATH, "-v", "quiet", "-show_entries", "format=duration", "-of", "csv=p=0", input_file]
|
|
|
|
|
+ try:
|
|
|
|
|
+ duration_result = subprocess.run(duration_cmd, capture_output=True, text=True, check=True)
|
|
|
|
|
+ total_duration = float(duration_result.stdout.strip())
|
|
|
|
|
+ except:
|
|
|
|
|
+ total_duration = 0
|
|
|
|
|
+ logger.debug("Could not determine video duration for progress tracking")
|
|
|
|
|
+
|
|
|
start_time = time.time()
|
|
start_time = time.time()
|
|
|
- result = subprocess.run(command, check=True, capture_output=True, text=True)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ # Start FFmpeg with progress monitoring
|
|
|
|
|
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, universal_newlines=True)
|
|
|
|
|
+
|
|
|
|
|
+ current_conversion_progress = 0
|
|
|
|
|
+ while True:
|
|
|
|
|
+ output = process.stdout.readline()
|
|
|
|
|
+ if output == '' and process.poll() is not None:
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ if output:
|
|
|
|
|
+ output = output.strip()
|
|
|
|
|
+
|
|
|
|
|
+ # Parse FFmpeg progress output
|
|
|
|
|
+ if output.startswith("out_time_us=") and total_duration > 0:
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Extract time in microseconds
|
|
|
|
|
+ time_us = int(output.split("=")[1])
|
|
|
|
|
+ current_time = time_us / 1000000 # Convert to seconds
|
|
|
|
|
+ progress = min(100, (current_time / total_duration) * 100)
|
|
|
|
|
+
|
|
|
|
|
+ if abs(progress - current_conversion_progress) >= 5: # Update every 5%
|
|
|
|
|
+ current_conversion_progress = progress
|
|
|
|
|
+
|
|
|
|
|
+ # Update Home Assistant with conversion progress
|
|
|
|
|
+ update_homeassistant_stats(
|
|
|
|
|
+ conversion_queue=conversion_queue.qsize(),
|
|
|
|
|
+ active_conversions=conversion_stats['active_conversions'],
|
|
|
|
|
+ current_conversion_progress=current_conversion_progress,
|
|
|
|
|
+ current_conversion_file=os.path.basename(input_file)[:50]
|
|
|
|
|
+ )
|
|
|
|
|
+ logger.info(f"Conversion progress: {current_conversion_progress:.1f}% - {os.path.basename(input_file)}")
|
|
|
|
|
+ except (ValueError, IndexError):
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ # Wait for process to complete
|
|
|
|
|
+ process.wait()
|
|
|
end_time = time.time()
|
|
end_time = time.time()
|
|
|
|
|
|
|
|
|
|
+ if process.returncode != 0:
|
|
|
|
|
+ stderr_output = process.stderr.read()
|
|
|
|
|
+ logger.error(f"FFmpeg failed with return code {process.returncode}: {stderr_output}")
|
|
|
|
|
+ raise subprocess.CalledProcessError(process.returncode, command, stderr=stderr_output)
|
|
|
|
|
+
|
|
|
conversion_time = end_time - start_time
|
|
conversion_time = end_time - start_time
|
|
|
logger.info(f"Conversion completed in {conversion_time:.1f}s: {mp4_file}")
|
|
logger.info(f"Conversion completed in {conversion_time:.1f}s: {mp4_file}")
|
|
|
|
|
|
|
|
|
|
+ # Clear conversion progress
|
|
|
|
|
+ update_homeassistant_stats(
|
|
|
|
|
+ conversion_queue=conversion_queue.qsize(),
|
|
|
|
|
+ active_conversions=conversion_stats['active_conversions'],
|
|
|
|
|
+ current_conversion_progress=100,
|
|
|
|
|
+ current_conversion_file=""
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
# Check if output file was created and has reasonable size
|
|
# Check if output file was created and has reasonable size
|
|
|
if os.path.exists(mp4_file):
|
|
if os.path.exists(mp4_file):
|
|
|
input_size = os.path.getsize(input_file)
|
|
input_size = os.path.getsize(input_file)
|
|
@@ -787,6 +1076,263 @@ def convert_to_apple_tv_compatible(input_file):
|
|
|
|
|
|
|
|
return False
|
|
return False
|
|
|
|
|
|
|
|
|
|
+def convert_to_apple_tv_compatible_with_target(input_file, target_file):
|
|
|
|
|
+ """Convert a video to Apple TV 4K compatible format with Intel VAAPI hardware acceleration and specific target filename."""
|
|
|
|
|
+ global conversion_stats
|
|
|
|
|
+ logger = logging.getLogger(__name__)
|
|
|
|
|
+
|
|
|
|
|
+ # Update conversion stats
|
|
|
|
|
+ conversion_stats['active_conversions'] += 1
|
|
|
|
|
+ conversion_stats['current_conversion'] = os.path.basename(input_file)
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(f"Converting {os.path.basename(input_file)} to Apple TV compatible format with GPU acceleration...")
|
|
|
|
|
+ logger.info(f"Target filename: {os.path.basename(target_file)}")
|
|
|
|
|
+ logger.debug(f"Input file: {input_file}")
|
|
|
|
|
+ logger.debug(f"Target file: {target_file}")
|
|
|
|
|
+
|
|
|
|
|
+ # Use a temporary output file during conversion to avoid conflicts
|
|
|
|
|
+ base_name, ext = os.path.splitext(target_file)
|
|
|
|
|
+ temp_output = f"{base_name}.converting{ext}"
|
|
|
|
|
+
|
|
|
|
|
+ # Test VAAPI support
|
|
|
|
|
+ vaapi_h264_available, vaapi_hevc_available = test_vaapi_support()
|
|
|
|
|
+
|
|
|
|
|
+ # Update Home Assistant
|
|
|
|
|
+ update_homeassistant_stats(
|
|
|
|
|
+ conversion_queue=conversion_queue.qsize(),
|
|
|
|
|
+ active_conversions=conversion_stats['active_conversions']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Detect video properties to optimize conversion
|
|
|
|
|
+ try:
|
|
|
|
|
+ import json
|
|
|
|
|
+ probe_cmd = [
|
|
|
|
|
+ FFPROBE_PATH, "-v", "quiet", "-print_format", "json",
|
|
|
|
|
+ "-show_format", "-show_streams", input_file
|
|
|
|
|
+ ]
|
|
|
|
|
+ logger.debug(f"Probing video with command: {' '.join(probe_cmd)}")
|
|
|
|
|
+ probe_result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
|
|
|
|
|
+ video_info = json.loads(probe_result.stdout)
|
|
|
|
|
+
|
|
|
|
|
+ # Find video stream
|
|
|
|
|
+ video_stream = next((s for s in video_info['streams'] if s['codec_type'] == 'video'), None)
|
|
|
|
|
+ is_4k = video_stream and int(video_stream.get('height', 0)) >= 2160
|
|
|
|
|
+ is_high_res = video_stream and int(video_stream.get('height', 0)) >= 1080
|
|
|
|
|
+
|
|
|
|
|
+ if video_stream:
|
|
|
|
|
+ height = video_stream.get('height', 'unknown')
|
|
|
|
|
+ codec = video_stream.get('codec_name', 'unknown')
|
|
|
|
|
+ bitrate = video_stream.get('bit_rate', 'unknown')
|
|
|
|
|
+ logger.info(f"Video properties: {height}p, codec: {codec}, bitrate: {bitrate}")
|
|
|
|
|
+
|
|
|
|
|
+ except (subprocess.CalledProcessError, json.JSONDecodeError, Exception) as e:
|
|
|
|
|
+ logger.warning(f"Could not probe video properties: {e}, using default settings")
|
|
|
|
|
+ is_4k = False
|
|
|
|
|
+ is_high_res = True
|
|
|
|
|
+
|
|
|
|
|
+ # Intel VAAPI optimized encoding based on source quality and hardware support
|
|
|
|
|
+ if is_4k and vaapi_hevc_available:
|
|
|
|
|
+ # 4K content with VAAPI HEVC: Use hardware-accelerated H.265
|
|
|
|
|
+ codec_args = [
|
|
|
|
|
+ "-vaapi_device", "/dev/dri/renderD128", # VAAPI device
|
|
|
|
|
+ "-vf", "format=nv12,hwupload", # Upload to GPU memory
|
|
|
|
|
+ "-c:v", "hevc_vaapi", # Intel VAAPI HEVC encoder
|
|
|
|
|
+ "-qp", "20", # Quality parameter (lower = better)
|
|
|
|
|
+ "-rc_mode", "CQP", # Constant Quality mode
|
|
|
|
|
+ ]
|
|
|
|
|
+ logger.info("🚀 Using Intel VAAPI HEVC hardware acceleration for 4K content")
|
|
|
|
|
+ elif (is_4k or is_high_res) and vaapi_h264_available:
|
|
|
|
|
+ # 4K/1080p content with VAAPI H.264: Use hardware-accelerated H.264
|
|
|
|
|
+ quality = "20" if is_4k else "22" # Slightly lower quality for 1080p
|
|
|
|
|
+ codec_args = [
|
|
|
|
|
+ "-vaapi_device", "/dev/dri/renderD128", # VAAPI device
|
|
|
|
|
+ "-vf", "format=nv12,hwupload", # Upload to GPU memory
|
|
|
|
|
+ "-c:v", "h264_vaapi", # Intel VAAPI H.264 encoder
|
|
|
|
|
+ "-qp", quality, # Quality parameter
|
|
|
|
|
+ "-rc_mode", "CQP", # Constant Quality mode
|
|
|
|
|
+ "-profile:v", "high", # H.264 High Profile
|
|
|
|
|
+ ]
|
|
|
|
|
+ quality_label = "4K" if is_4k else "1080p"
|
|
|
|
|
+ logger.info(f"🚀 Using Intel VAAPI H.264 hardware acceleration for {quality_label} content")
|
|
|
|
|
+ elif is_4k:
|
|
|
|
|
+ # 4K content without VAAPI: Fall back to software H.265
|
|
|
|
|
+ codec_args = [
|
|
|
|
|
+ "-c:v", "libx265", # Software H.265 for 4K
|
|
|
|
|
+ "-preset", "medium", # Better compression for 4K
|
|
|
|
|
+ "-crf", "20", # Higher quality for 4K content
|
|
|
|
|
+ "-level", "5.1", # Level 5.1 supports 4K
|
|
|
|
|
+ "-pix_fmt", "yuv420p", # Standard compatibility
|
|
|
|
|
+ ]
|
|
|
|
|
+ logger.info("⚠️ Using software H.265 encoding for 4K content (VAAPI not available)")
|
|
|
|
|
+ elif is_high_res:
|
|
|
|
|
+ # 1080p content without VAAPI: Use software H.264
|
|
|
|
|
+ codec_args = [
|
|
|
|
|
+ "-c:v", "libx264", # Software H.264 for 1080p
|
|
|
|
|
+ "-profile:v", "high", # High profile for better quality
|
|
|
|
|
+ "-preset", "fast", # Faster encoding for 1080p
|
|
|
|
|
+ "-crf", "21", # Good quality for 1080p
|
|
|
|
|
+ "-level", "4.1", # Level 4.1 supports 1080p
|
|
|
|
|
+ ]
|
|
|
|
|
+ logger.info("⚠️ Using software H.264 encoding for 1080p content (VAAPI not available)")
|
|
|
|
|
+ else:
|
|
|
|
|
+ # Lower resolution: Use software H.264 with faster settings
|
|
|
|
|
+ codec_args = [
|
|
|
|
|
+ "-c:v", "libx264", # Software H.264 for lower res
|
|
|
|
|
+ "-profile:v", "main", # Main profile for lower res
|
|
|
|
|
+ "-preset", "fast", # Fast encoding
|
|
|
|
|
+ "-crf", "23", # Standard quality
|
|
|
|
|
+ "-level", "3.1", # Level 3.1 for lower res
|
|
|
|
|
+ ]
|
|
|
|
|
+ logger.info("Using software H.264 encoding for lower resolution content")
|
|
|
|
|
+
|
|
|
|
|
+ # Add progress reporting to FFmpeg command
|
|
|
|
|
+ command = [
|
|
|
|
|
+ FFMPEG_PATH, "-i", input_file,
|
|
|
|
|
+ *codec_args,
|
|
|
|
|
+ "-c:a", "aac", # AAC audio codec
|
|
|
|
|
+ "-b:a", "192k", # Higher audio bitrate for better quality
|
|
|
|
|
+ "-movflags", "+faststart", # Optimize for streaming/quick playback
|
|
|
|
|
+ "-progress", "pipe:1", # Output progress to stdout
|
|
|
|
|
+ "-y", # Overwrite output file if exists
|
|
|
|
|
+ temp_output
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ logger.debug(f"FFmpeg command: {' '.join(command)}")
|
|
|
|
|
+ logger.info("Starting conversion... This may take a while for 4K content.")
|
|
|
|
|
+
|
|
|
|
|
+ # Get total duration for progress calculation
|
|
|
|
|
+ duration_cmd = [FFPROBE_PATH, "-v", "quiet", "-show_entries", "format=duration", "-of", "csv=p=0", input_file]
|
|
|
|
|
+ try:
|
|
|
|
|
+ duration_result = subprocess.run(duration_cmd, capture_output=True, text=True, check=True)
|
|
|
|
|
+ total_duration = float(duration_result.stdout.strip())
|
|
|
|
|
+ except:
|
|
|
|
|
+ total_duration = 0
|
|
|
|
|
+ logger.debug("Could not determine video duration for progress tracking")
|
|
|
|
|
+
|
|
|
|
|
+ start_time = time.time()
|
|
|
|
|
+
|
|
|
|
|
+ # Start FFmpeg with progress monitoring
|
|
|
|
|
+ process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, universal_newlines=True)
|
|
|
|
|
+
|
|
|
|
|
+ current_conversion_progress = 0
|
|
|
|
|
+ while True:
|
|
|
|
|
+ output = process.stdout.readline()
|
|
|
|
|
+ if output == '' and process.poll() is not None:
|
|
|
|
|
+ break
|
|
|
|
|
+
|
|
|
|
|
+ if output:
|
|
|
|
|
+ output = output.strip()
|
|
|
|
|
+
|
|
|
|
|
+ # Parse FFmpeg progress output
|
|
|
|
|
+ if output.startswith("out_time_us=") and total_duration > 0:
|
|
|
|
|
+ try:
|
|
|
|
|
+ # Extract time in microseconds
|
|
|
|
|
+ time_us = int(output.split("=")[1])
|
|
|
|
|
+ current_time = time_us / 1000000 # Convert to seconds
|
|
|
|
|
+ progress = min(100, (current_time / total_duration) * 100)
|
|
|
|
|
+
|
|
|
|
|
+ if abs(progress - current_conversion_progress) >= 5: # Update every 5%
|
|
|
|
|
+ current_conversion_progress = progress
|
|
|
|
|
+
|
|
|
|
|
+ # Update Home Assistant with conversion progress
|
|
|
|
|
+ update_homeassistant_stats(
|
|
|
|
|
+ conversion_queue=conversion_queue.qsize(),
|
|
|
|
|
+ active_conversions=conversion_stats['active_conversions'],
|
|
|
|
|
+ current_conversion_progress=current_conversion_progress,
|
|
|
|
|
+ current_conversion_file=os.path.basename(input_file)[:50]
|
|
|
|
|
+ )
|
|
|
|
|
+ logger.info(f"Conversion progress: {current_conversion_progress:.1f}% - {os.path.basename(input_file)}")
|
|
|
|
|
+ except (ValueError, IndexError):
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+ # Wait for process to complete
|
|
|
|
|
+ process.wait()
|
|
|
|
|
+ end_time = time.time()
|
|
|
|
|
+
|
|
|
|
|
+ if process.returncode != 0:
|
|
|
|
|
+ stderr_output = process.stderr.read()
|
|
|
|
|
+ logger.error(f"FFmpeg failed with return code {process.returncode}: {stderr_output}")
|
|
|
|
|
+ raise subprocess.CalledProcessError(process.returncode, command, stderr=stderr_output)
|
|
|
|
|
+
|
|
|
|
|
+ conversion_time = end_time - start_time
|
|
|
|
|
+ logger.info(f"Conversion completed in {conversion_time:.1f}s")
|
|
|
|
|
+
|
|
|
|
|
+ # Clear conversion progress
|
|
|
|
|
+ update_homeassistant_stats(
|
|
|
|
|
+ conversion_queue=conversion_queue.qsize(),
|
|
|
|
|
+ active_conversions=conversion_stats['active_conversions'],
|
|
|
|
|
+ current_conversion_progress=100,
|
|
|
|
|
+ current_conversion_file=""
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # Check if output file was created and has reasonable size
|
|
|
|
|
+ if os.path.exists(temp_output):
|
|
|
|
|
+ input_size = os.path.getsize(input_file)
|
|
|
|
|
+ output_size = os.path.getsize(temp_output)
|
|
|
|
|
+ size_ratio = output_size / input_size if input_size > 0 else 0
|
|
|
|
|
+
|
|
|
|
|
+ logger.info(f"File sizes - Input: {input_size // (1024*1024):.1f}MB, Output: {output_size // (1024*1024):.1f}MB (ratio: {size_ratio:.2f})")
|
|
|
|
|
+
|
|
|
|
|
+ # Only replace original if output seems reasonable
|
|
|
|
|
+ if output_size > input_size * 0.1:
|
|
|
|
|
+ # Move temporary file to final target location
|
|
|
|
|
+ os.rename(temp_output, target_file)
|
|
|
|
|
+ # Remove the original incompatible file
|
|
|
|
|
+ os.remove(input_file)
|
|
|
|
|
+ logger.info(f"✅ Conversion successful: {os.path.basename(input_file)} → {os.path.basename(target_file)}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ logger.warning(f"Output file seems too small, keeping original: {os.path.basename(input_file)}")
|
|
|
|
|
+ if os.path.exists(temp_output):
|
|
|
|
|
+ os.remove(temp_output)
|
|
|
|
|
+
|
|
|
|
|
+ # Update completion stats
|
|
|
|
|
+ conversion_stats['active_conversions'] -= 1
|
|
|
|
|
+ conversion_stats['completed_conversions'] += 1
|
|
|
|
|
+
|
|
|
|
|
+ # Update Home Assistant
|
|
|
|
|
+ update_homeassistant_stats(
|
|
|
|
|
+ conversion_queue=conversion_queue.qsize(),
|
|
|
|
|
+ active_conversions=conversion_stats['active_conversions']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ except subprocess.CalledProcessError as e:
|
|
|
|
|
+ conversion_stats['active_conversions'] -= 1
|
|
|
|
|
+ logger.error(f"Conversion failed for {os.path.basename(input_file)}: {e}")
|
|
|
|
|
+ if e.stderr:
|
|
|
|
|
+ logger.debug(f"FFmpeg error output: {e.stderr}")
|
|
|
|
|
+
|
|
|
|
|
+ # Clean up temporary file if it exists
|
|
|
|
|
+ if os.path.exists(temp_output):
|
|
|
|
|
+ os.remove(temp_output)
|
|
|
|
|
+ logger.debug(f"Cleaned up temporary file: {temp_output}")
|
|
|
|
|
+
|
|
|
|
|
+ # Update Home Assistant
|
|
|
|
|
+ update_homeassistant_stats(
|
|
|
|
|
+ conversion_queue=conversion_queue.qsize(),
|
|
|
|
|
+ active_conversions=conversion_stats['active_conversions']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return False
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ conversion_stats['active_conversions'] -= 1
|
|
|
|
|
+ logger.error(f"Unexpected error during conversion of {os.path.basename(input_file)}: {e}")
|
|
|
|
|
+
|
|
|
|
|
+ # Clean up temporary file if it exists
|
|
|
|
|
+ if os.path.exists(temp_output):
|
|
|
|
|
+ os.remove(temp_output)
|
|
|
|
|
+ logger.debug(f"Cleaned up temporary file: {temp_output}")
|
|
|
|
|
+
|
|
|
|
|
+ # Update Home Assistant
|
|
|
|
|
+ update_homeassistant_stats(
|
|
|
|
|
+ conversion_queue=conversion_queue.qsize(),
|
|
|
|
|
+ active_conversions=conversion_stats['active_conversions']
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
# Temporarily commented out video conversion functionality
|
|
# Temporarily commented out video conversion functionality
|
|
|
# def convert_to_mp4(input_file):
|
|
# def convert_to_mp4(input_file):
|
|
|
# """Convert a video to mp4 and delete the original file."""
|
|
# """Convert a video to mp4 and delete the original file."""
|