|
|
@@ -1,7 +1,6 @@
|
|
|
import os
|
|
|
import time
|
|
|
-import datetime
|
|
|
-from datetime import datetime, time as dt_time
|
|
|
+from datetime import datetime, time as dt_time, timedelta
|
|
|
import pickle
|
|
|
import threading
|
|
|
import queue
|
|
|
@@ -204,6 +203,7 @@ FFMPEG_PATH = "/usr/bin/ffmpeg"
|
|
|
# Global conversion queue and executor
|
|
|
conversion_queue = queue.Queue(maxsize=CONVERSION_QUEUE_SIZE)
|
|
|
conversion_executor = ThreadPoolExecutor(max_workers=MAX_CONCURRENT_CONVERSIONS, thread_name_prefix="VideoConverter")
|
|
|
+conversion_worker_stop = False # Flag to signal conversion worker to stop
|
|
|
|
|
|
# Progress tracking
|
|
|
download_stats = {
|
|
|
@@ -294,7 +294,7 @@ def get_recent_videos(youtube, channel_id):
|
|
|
"""Fetch videos from a channel published in the last `DAYS_BACK` days."""
|
|
|
logger = logging.getLogger(__name__)
|
|
|
recent_videos = []
|
|
|
- published_after = (datetime.datetime.now() - datetime.timedelta(days=DAYS_BACK)).isoformat("T") + "Z"
|
|
|
+ published_after = (datetime.now() - timedelta(days=DAYS_BACK)).isoformat("T") + "Z"
|
|
|
|
|
|
try:
|
|
|
request = youtube.search().list(
|
|
|
@@ -718,13 +718,14 @@ def print_queue_status():
|
|
|
|
|
|
def process_conversion_queue():
|
|
|
"""Background worker to process the conversion queue."""
|
|
|
+ global conversion_worker_stop
|
|
|
logger = logging.getLogger(__name__)
|
|
|
logger.info("Conversion queue worker started")
|
|
|
|
|
|
- while True:
|
|
|
+ while not conversion_worker_stop:
|
|
|
try:
|
|
|
# Get item from queue (blocks until available)
|
|
|
- queue_item = conversion_queue.get(timeout=30)
|
|
|
+ queue_item = conversion_queue.get(timeout=5) # Reduced timeout to check stop flag more often
|
|
|
|
|
|
# Handle both old format (priority, input_file) and new format (priority, input_file, target_file)
|
|
|
if len(queue_item) == 2:
|
|
|
@@ -761,7 +762,9 @@ def process_conversion_queue():
|
|
|
conversion_queue.task_done()
|
|
|
|
|
|
except queue.Empty:
|
|
|
- # No items in queue for 30 seconds, continue waiting
|
|
|
+ # No items in queue for 5 seconds, check if we should stop
|
|
|
+ if conversion_worker_stop:
|
|
|
+ break
|
|
|
logger.debug("Conversion queue empty, waiting for new items...")
|
|
|
continue
|
|
|
except Exception as e:
|
|
|
@@ -770,6 +773,8 @@ def process_conversion_queue():
|
|
|
conversion_queue.task_done()
|
|
|
except:
|
|
|
pass
|
|
|
+
|
|
|
+ logger.info("Conversion queue worker stopped")
|
|
|
|
|
|
def sanitize_filename(filename):
|
|
|
"""Sanitize a filename to remove invalid characters."""
|
|
|
@@ -781,7 +786,7 @@ def sanitize_filename(filename):
|
|
|
return sanitized
|
|
|
|
|
|
def test_vaapi_support():
|
|
|
- """Test if Intel VAAPI hardware acceleration is available."""
|
|
|
+ """Test if Intel VAAPI hardware acceleration is available for encoding and decoding."""
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
try:
|
|
|
@@ -799,19 +804,72 @@ def test_vaapi_support():
|
|
|
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
|
|
|
+ # Test VP9 VAAPI decoding capability - create a test VP9 file and try hardware decoding
|
|
|
+ vp9_vaapi_decode_available = False
|
|
|
+ try:
|
|
|
+ # Create a small VP9 test file
|
|
|
+ create_vp9_cmd = [FFMPEG_PATH, "-hide_banner", "-f", "lavfi", "-i", "testsrc=duration=1:size=320x240:rate=1",
|
|
|
+ "-c:v", "libvpx-vp9", "-frames:v", "5", "-f", "webm", "/tmp/vp9_test.webm"]
|
|
|
+ result = subprocess.run(create_vp9_cmd, capture_output=True, text=True, timeout=15)
|
|
|
+
|
|
|
+ if result.returncode == 0:
|
|
|
+ # Test VP9 hardware decoding with VAAPI
|
|
|
+ decode_vp9_cmd = [FFMPEG_PATH, "-hide_banner", "-hwaccel", "vaapi", "-hwaccel_device", "/dev/dri/renderD128",
|
|
|
+ "-i", "/tmp/vp9_test.webm", "-f", "null", "-"]
|
|
|
+ result = subprocess.run(decode_vp9_cmd, capture_output=True, text=True, timeout=10)
|
|
|
+ vp9_vaapi_decode_available = result.returncode == 0
|
|
|
+
|
|
|
+ # Clean up test file
|
|
|
+ try:
|
|
|
+ os.remove("/tmp/vp9_test.webm")
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ # Test H.264 VAAPI decoding - create a test file and try to decode it
|
|
|
+ h264_vaapi_decode_available = False
|
|
|
+ try:
|
|
|
+ # Create a small test H.264 file
|
|
|
+ create_test_cmd = [FFMPEG_PATH, "-hide_banner", "-f", "lavfi", "-i", "testsrc=duration=1:size=320x240:rate=1",
|
|
|
+ "-c:v", "libx264", "-frames:v", "5", "-f", "mp4", "/tmp/h264_test.mp4"]
|
|
|
+ subprocess.run(create_test_cmd, capture_output=True, text=True, timeout=10, check=True)
|
|
|
+
|
|
|
+ # Test hardware decoding
|
|
|
+ decode_test_cmd = [FFMPEG_PATH, "-hide_banner", "-hwaccel", "vaapi", "-hwaccel_device", "/dev/dri/renderD128",
|
|
|
+ "-i", "/tmp/h264_test.mp4", "-f", "null", "-"]
|
|
|
+ result = subprocess.run(decode_test_cmd, capture_output=True, text=True, timeout=10)
|
|
|
+ h264_vaapi_decode_available = result.returncode == 0
|
|
|
+
|
|
|
+ # Clean up test file
|
|
|
+ try:
|
|
|
+ os.remove("/tmp/h264_test.mp4")
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+ except:
|
|
|
+ pass
|
|
|
+
|
|
|
+ # Log results with comprehensive hardware support info
|
|
|
+ hw_support = []
|
|
|
+ if h264_vaapi_available:
|
|
|
+ hw_support.append("H.264 encode")
|
|
|
+ if hevc_vaapi_available:
|
|
|
+ hw_support.append("HEVC encode")
|
|
|
+ if h264_vaapi_decode_available:
|
|
|
+ hw_support.append("H.264 decode")
|
|
|
+ if vp9_vaapi_decode_available:
|
|
|
+ hw_support.append("VP9 decode")
|
|
|
+
|
|
|
+ if hw_support:
|
|
|
+ logger.info(f"🚀 Intel VAAPI hardware acceleration available: {', '.join(hw_support)}")
|
|
|
else:
|
|
|
- logger.info("⚠️ Intel VAAPI hardware acceleration not available, falling back to software encoding")
|
|
|
- return False, False
|
|
|
+ logger.info("⚠️ Intel VAAPI hardware acceleration not available, falling back to software")
|
|
|
+
|
|
|
+ return (h264_vaapi_available, hevc_vaapi_available, h264_vaapi_decode_available, vp9_vaapi_decode_available)
|
|
|
|
|
|
except Exception as e:
|
|
|
logger.warning(f"Could not test VAAPI support: {e}")
|
|
|
- return False, False
|
|
|
+ return False, False, False, False
|
|
|
|
|
|
def convert_to_apple_tv_compatible(input_file):
|
|
|
"""Convert a video to Apple TV 4K compatible format with Intel VAAPI hardware acceleration."""
|
|
|
@@ -839,7 +897,7 @@ def convert_to_apple_tv_compatible(input_file):
|
|
|
logger.debug(f"Output file: {mp4_file}")
|
|
|
|
|
|
# Test VAAPI support
|
|
|
- vaapi_h264_available, vaapi_hevc_available = test_vaapi_support()
|
|
|
+ h264_encode, hevc_encode, h264_decode, vp9_decode = test_vaapi_support()
|
|
|
|
|
|
# Update Home Assistant
|
|
|
update_homeassistant_stats(
|
|
|
@@ -862,6 +920,7 @@ def convert_to_apple_tv_compatible(input_file):
|
|
|
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
|
|
|
+ input_codec = video_stream.get('codec_name', 'unknown').lower() if video_stream else 'unknown'
|
|
|
|
|
|
if video_stream:
|
|
|
height = video_stream.get('height', 'unknown')
|
|
|
@@ -873,24 +932,41 @@ def convert_to_apple_tv_compatible(input_file):
|
|
|
logger.warning(f"Could not probe video properties: {e}, using default settings")
|
|
|
is_4k = False
|
|
|
is_high_res = True
|
|
|
+ input_codec = 'unknown'
|
|
|
+
|
|
|
+ # Determine optimal hardware decoding strategy based on input codec
|
|
|
+ hwaccel_args = []
|
|
|
+ filter_args = []
|
|
|
+
|
|
|
+ if input_codec in ['vp9', 'vp09'] and vp9_decode:
|
|
|
+ # Use Intel VAAPI for VP9 hardware decoding
|
|
|
+ hwaccel_args = ["-hwaccel", "vaapi", "-hwaccel_device", "/dev/dri/renderD128"]
|
|
|
+ filter_args = ["-vaapi_device", "/dev/dri/renderD128", "-vf", "format=nv12,hwupload"]
|
|
|
+ logger.info(f"🚀 Using Intel VAAPI hardware decoding for VP9 input")
|
|
|
+ elif input_codec in ['h264', 'avc'] and h264_decode:
|
|
|
+ # Use Intel VAAPI for H.264 hardware decoding (decode + encode on same device)
|
|
|
+ hwaccel_args = ["-hwaccel", "vaapi", "-hwaccel_device", "/dev/dri/renderD128"]
|
|
|
+ filter_args = ["-vaapi_device", "/dev/dri/renderD128", "-vf", "format=nv12,hwupload"]
|
|
|
+ logger.info(f"🚀 Using Intel VAAPI hardware decoding for H.264 input")
|
|
|
+ else:
|
|
|
+ # Fallback: CPU decode with GPU upload for encoding compatibility
|
|
|
+ filter_args = ["-vaapi_device", "/dev/dri/renderD128", "-vf", "format=nv12,hwupload"]
|
|
|
+ logger.info(f"Using software decoding with GPU upload for {input_codec} input")
|
|
|
|
|
|
# Intel VAAPI optimized encoding based on source quality and hardware support:
|
|
|
- if is_4k and vaapi_hevc_available:
|
|
|
+ if is_4k and hevc_encode:
|
|
|
# 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:
|
|
|
+ elif (is_4k or is_high_res) and h264_encode:
|
|
|
# 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
|
|
|
+ *filter_args,
|
|
|
"-c:v", "h264_vaapi", # Intel VAAPI H.264 encoder
|
|
|
"-qp", quality, # Quality parameter
|
|
|
"-rc_mode", "CQP", # Constant Quality mode
|
|
|
@@ -931,8 +1007,11 @@ def convert_to_apple_tv_compatible(input_file):
|
|
|
|
|
|
# Add progress reporting to FFmpeg command
|
|
|
command = [
|
|
|
- FFMPEG_PATH, "-i", input_file,
|
|
|
- *codec_args,
|
|
|
+ FFMPEG_PATH,
|
|
|
+ *hwaccel_args, # Hardware decode arguments (if any)
|
|
|
+ "-i", input_file,
|
|
|
+ *filter_args, # Video filter arguments (if any)
|
|
|
+ *codec_args, # Video codec args (encode)
|
|
|
"-c:a", "aac", # AAC audio codec
|
|
|
"-b:a", "192k", # Higher audio bitrate for better quality
|
|
|
"-movflags", "+faststart", # Optimize for streaming/quick playback
|
|
|
@@ -1094,7 +1173,7 @@ def convert_to_apple_tv_compatible_with_target(input_file, target_file):
|
|
|
temp_output = f"{base_name}.converting{ext}"
|
|
|
|
|
|
# Test VAAPI support
|
|
|
- vaapi_h264_available, vaapi_hevc_available = test_vaapi_support()
|
|
|
+ h264_encode, hevc_encode, h264_decode, vp9_decode = test_vaapi_support()
|
|
|
|
|
|
# Update Home Assistant
|
|
|
update_homeassistant_stats(
|
|
|
@@ -1117,6 +1196,7 @@ def convert_to_apple_tv_compatible_with_target(input_file, target_file):
|
|
|
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
|
|
|
+ input_codec = video_stream.get('codec_name', 'unknown').lower() if video_stream else 'unknown'
|
|
|
|
|
|
if video_stream:
|
|
|
height = video_stream.get('height', 'unknown')
|
|
|
@@ -1128,24 +1208,40 @@ def convert_to_apple_tv_compatible_with_target(input_file, target_file):
|
|
|
logger.warning(f"Could not probe video properties: {e}, using default settings")
|
|
|
is_4k = False
|
|
|
is_high_res = True
|
|
|
+ input_codec = 'unknown'
|
|
|
+
|
|
|
+ # Determine optimal hardware decoding strategy based on input codec
|
|
|
+ hwaccel_args = []
|
|
|
+ filter_args = []
|
|
|
+
|
|
|
+ if input_codec in ['vp9', 'vp09'] and vp9_decode:
|
|
|
+ # Use Intel VAAPI for VP9 hardware decoding
|
|
|
+ hwaccel_args = ["-hwaccel", "vaapi", "-hwaccel_device", "/dev/dri/renderD128"]
|
|
|
+ filter_args = ["-vaapi_device", "/dev/dri/renderD128", "-vf", "format=nv12,hwupload"]
|
|
|
+ logger.info(f"🚀 Using Intel VAAPI hardware decoding for VP9 input")
|
|
|
+ elif input_codec in ['h264', 'avc'] and h264_decode:
|
|
|
+ # Use Intel VAAPI for H.264 hardware decoding (decode + encode on same device)
|
|
|
+ hwaccel_args = ["-hwaccel", "vaapi", "-hwaccel_device", "/dev/dri/renderD128"]
|
|
|
+ filter_args = ["-vaapi_device", "/dev/dri/renderD128", "-vf", "format=nv12,hwupload"]
|
|
|
+ logger.info(f"🚀 Using Intel VAAPI hardware decoding for H.264 input")
|
|
|
+ else:
|
|
|
+ # Fallback: CPU decode with GPU upload for encoding compatibility
|
|
|
+ filter_args = ["-vaapi_device", "/dev/dri/renderD128", "-vf", "format=nv12,hwupload"]
|
|
|
+ logger.info(f"Using software decoding with GPU upload for {input_codec} input")
|
|
|
|
|
|
# Intel VAAPI optimized encoding based on source quality and hardware support
|
|
|
- if is_4k and vaapi_hevc_available:
|
|
|
+ if is_4k and hevc_encode:
|
|
|
# 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:
|
|
|
+ elif (is_4k or is_high_res) and h264_encode:
|
|
|
# 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
|
|
|
@@ -1186,7 +1282,10 @@ def convert_to_apple_tv_compatible_with_target(input_file, target_file):
|
|
|
|
|
|
# Add progress reporting to FFmpeg command
|
|
|
command = [
|
|
|
- FFMPEG_PATH, "-i", input_file,
|
|
|
+ FFMPEG_PATH,
|
|
|
+ *hwaccel_args, # Hardware decode arguments (if any)
|
|
|
+ "-i", input_file,
|
|
|
+ *filter_args, # Video filter arguments (if any)
|
|
|
*codec_args,
|
|
|
"-c:a", "aac", # AAC audio codec
|
|
|
"-b:a", "192k", # Higher audio bitrate for better quality
|
|
|
@@ -1435,6 +1534,29 @@ def check_exit_time():
|
|
|
print("Current time exceeds exit time. Terminating script.")
|
|
|
sys.exit(0)
|
|
|
|
|
|
+def cleanup_conversion_worker():
|
|
|
+ """Clean up the conversion worker thread and wait for pending conversions."""
|
|
|
+ global conversion_worker_stop
|
|
|
+ logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+ logger.info("Cleaning up conversion worker...")
|
|
|
+
|
|
|
+ # Signal the worker to stop
|
|
|
+ conversion_worker_stop = True
|
|
|
+
|
|
|
+ # Wait for current conversions to finish if any are running
|
|
|
+ try:
|
|
|
+ if not conversion_queue.empty():
|
|
|
+ logger.info(f"Waiting for {conversion_queue.qsize()} pending conversions to complete...")
|
|
|
+ conversion_queue.join()
|
|
|
+ logger.info("All pending conversions completed")
|
|
|
+ else:
|
|
|
+ logger.info("No pending conversions")
|
|
|
+ except Exception as e:
|
|
|
+ logger.error(f"Error waiting for conversions to complete: {e}")
|
|
|
+
|
|
|
+ logger.info("Conversion worker cleanup completed")
|
|
|
+
|
|
|
def main():
|
|
|
global download_stats
|
|
|
logger = logging.getLogger(__name__)
|
|
|
@@ -1455,7 +1577,7 @@ def main():
|
|
|
logger.info(f"Max concurrent conversions: {MAX_CONCURRENT_CONVERSIONS}")
|
|
|
|
|
|
# Start background conversion worker
|
|
|
- conversion_worker = threading.Thread(target=process_conversion_queue, daemon=True, name="ConversionQueueWorker")
|
|
|
+ conversion_worker = threading.Thread(target=process_conversion_queue, daemon=False, name="ConversionQueueWorker")
|
|
|
conversion_worker.start()
|
|
|
logger.debug("Conversion queue worker thread started")
|
|
|
|
|
|
@@ -1596,8 +1718,15 @@ def main():
|
|
|
|
|
|
# Wait for all conversions to complete
|
|
|
logger.info("Waiting for all conversions to complete...")
|
|
|
- conversion_executor.shutdown(wait=True)
|
|
|
- logger.info("All conversions completed!")
|
|
|
+ try:
|
|
|
+ # Wait for all queued conversions to finish
|
|
|
+ conversion_queue.join()
|
|
|
+ logger.info("All conversions completed!")
|
|
|
+ except KeyboardInterrupt:
|
|
|
+ logger.info("Interrupted while waiting for conversions")
|
|
|
+ finally:
|
|
|
+ # Clean up conversion worker
|
|
|
+ cleanup_conversion_worker()
|
|
|
|
|
|
# Final Home Assistant update
|
|
|
update_homeassistant_stats(
|
|
|
@@ -1791,15 +1920,19 @@ if __name__ == "__main__":
|
|
|
schedule_downloads()
|
|
|
except KeyboardInterrupt:
|
|
|
logger.info("Service interrupted by user")
|
|
|
+ cleanup_conversion_worker()
|
|
|
except Exception as e:
|
|
|
logger.error(f"Service error: {e}", exc_info=True)
|
|
|
+ cleanup_conversion_worker()
|
|
|
else:
|
|
|
logger.info("Running YouTube Predownloader once")
|
|
|
try:
|
|
|
main()
|
|
|
except KeyboardInterrupt:
|
|
|
logger.info("Process interrupted by user")
|
|
|
+ cleanup_conversion_worker()
|
|
|
except Exception as e:
|
|
|
logger.error(f"Process error: {e}", exc_info=True)
|
|
|
+ cleanup_conversion_worker()
|
|
|
finally:
|
|
|
logger.info("Process finished")
|