Murad Jabir 1 рік тому
батько
коміт
dd2ba78b10
4 змінених файлів з 744 додано та 55 видалено
  1. 43 7
      HOMEASSISTANT_SETUP.md
  2. 44 0
      homeassistant_integration.py
  3. 594 48
      main.py
  4. 63 0
      setup_gpu.sh

+ 43 - 7
HOMEASSISTANT_SETUP.md

@@ -10,11 +10,15 @@ This integration provides real-time monitoring of your YouTube predownloader thr
 2. **Conversion Queue Length** - Number of videos waiting for conversion  
 3. **Download Progress** - Overall download completion percentage
 4. **Conversion Progress** - Individual conversion progress
-5. **Download Speed** - Current download speed in MB/s
-6. **CPU Usage** - Process CPU utilization percentage
-7. **Memory Usage** - Process memory utilization (% and MB)
-8. **Active Downloads** - Number of currently downloading videos
-9. **Active Conversions** - Number of currently converting videos
+5. **Current Download Progress** - Real-time progress of active download (0-100%)
+6. **Current Download File** - Name of currently downloading file
+7. **Current Conversion Progress** - Real-time progress of active conversion (0-100%)
+8. **Current Conversion File** - Name of currently converting file
+9. **Download Speed** - Current download speed in MB/s
+10. **CPU Usage** - Process CPU utilization percentage
+11. **Memory Usage** - Process memory utilization (% and MB)
+12. **Active Downloads** - Number of currently downloading videos
+13. **Active Conversions** - Number of currently converting videos
 
 ### 🔧 **Setup Instructions:**
 
@@ -72,6 +76,10 @@ entities:
   - sensor.youtube_conversion_queue
   - sensor.youtube_download_progress
   - sensor.youtube_conversion_progress
+  - sensor.youtube_current_download_progress
+  - sensor.youtube_current_download_file
+  - sensor.youtube_current_conversion_progress
+  - sensor.youtube_current_conversion_file
   - sensor.youtube_download_speed
   - sensor.youtube_cpu_usage
   - sensor.youtube_memory_usage
@@ -81,13 +89,41 @@ entities:
 
 ### 📈 **Example Dashboard Cards:**
 
-#### **Progress Card:**
+#### **Progress Cards:**
+```yaml
+type: gauge
+entity: sensor.youtube_current_download_progress
+min: 0
+max: 100
+name: Current Download
+```
+
+```yaml
+type: gauge
+entity: sensor.youtube_current_conversion_progress
+min: 0
+max: 100
+name: Current Conversion
+```
+
+#### **Current Operation Cards:**
+```yaml
+type: markdown
+content: |
+  **Currently Downloading:**
+  {{ states('sensor.youtube_current_download_file') }}
+  
+  **Currently Converting:**
+  {{ states('sensor.youtube_current_conversion_file') }}
+```
+
+#### **Overall Progress Card:**
 ```yaml
 type: gauge
 entity: sensor.youtube_download_progress
 min: 0
 max: 100
-name: Download Progress
+name: Overall Progress
 ```
 
 #### **Queue Status:**

+ 44 - 0
homeassistant_integration.py

@@ -35,6 +35,10 @@ class HomeAssistantIntegration:
             'conversion_queue_length': 0,
             'download_progress': 0,
             'conversion_progress': 0,
+            'current_download_progress': 0,
+            'current_download_file': '',
+            'current_conversion_progress': 0,
+            'current_conversion_file': '',
             'download_speed': 0,
             'cpu_usage': 0,
             'memory_usage': 0,
@@ -119,6 +123,20 @@ class HomeAssistantIntegration:
                 'icon': 'mdi:progress-download',
                 'device_class': None
             },
+            {
+                'name': 'YouTube Current Download Progress',
+                'key': 'current_download_progress',
+                'unit': '%',
+                'icon': 'mdi:download',
+                'device_class': None
+            },
+            {
+                'name': 'YouTube Current Download File',
+                'key': 'current_download_file',
+                'unit': None,
+                'icon': 'mdi:file-video',
+                'device_class': None
+            },
             {
                 'name': 'YouTube Conversion Progress',
                 'key': 'conversion_progress',
@@ -126,6 +144,20 @@ class HomeAssistantIntegration:
                 'icon': 'mdi:progress-wrench',
                 'device_class': None
             },
+            {
+                'name': 'YouTube Current Conversion Progress',
+                'key': 'current_conversion_progress',
+                'unit': '%',
+                'icon': 'mdi:cog',
+                'device_class': None
+            },
+            {
+                'name': 'YouTube Current Conversion File',
+                'key': 'current_conversion_file',
+                'unit': None,
+                'icon': 'mdi:file-cog',
+                'device_class': None
+            },
             {
                 'name': 'YouTube Download Speed',
                 'key': 'download_speed',
@@ -232,6 +264,8 @@ class HomeAssistantIntegration:
     
     def update_stats(self, download_queue=0, conversion_queue=0, download_progress=0, 
                     conversion_progress=0, download_speed=0, active_downloads=0, active_conversions=0,
+                    current_download_progress=None, current_download_file=None,
+                    current_conversion_progress=None, current_conversion_file=None,
                     service_status=None, next_run_time=None, current_run_start=None, last_run_completed=None):
         """Update statistics"""
         # Update provided stats
@@ -246,6 +280,16 @@ class HomeAssistantIntegration:
             'last_updated': datetime.now().isoformat()
         }
         
+        # Add current operation progress if provided
+        if current_download_progress is not None:
+            update_dict['current_download_progress'] = round(current_download_progress, 1)
+        if current_download_file is not None:
+            update_dict['current_download_file'] = str(current_download_file)[:100]  # Limit length
+        if current_conversion_progress is not None:
+            update_dict['current_conversion_progress'] = round(current_conversion_progress, 1)
+        if current_conversion_file is not None:
+            update_dict['current_conversion_file'] = str(current_conversion_file)[:100]  # Limit length
+        
         # Add optional scheduling stats
         if service_status is not None:
             update_dict['service_status'] = service_status

+ 594 - 48
main.py

@@ -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.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)
         if logger.isEnabledFor(logging.DEBUG):
             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",
             "--write-info-json",  # Write metadata for analysis
             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)}")
@@ -425,15 +469,73 @@ def download_video(video_id, output_path, video_title="Unknown"):
         try:
             start_time = time.time()
             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()
             
+            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
             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
             try:
@@ -477,29 +579,55 @@ def download_video(video_id, output_path, video_title="Unknown"):
                                 file_size_mb = file_size / (1024 * 1024)
                                 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
                                 needs_conversion = False
                                 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
-                                    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:
                                     logger.info(f"Queuing for conversion: {conversion_reason}")
                                     # Determine priority based on video resolution
                                     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()
                                 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:
                             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}")
         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():
     """Get current conversion queue status."""
     queue_size = conversion_queue.qsize()
@@ -579,7 +725,18 @@ def process_conversion_queue():
     while True:
         try:
             # 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
             if not os.path.exists(input_file):
@@ -587,10 +744,14 @@ def process_conversion_queue():
                 conversion_queue.task_done()
                 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:
                 logger.info(f"Conversion completed successfully: {os.path.basename(input_file)}")
@@ -611,8 +772,50 @@ def process_conversion_queue():
             except:
                 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):
-    """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
     logger = logging.getLogger(__name__)
     
@@ -632,10 +835,13 @@ def convert_to_apple_tv_compatible(input_file):
     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...")
+    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"Output file: {mp4_file}")
     
+    # Test VAAPI support
+    vaapi_h264_available, vaapi_hevc_available = test_vaapi_support()
+    
     # Update Home Assistant
     update_homeassistant_stats(
         conversion_queue=conversion_queue.qsize(),
@@ -669,44 +875,69 @@ def convert_to_apple_tv_compatible(input_file):
         is_4k = False
         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 = [
-            "-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:
-        # 1080p content: Use H.264 for maximum compatibility
+        # 1080p content without VAAPI: Use software H.264
         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:
-        # Lower resolution: Use H.264 with faster settings
+        # Lower resolution: Use software H.264 with faster settings
         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 = [
         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
         mp4_file
     ]
@@ -715,13 +946,71 @@ def convert_to_apple_tv_compatible(input_file):
         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()
-        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()
         
+        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: {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
         if os.path.exists(mp4_file):
             input_size = os.path.getsize(input_file)
@@ -787,6 +1076,263 @@ def convert_to_apple_tv_compatible(input_file):
         
         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
 # def convert_to_mp4(input_file):
 #     """Convert a video to mp4 and delete the original file."""

+ 63 - 0
setup_gpu.sh

@@ -0,0 +1,63 @@
+#!/bin/bash
+# GPU Setup Script for YouTube Predownloader
+# This script sets up Intel GPU hardware acceleration
+
+echo "🚀 Setting up Intel GPU hardware acceleration..."
+
+# Check if user is in video and render groups
+if ! groups "$USER" | grep -q "\bvideo\b"; then
+    echo "Adding $USER to video group..."
+    sudo usermod -a -G video "$USER"
+    NEED_RELOGIN=true
+fi
+
+if ! groups "$USER" | grep -q "\brender\b"; then
+    echo "Adding $USER to render group..."
+    sudo usermod -a -G render "$USER"
+    NEED_RELOGIN=true
+fi
+
+# Fix GPU device permissions
+echo "Setting GPU device permissions..."
+sudo chmod 666 /dev/dri/renderD128
+sudo chmod 666 /dev/dri/card1
+
+# Create udev rule for persistent permissions
+echo "Creating udev rule for persistent GPU permissions..."
+sudo tee /etc/udev/rules.d/99-gpu-permissions.rules > /dev/null << 'EOF'
+# Intel GPU permissions for hardware acceleration
+SUBSYSTEM=="drm", KERNEL=="renderD*", GROUP="render", MODE="0666"
+SUBSYSTEM=="drm", KERNEL=="card*", GROUP="video", MODE="0666"
+EOF
+
+# Reload udev rules
+sudo udevadm control --reload-rules
+sudo udevadm trigger
+
+# Test hardware acceleration
+echo "Testing hardware acceleration..."
+if vainfo --display drm --device /dev/dri/renderD128 > /dev/null 2>&1; then
+    echo "✅ Intel VAAPI hardware acceleration is working!"
+else
+    echo "❌ Hardware acceleration test failed"
+fi
+
+# Test FFmpeg VAAPI
+echo "Testing FFmpeg VAAPI support..."
+if ffmpeg -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 - > /dev/null 2>&1; then
+    echo "✅ FFmpeg VAAPI H.264 encoding is working!"
+else
+    echo "❌ FFmpeg VAAPI test failed"
+fi
+
+if [[ "$NEED_RELOGIN" == "true" ]]; then
+    echo ""
+    echo "⚠️  Group membership changes require a logout/login to take effect."
+    echo "   Either logout and login again, or restart your session."
+fi
+
+echo ""
+echo "🎬 GPU acceleration setup complete!"
+echo "   Your OptiPlex with Intel HD Graphics 630 is ready for fast video conversions!"