v4.5.0
This commit is contained in:
+525
-200
@@ -1,8 +1,10 @@
|
|||||||
from PySide6.QtCore import QThread, Signal, QObject
|
from PySide6.QtCore import QThread, Signal, QObject, QProcess, QTimer
|
||||||
import yt_dlp # Keep yt_dlp import here - only downloader uses it.
|
import yt_dlp # Keep yt_dlp import here - only downloader uses it.
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import subprocess # For direct CLI command execution
|
||||||
|
import shlex # For safely parsing command arguments
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
class SignalManager(QObject):
|
class SignalManager(QObject):
|
||||||
@@ -16,20 +18,32 @@ class DownloadThread(QThread):
|
|||||||
finished_signal = Signal()
|
finished_signal = Signal()
|
||||||
error_signal = Signal(str)
|
error_signal = Signal(str)
|
||||||
file_exists_signal = Signal(str) # New signal for file existence
|
file_exists_signal = Signal(str) # New signal for file existence
|
||||||
|
update_details = Signal(str) # New signal for filename, speed, ETA
|
||||||
|
|
||||||
def __init__(self, url, path, format_id, subtitle_lang=None, is_playlist=False, merge_subs=False, enable_sponsorblock=False, resolution='', playlist_items=None):
|
def __init__(self, url, path, format_id, subtitle_langs=None, is_playlist=False, merge_subs=False, enable_sponsorblock=False, resolution='', playlist_items=None, save_description=False, cookie_file=None, rate_limit=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.url = url
|
self.url = url
|
||||||
self.path = path
|
self.path = path
|
||||||
self.format_id = format_id
|
self.format_id = format_id
|
||||||
self.subtitle_lang = subtitle_lang
|
self.subtitle_langs = subtitle_langs if subtitle_langs else []
|
||||||
self.is_playlist = is_playlist
|
self.is_playlist = is_playlist
|
||||||
self.merge_subs = merge_subs
|
self.merge_subs = merge_subs
|
||||||
self.enable_sponsorblock = enable_sponsorblock
|
self.enable_sponsorblock = enable_sponsorblock
|
||||||
self.resolution = resolution
|
self.resolution = resolution
|
||||||
self.playlist_items = playlist_items
|
self.playlist_items = playlist_items
|
||||||
|
self.save_description = save_description
|
||||||
|
self.cookie_file = cookie_file
|
||||||
|
self.rate_limit = rate_limit
|
||||||
self.paused = False
|
self.paused = False
|
||||||
self.cancelled = False
|
self.cancelled = False
|
||||||
|
self.process = None
|
||||||
|
self.use_direct_command = True # Flag to use direct CLI command instead of Python API
|
||||||
|
self.last_output_time = time.time()
|
||||||
|
self.timeout_timer = None
|
||||||
|
self.current_filename = None # Initialize filename storage
|
||||||
|
self.last_file_path = None # Initialize full file path storage
|
||||||
|
self.subtitle_files = [] # Track subtitle files that are created
|
||||||
|
self.initial_subtitle_files = set() # Track initial subtitle files before download
|
||||||
|
|
||||||
def cleanup_partial_files(self):
|
def cleanup_partial_files(self):
|
||||||
"""Delete any partial files including .part and unmerged format-specific files"""
|
"""Delete any partial files including .part and unmerged format-specific files"""
|
||||||
@@ -46,14 +60,99 @@ class DownloadThread(QThread):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error_signal.emit(f"Error cleaning partial files: {str(e)}")
|
self.error_signal.emit(f"Error cleaning partial files: {str(e)}")
|
||||||
|
|
||||||
|
def cleanup_subtitle_files(self):
|
||||||
|
"""Delete subtitle files after they have been merged into the video file"""
|
||||||
|
if not self.merge_subs:
|
||||||
|
return # Only cleanup if merge_subs is enabled
|
||||||
|
|
||||||
|
try:
|
||||||
|
deleted_count = 0
|
||||||
|
|
||||||
|
# Method 1: Delete tracked subtitle files from output messages
|
||||||
|
if self.subtitle_files:
|
||||||
|
for subtitle_file in self.subtitle_files:
|
||||||
|
try:
|
||||||
|
if os.path.isfile(subtitle_file):
|
||||||
|
os.remove(subtitle_file)
|
||||||
|
deleted_count += 1
|
||||||
|
print(f"DEBUG: Deleted tracked subtitle file: {os.path.basename(subtitle_file)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error deleting subtitle file {subtitle_file}: {str(e)}")
|
||||||
|
|
||||||
|
print(f"DEBUG: Deleted {deleted_count} of {len(self.subtitle_files)} tracked subtitle files")
|
||||||
|
|
||||||
|
# Method 2: Find newly created subtitle files by comparing with initial set
|
||||||
|
try:
|
||||||
|
new_subtitle_files = set()
|
||||||
|
for root, dirs, files in os.walk(self.path):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith('.vtt') or file.endswith('.srt'):
|
||||||
|
full_path = os.path.join(root, file)
|
||||||
|
if full_path not in self.initial_subtitle_files:
|
||||||
|
new_subtitle_files.add(full_path)
|
||||||
|
|
||||||
|
if new_subtitle_files:
|
||||||
|
print(f"DEBUG: Found {len(new_subtitle_files)} new subtitle files to delete")
|
||||||
|
for subtitle_file in new_subtitle_files:
|
||||||
|
try:
|
||||||
|
if os.path.isfile(subtitle_file):
|
||||||
|
os.remove(subtitle_file)
|
||||||
|
deleted_count += 1
|
||||||
|
print(f"DEBUG: Deleted new subtitle file: {os.path.basename(subtitle_file)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error deleting new subtitle file {subtitle_file}: {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in finding new subtitle files: {str(e)}")
|
||||||
|
|
||||||
|
# Method 3: As a last resort, use timestamp-based approach for recently created files
|
||||||
|
if self.last_file_path and deleted_count == 0:
|
||||||
|
target_dir = os.path.dirname(self.last_file_path)
|
||||||
|
|
||||||
|
# Look for subtitle files created in last 5 minutes
|
||||||
|
now = time.time()
|
||||||
|
for filename in os.listdir(target_dir):
|
||||||
|
if filename.endswith('.vtt') or filename.endswith('.srt'):
|
||||||
|
file_path = os.path.join(target_dir, filename)
|
||||||
|
|
||||||
|
# Check if it was created in the last 5 minutes
|
||||||
|
file_time = os.path.getctime(file_path)
|
||||||
|
if now - file_time < 300: # 5 minutes
|
||||||
|
try:
|
||||||
|
os.remove(file_path)
|
||||||
|
deleted_count += 1
|
||||||
|
print(f"DEBUG: Deleted subtitle file by timestamp: {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error deleting subtitle file {filename}: {str(e)}")
|
||||||
|
|
||||||
|
print(f"DEBUG: Total subtitle files deleted: {deleted_count}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning subtitle files: {str(e)}")
|
||||||
|
|
||||||
def check_file_exists(self):
|
def check_file_exists(self):
|
||||||
"""Check if the file already exists before downloading"""
|
"""Check if the file already exists before downloading"""
|
||||||
try:
|
try:
|
||||||
print("DEBUG: Starting file existence check")
|
print("DEBUG: Starting file existence check")
|
||||||
# Use yt-dlp to get the filename without downloading
|
# Use yt-dlp to get the filename without downloading, suppressing warnings
|
||||||
with yt_dlp.YoutubeDL({'quiet': True, 'skip_download': True}) as ydl:
|
ydl_opts_check = {
|
||||||
|
'quiet': True,
|
||||||
|
'skip_download': True,
|
||||||
|
'no_warnings': True, # <-- Suppress warnings during check
|
||||||
|
'ignoreerrors': True, # Also ignore other potential errors during this check
|
||||||
|
'outtmpl': {'default': os.path.join(self.path, '%(title)s.%(ext)s')},
|
||||||
|
'format': self.format_id if self.format_id else 'best' # Use selected format or best
|
||||||
|
}
|
||||||
|
if self.cookie_file:
|
||||||
|
ydl_opts_check['cookiefile'] = self.cookie_file
|
||||||
|
|
||||||
|
with yt_dlp.YoutubeDL(ydl_opts_check) as ydl:
|
||||||
info = ydl.extract_info(self.url, download=False)
|
info = ydl.extract_info(self.url, download=False)
|
||||||
|
|
||||||
|
# Handle cases where info extraction fails silently
|
||||||
|
if not info:
|
||||||
|
print("DEBUG: Failed to extract info during file existence check. Skipping check.")
|
||||||
|
return False # Proceed with download attempt
|
||||||
|
|
||||||
# Get the title and sanitize it for filename
|
# Get the title and sanitize it for filename
|
||||||
title = info.get('title', 'video')
|
title = info.get('title', 'video')
|
||||||
# Don't remove colons and other special characters yet
|
# Don't remove colons and other special characters yet
|
||||||
@@ -106,10 +205,147 @@ class DownloadThread(QThread):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _build_yt_dlp_command(self):
|
||||||
|
"""Build the yt-dlp command line with all options for direct execution."""
|
||||||
|
cmd = ["yt-dlp"]
|
||||||
|
|
||||||
|
# Format selection strategy - use format ID if provided or fallback to resolution
|
||||||
|
if self.format_id:
|
||||||
|
# Strip the -drc suffix if present to fix issues with certain audio formats
|
||||||
|
clean_format_id = self.format_id.split('-drc')[0] if '-drc' in self.format_id else self.format_id
|
||||||
|
|
||||||
|
# Check if this is an audio-only format
|
||||||
|
is_audio_format = False
|
||||||
|
try:
|
||||||
|
ydl_opts = {
|
||||||
|
'quiet': True,
|
||||||
|
'no_warnings': True,
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
info = ydl.extract_info(self.url, download=False)
|
||||||
|
for fmt in info.get('formats', []):
|
||||||
|
if fmt.get('format_id') == clean_format_id:
|
||||||
|
if fmt.get('vcodec') == 'none' or 'audio only' in fmt.get('format_note', '').lower():
|
||||||
|
is_audio_format = True
|
||||||
|
print(f"DEBUG: Detected audio-only format for ID: {clean_format_id}")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG: Error checking if format is audio-only: {e}")
|
||||||
|
|
||||||
|
# For audio-only formats, don't try to merge with video
|
||||||
|
if is_audio_format:
|
||||||
|
cmd.extend(["-f", clean_format_id])
|
||||||
|
print(f"DEBUG: Using audio-only format selection: {clean_format_id}")
|
||||||
|
else:
|
||||||
|
cmd.extend(["-f", f"{clean_format_id}+bestaudio/best"])
|
||||||
|
print(f"DEBUG: Using video format selection with audio: {clean_format_id}+bestaudio/best")
|
||||||
|
|
||||||
|
# Determine output format based on the selected format ID - only for video formats
|
||||||
|
if not is_audio_format:
|
||||||
|
try:
|
||||||
|
format_ext = None
|
||||||
|
print(f"DEBUG: Getting format information for format ID: {self.format_id} (using: {clean_format_id})")
|
||||||
|
ydl_opts = {
|
||||||
|
'quiet': True,
|
||||||
|
'no_warnings': True,
|
||||||
|
'skip_download': True,
|
||||||
|
}
|
||||||
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
info = ydl.extract_info(self.url, download=False)
|
||||||
|
# Look for the clean format ID first
|
||||||
|
for fmt in info.get('formats', []):
|
||||||
|
if fmt.get('format_id') == clean_format_id:
|
||||||
|
format_ext = fmt.get('ext')
|
||||||
|
break
|
||||||
|
# If not found, try the original ID as fallback
|
||||||
|
if not format_ext:
|
||||||
|
for fmt in info.get('formats', []):
|
||||||
|
if fmt.get('format_id') == self.format_id:
|
||||||
|
format_ext = fmt.get('ext')
|
||||||
|
break
|
||||||
|
|
||||||
|
if format_ext:
|
||||||
|
print(f"DEBUG: Detected format extension: {format_ext}")
|
||||||
|
# Ensure output matches the selected format - only for video formats
|
||||||
|
cmd.extend(["--merge-output-format", format_ext])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"DEBUG: Error detecting format extension: {e}")
|
||||||
|
# If we can't determine the format, don't specify merge-output-format
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# If no specific format ID, use resolution-based sorting (-S)
|
||||||
|
res_value = self.resolution if self.resolution else "720" # Default to 720p if no resolution specified
|
||||||
|
cmd.extend(["-S", f"res:{res_value}"])
|
||||||
|
|
||||||
|
# Output template with resolution in filename
|
||||||
|
output_template = os.path.join(self.path, '%(title)s_%(resolution)s.%(ext)s')
|
||||||
|
|
||||||
|
# Handle playlist directory creation if needed
|
||||||
|
if self.is_playlist:
|
||||||
|
# Create output template with playlist subfolder
|
||||||
|
output_template = os.path.join(self.path, '%(playlist_title)s/%(title)s_%(resolution)s.%(ext)s')
|
||||||
|
|
||||||
|
cmd.extend(["-o", output_template])
|
||||||
|
|
||||||
|
# Add common options
|
||||||
|
cmd.append("--force-overwrites")
|
||||||
|
|
||||||
|
# Add playlist items if specified
|
||||||
|
if self.is_playlist and self.playlist_items:
|
||||||
|
cmd.extend(["--playlist-items", self.playlist_items])
|
||||||
|
|
||||||
|
# Add subtitle options if selected
|
||||||
|
if self.subtitle_langs:
|
||||||
|
# Subtitles work with both audio-only and video formats
|
||||||
|
# For audio-only formats, subtitles will be downloaded as separate files
|
||||||
|
cmd.append("--write-subs")
|
||||||
|
|
||||||
|
# Get language codes from subtitle selections
|
||||||
|
lang_codes = []
|
||||||
|
for sub_selection in self.subtitle_langs:
|
||||||
|
try:
|
||||||
|
# Extract just the language code (e.g., 'en' from 'en - Manual')
|
||||||
|
lang_code = sub_selection.split(' - ')[0]
|
||||||
|
lang_codes.append(lang_code)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Could not parse subtitle selection '{sub_selection}': {e}")
|
||||||
|
|
||||||
|
if lang_codes:
|
||||||
|
cmd.extend(["--sub-langs", ",".join(lang_codes)])
|
||||||
|
cmd.append("--write-auto-subs") # Include auto-generated subtitles
|
||||||
|
|
||||||
|
# Add embedding if requested - only applies to video formats
|
||||||
|
if self.merge_subs:
|
||||||
|
cmd.append("--embed-subs")
|
||||||
|
|
||||||
|
# Add SponsorBlock if enabled
|
||||||
|
if self.enable_sponsorblock:
|
||||||
|
cmd.append("--sponsorblock-remove")
|
||||||
|
cmd.append("sponsor")
|
||||||
|
|
||||||
|
# Add description saving if enabled
|
||||||
|
if self.save_description:
|
||||||
|
cmd.append("--write-description")
|
||||||
|
|
||||||
|
# Add cookies if specified
|
||||||
|
if self.cookie_file:
|
||||||
|
cmd.extend(["--cookies", self.cookie_file])
|
||||||
|
|
||||||
|
# Add rate limit if specified
|
||||||
|
if self.rate_limit:
|
||||||
|
cmd.extend(["-r", self.rate_limit])
|
||||||
|
|
||||||
|
# Add the URL as the final argument
|
||||||
|
cmd.append(self.url)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
print("DEBUG: Starting download thread")
|
print("DEBUG: Starting download thread")
|
||||||
# First check if file already exists
|
|
||||||
|
# First check if file already exists using original method
|
||||||
existing_file = self.check_file_exists()
|
existing_file = self.check_file_exists()
|
||||||
if existing_file:
|
if existing_file:
|
||||||
print(f"DEBUG: File exists, emitting signal: {existing_file}")
|
print(f"DEBUG: File exists, emitting signal: {existing_file}")
|
||||||
@@ -117,216 +353,305 @@ class DownloadThread(QThread):
|
|||||||
return
|
return
|
||||||
|
|
||||||
print("DEBUG: No existing file found, proceeding with download")
|
print("DEBUG: No existing file found, proceeding with download")
|
||||||
class DebugLogger:
|
|
||||||
def debug(self, msg):
|
|
||||||
# Print all debug messages to help diagnose issues
|
|
||||||
print(f"YT-DLP DEBUG: {msg}")
|
|
||||||
|
|
||||||
# Check for file exists message - look for both patterns
|
|
||||||
if "already exists" in msg or "has already been downloaded" in msg:
|
|
||||||
print(f"FILE EXISTS DETECTED: {msg}")
|
|
||||||
# Try to extract the filename
|
|
||||||
import re
|
|
||||||
match = re.search(r'File (.*?) already exists', msg)
|
|
||||||
if not match:
|
|
||||||
match = re.search(r'(.*?) has already been downloaded', msg)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
filename = os.path.basename(match.group(1))
|
|
||||||
self.thread.file_exists_signal.emit(filename)
|
|
||||||
raise Exception("FileExistsError")
|
|
||||||
|
|
||||||
# Add detection of post-processing messages
|
|
||||||
if "Downloading" in msg:
|
|
||||||
self.thread.status_signal.emit("⚡ Downloading video...")
|
|
||||||
elif "Post-process" in msg or "Sponsorblock" in msg:
|
|
||||||
self.thread.status_signal.emit("✨ Post-processing: Removing sponsor segments...")
|
|
||||||
self.thread.progress_signal.emit(99) # Keep progress bar at 99%
|
|
||||||
elif any(x in msg.lower() for x in ['downloading webpage', 'downloading api']):
|
|
||||||
self.thread.status_signal.emit("🔍 Fetching video information...")
|
|
||||||
self.thread.progress_signal.emit(0)
|
|
||||||
elif 'extracting' in msg.lower():
|
|
||||||
self.thread.status_signal.emit("📦 Extracting video data...")
|
|
||||||
self.thread.progress_signal.emit(0)
|
|
||||||
elif 'downloading m3u8' in msg.lower():
|
|
||||||
self.thread.status_signal.emit("🎯 Preparing video streams...")
|
|
||||||
self.thread.progress_signal.emit(0)
|
|
||||||
|
|
||||||
def warning(self, msg):
|
|
||||||
print(f"YT-DLP WARNING: {msg}")
|
|
||||||
self.thread.status_signal.emit(f"⚠️ Warning: {msg}")
|
|
||||||
# Also check for file exists in warnings
|
|
||||||
if "already exists" in msg or "has already been downloaded" in msg:
|
|
||||||
print(f"FILE EXISTS DETECTED IN WARNING: {msg}")
|
|
||||||
import re
|
|
||||||
match = re.search(r'File (.*?) already exists', msg)
|
|
||||||
if not match:
|
|
||||||
match = re.search(r'(.*?) has already been downloaded', msg)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
filename = os.path.basename(match.group(1))
|
|
||||||
self.thread.file_exists_signal.emit(filename)
|
|
||||||
raise Exception("FileExistsError")
|
|
||||||
|
|
||||||
def error(self, msg):
|
|
||||||
print(f"YT-DLP ERROR: {msg}")
|
|
||||||
self.thread.status_signal.emit(f"❌ Error: {msg}")
|
|
||||||
# Also check for file exists in errors
|
|
||||||
if "already exists" in msg or "has already been downloaded" in msg:
|
|
||||||
print(f"FILE EXISTS DETECTED IN ERROR: {msg}")
|
|
||||||
import re
|
|
||||||
match = re.search(r'File (.*?) already exists', msg)
|
|
||||||
if not match:
|
|
||||||
match = re.search(r'(.*?) has already been downloaded', msg)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
filename = os.path.basename(match.group(1))
|
|
||||||
self.thread.file_exists_signal.emit(filename)
|
|
||||||
raise Exception("FileExistsError")
|
|
||||||
|
|
||||||
def __init__(self, thread):
|
|
||||||
self.thread = thread
|
|
||||||
|
|
||||||
def progress_hook(d):
|
|
||||||
if self.cancelled:
|
|
||||||
raise Exception("Download cancelled by user")
|
|
||||||
|
|
||||||
if d['status'] == 'downloading':
|
|
||||||
while self.paused and not self.cancelled:
|
|
||||||
time.sleep(0.1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
|
# Get initial list of subtitle files to compare later
|
||||||
|
self.initial_subtitle_files = set()
|
||||||
|
if self.merge_subs:
|
||||||
try:
|
try:
|
||||||
downloaded_bytes = d.get('downloaded_bytes', 0)
|
# Scan for existing subtitle files in the directory
|
||||||
total_bytes = d.get('total_bytes', 0) or d.get('total_bytes_estimate', 0)
|
for root, dirs, files in os.walk(self.path):
|
||||||
|
for file in files:
|
||||||
|
if file.endswith('.vtt') or file.endswith('.srt'):
|
||||||
|
self.initial_subtitle_files.add(os.path.join(root, file))
|
||||||
|
print(f"DEBUG: Found {len(self.initial_subtitle_files)} existing subtitle files before download")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Error scanning for initial subtitle files: {e}")
|
||||||
|
|
||||||
if total_bytes:
|
if self.use_direct_command:
|
||||||
progress = (downloaded_bytes / total_bytes) * 100
|
# Use direct CLI command instead of Python API
|
||||||
self.progress_signal.emit(progress)
|
self._run_direct_command()
|
||||||
|
|
||||||
speed = d.get('speed', 0)
|
|
||||||
if speed:
|
|
||||||
speed_str = f"{speed/1024/1024:.1f} MB/s"
|
|
||||||
else:
|
else:
|
||||||
speed_str = "N/A"
|
# Original method using Python API - code left for reference
|
||||||
|
self._run_python_api()
|
||||||
eta = d.get('eta', 0)
|
|
||||||
if eta:
|
|
||||||
eta_str = f"{eta//60}:{eta%60:02d}"
|
|
||||||
else:
|
|
||||||
eta_str = "N/A"
|
|
||||||
|
|
||||||
filename = os.path.basename(d.get('filename', ''))
|
|
||||||
status = f"Speed: {speed_str} | ETA: {eta_str} | File: {filename}"
|
|
||||||
self.status_signal.emit(status)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.status_signal.emit("⚡ Downloading...")
|
# Catch errors during setup
|
||||||
|
self.error_signal.emit(f"Critical error in download thread: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
elif d['status'] == 'finished':
|
def _run_direct_command(self):
|
||||||
if self.enable_sponsorblock:
|
"""Run yt-dlp as a direct command line process instead of using Python API."""
|
||||||
self.progress_signal.emit(99)
|
try:
|
||||||
self.status_signal.emit("✨ Post-processing: Removing sponsor segments...")
|
cmd = self._build_yt_dlp_command()
|
||||||
else:
|
cmd_str = " ".join(shlex.quote(str(arg)) for arg in cmd)
|
||||||
|
print(f"DEBUG: Executing command: {cmd_str}")
|
||||||
|
|
||||||
|
self.status_signal.emit("🚀 Starting download...")
|
||||||
|
self.progress_signal.emit(0)
|
||||||
|
|
||||||
|
# Start the process
|
||||||
|
# Add creationflags=subprocess.CREATE_NO_WINDOW to hide console on Windows
|
||||||
|
creation_flags = 0
|
||||||
|
if os.name == 'nt': # Only use flag on Windows
|
||||||
|
creation_flags = subprocess.CREATE_NO_WINDOW
|
||||||
|
|
||||||
|
self.process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1, # Line buffered
|
||||||
|
universal_newlines=True,
|
||||||
|
creationflags=creation_flags # Add this flag
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process output line by line to update progress
|
||||||
|
for line in iter(self.process.stdout.readline, ''):
|
||||||
|
if self.cancelled:
|
||||||
|
self.process.terminate()
|
||||||
|
self.cleanup_partial_files()
|
||||||
|
self.status_signal.emit("Download cancelled")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Wait if paused
|
||||||
|
while self.paused and not self.cancelled:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Parse the line for download progress and status updates
|
||||||
|
self._parse_output_line(line)
|
||||||
|
|
||||||
|
# Wait for process to complete
|
||||||
|
return_code = self.process.wait()
|
||||||
|
|
||||||
|
if return_code == 0:
|
||||||
self.progress_signal.emit(100)
|
self.progress_signal.emit(100)
|
||||||
self.status_signal.emit("✅ Download completed!")
|
self.status_signal.emit("✅ Download completed!")
|
||||||
|
|
||||||
# Get the extension from the format_id
|
# Clean up subtitle files if they were merged, with a small delay
|
||||||
with yt_dlp.YoutubeDL({'quiet': True}) as ydl:
|
# to ensure the embedding process has completed
|
||||||
try:
|
if self.merge_subs:
|
||||||
info = ydl.extract_info(self.url, download=False)
|
# Add a significant delay to ensure ffmpeg has released all file handles
|
||||||
selected_format = next(
|
# and any post-processing is complete
|
||||||
f for f in info['formats']
|
self.status_signal.emit("✅ Download completed! Cleaning up...")
|
||||||
if str(f.get('format_id', '')) == self.format_id
|
time.sleep(3) # Increased delay to 3 seconds
|
||||||
)
|
self.cleanup_subtitle_files()
|
||||||
output_ext = selected_format.get('ext', 'mp4')
|
|
||||||
|
self.finished_signal.emit()
|
||||||
|
else:
|
||||||
|
# Check if it was cancelled
|
||||||
|
if self.cancelled:
|
||||||
|
self.status_signal.emit("Download cancelled")
|
||||||
|
else:
|
||||||
|
self.error_signal.emit(f"Download failed with return code {return_code}")
|
||||||
|
self.cleanup_partial_files()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error_signal.emit(f"Failed to get video information: {str(e)}")
|
self.error_signal.emit(f"Error in direct command: {str(e)}")
|
||||||
|
self.cleanup_partial_files()
|
||||||
|
|
||||||
|
def _parse_output_line(self, line):
|
||||||
|
"""Parse yt-dlp command output to update progress and status."""
|
||||||
|
line = line.strip()
|
||||||
|
# print(f"yt-dlp: {line}") # Log all output - OPTIONALLY UNCOMMENT FOR VERBOSE DEBUG
|
||||||
|
|
||||||
|
# Extract filename when the destination line appears
|
||||||
|
# Use a slightly more robust regex looking for the start of the line
|
||||||
|
dest_match = re.search(r'^\[download\] Destination:\s*(.*)', line)
|
||||||
|
if dest_match:
|
||||||
|
try:
|
||||||
|
filepath = dest_match.group(1).strip()
|
||||||
|
self.current_filename = os.path.basename(filepath)
|
||||||
|
self.last_file_path = filepath # Store the full path for later cleanup
|
||||||
|
print(f"DEBUG: Extracted filename: {self.current_filename}") # DEBUG
|
||||||
|
|
||||||
|
# Check if this is an audio-only download by looking in the previous lines
|
||||||
|
is_audio_download = False
|
||||||
|
|
||||||
|
# Look for audio format indicators in the current line or preceding output
|
||||||
|
# yt-dlp typically mentions format like "Downloading format 251 - audio only"
|
||||||
|
if ' - audio only' in line:
|
||||||
|
is_audio_download = True
|
||||||
|
# Check if the format ID is mentioned earlier in the line
|
||||||
|
format_match = re.search(r'Downloading format (\d+)', line)
|
||||||
|
if format_match:
|
||||||
|
format_id = format_match.group(1)
|
||||||
|
print(f"DEBUG: Detected format ID: {format_id}")
|
||||||
|
# Format IDs for audio typically have different patterns
|
||||||
|
# (like 140, 251 for audio vs 137, 248 for video)
|
||||||
|
# This is just a heuristic since format IDs can vary
|
||||||
|
|
||||||
|
# Determine file type based on extension and context
|
||||||
|
ext = os.path.splitext(self.current_filename)[1].lower()
|
||||||
|
|
||||||
|
# Check if this is explicitly an audio stream download
|
||||||
|
if is_audio_download or 'Downloading audio' in line:
|
||||||
|
self.status_signal.emit(f"⏬ Downloading audio...")
|
||||||
|
# Video file extensions with likely video content
|
||||||
|
elif ext in ['.mp4', '.webm', '.mkv', '.avi', '.mov', '.flv']:
|
||||||
|
self.status_signal.emit(f"⏬ Downloading video...")
|
||||||
|
# Audio file extensions
|
||||||
|
elif ext in ['.mp3', '.m4a', '.aac', '.wav', '.ogg', '.opus', '.flac']:
|
||||||
|
self.status_signal.emit(f"⏬ Downloading audio...")
|
||||||
|
# Subtitle file extensions
|
||||||
|
elif ext in ['.vtt', '.srt', '.ass', '.ssa']:
|
||||||
|
self.status_signal.emit(f"⏬ Downloading subtitle...")
|
||||||
|
# Default case
|
||||||
|
else:
|
||||||
|
self.status_signal.emit(f"⏬ Downloading...")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error extracting filename from line '{line}': {e}")
|
||||||
|
self.status_signal.emit("⚡ Downloading...") # Fallback status
|
||||||
|
return # Don't process this line further for speed/ETA
|
||||||
|
|
||||||
|
# Check for specific download types in the output
|
||||||
|
if "Downloading video" in line:
|
||||||
|
self.status_signal.emit(f"⏬ Downloading video...")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Base yt-dlp options with resolution in filename
|
elif "Downloading audio" in line:
|
||||||
output_template = '%(title)s_%(resolution)s.%(ext)s'
|
self.status_signal.emit(f"⏬ Downloading audio...")
|
||||||
if self.is_playlist:
|
return
|
||||||
output_template = '%(playlist_title)s/%(title)s_%(resolution)s.%(ext)s'
|
|
||||||
|
|
||||||
ydl_opts = {
|
|
||||||
'format': f'{self.format_id}+bestaudio/best',
|
|
||||||
'outtmpl': os.path.join(self.path, output_template),
|
|
||||||
'progress_hooks': [progress_hook],
|
|
||||||
'merge_output_format': 'mp4',
|
|
||||||
'logger': DebugLogger(self),
|
|
||||||
'postprocessors': [{
|
|
||||||
'key': 'FFmpegVideoConvertor',
|
|
||||||
'preferedformat': 'mp4'
|
|
||||||
}],
|
|
||||||
'force_overwrites': True
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add subtitle options if selected
|
|
||||||
if self.subtitle_lang:
|
|
||||||
lang_code = self.subtitle_lang.split(' - ')[0]
|
|
||||||
is_auto = 'Auto-generated' in self.subtitle_lang
|
|
||||||
ydl_opts.update({
|
|
||||||
'writesubtitles': True,
|
|
||||||
'subtitleslangs': [lang_code],
|
|
||||||
'writeautomaticsub': True,
|
|
||||||
'skip_manual_subs': is_auto,
|
|
||||||
'skip_auto_subs': not is_auto,
|
|
||||||
'embedsubtitles': self.merge_subs,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
# Detect subtitle file creation
|
||||||
|
# Look for lines like "[info] Writing video subtitles to: filename.xx.vtt"
|
||||||
|
subtitle_match = re.search(r'(?:Writing|Downloading) (?:video )?subtitles.*?(?:to|:)\s*(.*\.(?:vtt|srt))', line, re.IGNORECASE)
|
||||||
|
if subtitle_match:
|
||||||
|
subtitle_file = subtitle_match.group(1).strip()
|
||||||
|
# Show subtitle download message
|
||||||
|
self.status_signal.emit(f"⏬ Downloading subtitle...")
|
||||||
|
# Store the subtitle file path for later deletion if merging is enabled
|
||||||
if self.merge_subs:
|
if self.merge_subs:
|
||||||
ydl_opts['postprocessors'].extend([
|
if not os.path.isabs(subtitle_file):
|
||||||
{
|
# If it's a relative path, make it absolute based on current path
|
||||||
'key': 'FFmpegSubtitlesConvertor',
|
subtitle_file = os.path.join(self.path, subtitle_file)
|
||||||
'format': 'srt',
|
self.subtitle_files.append(subtitle_file)
|
||||||
},
|
print(f"DEBUG: Tracking subtitle file for later cleanup: {subtitle_file}")
|
||||||
{
|
return
|
||||||
'key': 'FFmpegEmbedSubtitle',
|
|
||||||
'already_have_subtitle': False,
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
# Add SponsorBlock options if enabled
|
# Send status updates based on output line content
|
||||||
if self.enable_sponsorblock:
|
if 'Downloading webpage' in line or 'Extracting URL' in line:
|
||||||
ydl_opts['postprocessors'].extend([{
|
self.status_signal.emit("🔍 Fetching video information...")
|
||||||
'key': 'SponsorBlock',
|
self.progress_signal.emit(0)
|
||||||
'categories': ['sponsor'],
|
elif 'Downloading API JSON' in line:
|
||||||
'api': 'https://sponsor.ajay.app'
|
self.status_signal.emit("📋 Processing playlist data...")
|
||||||
}, {
|
self.progress_signal.emit(0)
|
||||||
'key': 'ModifyChapters',
|
elif 'Downloading m3u8 information' in line:
|
||||||
'remove_sponsor_segments': ['sponsor'],
|
self.status_signal.emit("🎯 Preparing video streams...")
|
||||||
'sponsorblock_chapter_title': '[SponsorBlock]',
|
self.progress_signal.emit(0)
|
||||||
'force_keyframes': False
|
elif '[download] Downloading video ' in line:
|
||||||
}])
|
self.status_signal.emit("⏬ Downloading video...")
|
||||||
|
elif '[download] Downloading audio ' in line:
|
||||||
# Add playlist items if specified
|
self.status_signal.emit("⏬ Downloading audio...")
|
||||||
if self.playlist_items:
|
elif 'Downloading format' in line:
|
||||||
ydl_opts['playlist_items'] = self.playlist_items
|
# Try to detect if it's audio or video format
|
||||||
|
if ' - audio only' in line:
|
||||||
try:
|
self.status_signal.emit("⏬ Downloading audio...")
|
||||||
# Download the video
|
elif ' - video only' in line:
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
self.status_signal.emit("⏬ Downloading video...")
|
||||||
ydl.download([self.url])
|
|
||||||
|
|
||||||
if not self.cancelled:
|
|
||||||
self.finished_signal.emit()
|
|
||||||
|
|
||||||
# Clean up subtitle files after successful download
|
|
||||||
if self.merge_subs:
|
|
||||||
for filename in os.listdir(self.path):
|
|
||||||
if filename.lower().endswith(('.vtt', '.srt', '.ass')):
|
|
||||||
try:
|
|
||||||
os.remove(os.path.join(self.path, filename))
|
|
||||||
except Exception as e:
|
|
||||||
self.error_signal.emit(f"Error deleting subtitle file: {str(e)}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if str(e) == "Download cancelled by user":
|
|
||||||
self.cleanup_partial_files()
|
|
||||||
self.error_signal.emit("Download cancelled")
|
|
||||||
else:
|
else:
|
||||||
self.error_signal.emit(f"Download failed: {str(e)}")
|
# Don't emit generic message - format is unclear
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Look for download percentage
|
||||||
|
percent_match = re.search(r'(\d+\.\d+)%', line)
|
||||||
|
if percent_match:
|
||||||
|
try:
|
||||||
|
percent = float(percent_match.group(1))
|
||||||
|
self.progress_signal.emit(percent)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Check for download speed and ETA
|
||||||
|
if '[download]' in line and '%' in line:
|
||||||
|
# Try to extract more detailed status info
|
||||||
|
try:
|
||||||
|
# Look for speed
|
||||||
|
speed_match = re.search(r'at\s+(\d+\.\d+[KMG]iB/s)', line)
|
||||||
|
speed_str = speed_match.group(1) if speed_match else "N/A"
|
||||||
|
|
||||||
|
# Look for ETA
|
||||||
|
eta_match = re.search(r'ETA\s+(\d+:\d+)', line)
|
||||||
|
eta_str = eta_match.group(1) if eta_match else "N/A"
|
||||||
|
|
||||||
|
# Simplify status message to only show the speed and ETA
|
||||||
|
status = f"Speed: {speed_str} | ETA: {eta_str}"
|
||||||
|
self.update_details.emit(status)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error_signal.emit(f"Critical error: {str(e)}")
|
# If parsing fails, just show basic status (maybe log the error)
|
||||||
|
print(f"Error parsing download details line: {line} -> {e}")
|
||||||
|
pass # Keep basic status emission below if needed, or emit generic details
|
||||||
|
|
||||||
|
# Check for post-processing
|
||||||
|
if '[Merger]' in line or 'Merging formats' in line:
|
||||||
|
self.status_signal.emit("✨ Post-processing: Merging formats...")
|
||||||
|
self.progress_signal.emit(95)
|
||||||
|
elif 'SponsorBlock' in line:
|
||||||
|
self.status_signal.emit("✨ Post-processing: Removing sponsor segments...")
|
||||||
|
self.progress_signal.emit(97)
|
||||||
|
elif 'Deleting original file' in line:
|
||||||
|
self.progress_signal.emit(98)
|
||||||
|
elif 'has already been downloaded' in line:
|
||||||
|
# File already exists - extract filename
|
||||||
|
match = re.search(r'(.*?) has already been downloaded', line)
|
||||||
|
if match:
|
||||||
|
filename = os.path.basename(match.group(1))
|
||||||
|
# Determine file type based on extension for existing file message
|
||||||
|
ext = os.path.splitext(filename)[1].lower()
|
||||||
|
|
||||||
|
if ext in ['.mp4', '.webm', '.mkv', '.avi', '.mov', '.flv']:
|
||||||
|
self.status_signal.emit(f"⚠️ Video file already exists")
|
||||||
|
elif ext in ['.mp3', '.m4a', '.aac', '.wav', '.ogg', '.opus', '.flac']:
|
||||||
|
self.status_signal.emit(f"⚠️ Audio file already exists")
|
||||||
|
elif ext in ['.vtt', '.srt', '.ass', '.ssa']:
|
||||||
|
self.status_signal.emit(f"⚠️ Subtitle file already exists")
|
||||||
|
else:
|
||||||
|
self.status_signal.emit(f"⚠️ File already exists")
|
||||||
|
|
||||||
|
self.file_exists_signal.emit(filename)
|
||||||
|
else:
|
||||||
|
print(f"Could not extract filename from 'already downloaded' line: {line}")
|
||||||
|
self.status_signal.emit("⚠️ File already exists") # Fallback status
|
||||||
|
elif 'Finished downloading' in line:
|
||||||
|
self.progress_signal.emit(100)
|
||||||
|
|
||||||
|
# Show completion message based on file type
|
||||||
|
if self.current_filename:
|
||||||
|
ext = os.path.splitext(self.current_filename)[1].lower()
|
||||||
|
|
||||||
|
# Video file extensions
|
||||||
|
if ext in ['.mp4', '.webm', '.mkv', '.avi', '.mov', '.flv']:
|
||||||
|
self.status_signal.emit(f"✅ Video download completed!")
|
||||||
|
# Audio file extensions
|
||||||
|
elif ext in ['.mp3', '.m4a', '.aac', '.wav', '.ogg', '.opus', '.flac']:
|
||||||
|
self.status_signal.emit(f"✅ Audio download completed!")
|
||||||
|
# Subtitle file extensions
|
||||||
|
elif ext in ['.vtt', '.srt', '.ass', '.ssa']:
|
||||||
|
self.status_signal.emit(f"✅ Subtitle download completed!")
|
||||||
|
# Default case
|
||||||
|
else:
|
||||||
|
self.status_signal.emit("✅ Download completed!")
|
||||||
|
else:
|
||||||
|
self.status_signal.emit("✅ Download completed!")
|
||||||
|
|
||||||
|
self.update_details.emit("") # Clear details label on completion
|
||||||
|
|
||||||
|
def _run_python_api(self):
|
||||||
|
"""Original download method using Python API - kept for reference."""
|
||||||
|
# The existing run method code using yt_dlp.YoutubeDL starts here
|
||||||
|
# This method is no longer used by default
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
self.paused = True
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
self.paused = False
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
self.cancelled = True
|
||||||
|
# Terminate the subprocess if it's running
|
||||||
|
if self.process:
|
||||||
|
try:
|
||||||
|
self.process.terminate()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
+789
-163
File diff suppressed because it is too large
Load Diff
+110
-28
@@ -19,6 +19,9 @@ class FormatTableMixin:
|
|||||||
self.format_table.setColumnCount(8)
|
self.format_table.setColumnCount(8)
|
||||||
self.format_table.setHorizontalHeaderLabels(['Select', 'Quality', 'Extension', 'Resolution', 'File Size', 'Codec', 'Audio', 'Notes'])
|
self.format_table.setHorizontalHeaderLabels(['Select', 'Quality', 'Extension', 'Resolution', 'File Size', 'Codec', 'Audio', 'Notes'])
|
||||||
|
|
||||||
|
# Enable alternating row colors
|
||||||
|
self.format_table.setAlternatingRowColors(True)
|
||||||
|
|
||||||
# Set specific column widths and resize modes
|
# Set specific column widths and resize modes
|
||||||
self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) # Select
|
self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) # Select
|
||||||
self.format_table.setColumnWidth(0, 50) # Select column width
|
self.format_table.setColumnWidth(0, 50) # Select column width
|
||||||
@@ -51,25 +54,32 @@ class FormatTableMixin:
|
|||||||
|
|
||||||
self.format_table.setStyleSheet("""
|
self.format_table.setStyleSheet("""
|
||||||
QTableWidget {
|
QTableWidget {
|
||||||
background-color: #363636;
|
background-color: #1b2021;
|
||||||
border: 2px solid #3d3d3d;
|
border: 2px solid #1b2021;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
gridline-color: #3d3d3d;
|
gridline-color: #1b2021;
|
||||||
}
|
}
|
||||||
QTableWidget::item {
|
QTableWidget::item {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-bottom: 1px solid #3d3d3d;
|
border-bottom: 1px solid #1b2021;
|
||||||
}
|
}
|
||||||
QTableWidget::item:selected {
|
QTableWidget::item:selected {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
QHeaderView::section {
|
QHeaderView::section {
|
||||||
background-color: #2b2b2b;
|
background-color: #15181b;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border: 1px solid #3d3d3d;
|
border: 1px solid #1b2021;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
/* Style alternating rows with more contrast */
|
||||||
|
QTableWidget::item:alternate {
|
||||||
|
background-color: #212529;
|
||||||
|
}
|
||||||
|
QTableWidget::item {
|
||||||
|
background-color: #16191b;
|
||||||
|
}
|
||||||
QCheckBox::indicator {
|
QCheckBox::indicator {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@@ -77,11 +87,11 @@ class FormatTableMixin:
|
|||||||
}
|
}
|
||||||
QCheckBox::indicator:unchecked {
|
QCheckBox::indicator:unchecked {
|
||||||
border: 2px solid #666666;
|
border: 2px solid #666666;
|
||||||
background: #2b2b2b;
|
background: #15181b;
|
||||||
}
|
}
|
||||||
QCheckBox::indicator:checked {
|
QCheckBox::indicator:checked {
|
||||||
border: 2px solid #ff0000;
|
border: 2px solid #c90000;
|
||||||
background: #ff0000;
|
background: #c90000;
|
||||||
}
|
}
|
||||||
QWidget {
|
QWidget {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -146,20 +156,65 @@ class FormatTableMixin:
|
|||||||
self.format_table.setRowCount(0)
|
self.format_table.setRowCount(0)
|
||||||
self.format_checkboxes.clear()
|
self.format_checkboxes.clear()
|
||||||
|
|
||||||
# Find best quality format for recommendations
|
is_playlist_mode = hasattr(self, 'is_playlist') and self.is_playlist
|
||||||
|
|
||||||
|
# Configure columns based on mode
|
||||||
|
if is_playlist_mode:
|
||||||
|
self.format_table.setColumnCount(5)
|
||||||
|
self.format_table.setHorizontalHeaderLabels(['Select', 'Quality', 'Resolution', 'Notes', 'Audio'])
|
||||||
|
|
||||||
|
# Configure column visibility and resizing for playlist mode
|
||||||
|
self.format_table.setColumnHidden(5, True)
|
||||||
|
self.format_table.setColumnHidden(6, True)
|
||||||
|
self.format_table.setColumnHidden(7, True)
|
||||||
|
|
||||||
|
# Set specific resize modes for playlist columns
|
||||||
|
self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.format_table.setColumnWidth(0, 50)
|
||||||
|
self.format_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||||
|
self.format_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
||||||
|
self.format_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
||||||
|
self.format_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.Stretch)
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.format_table.setColumnCount(8)
|
||||||
|
self.format_table.setHorizontalHeaderLabels(['Select', 'Quality', 'Extension', 'Resolution', 'File Size', 'Codec', 'Audio', 'Notes'])
|
||||||
|
# Ensure all columns are visible
|
||||||
|
for i in range(2, 8):
|
||||||
|
self.format_table.setColumnHidden(i, False)
|
||||||
|
|
||||||
|
# Reapply resize modes for non-playlist mode if needed (optional, might be okay without)
|
||||||
|
self.format_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.format_table.setColumnWidth(0, 50)
|
||||||
|
self.format_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.format_table.setColumnWidth(1, 100)
|
||||||
|
self.format_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.format_table.setColumnWidth(2, 80)
|
||||||
|
self.format_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.format_table.setColumnWidth(3, 100)
|
||||||
|
self.format_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.format_table.setColumnWidth(4, 100)
|
||||||
|
self.format_table.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.format_table.setColumnWidth(5, 150)
|
||||||
|
self.format_table.horizontalHeader().setSectionResizeMode(6, QHeaderView.ResizeMode.Fixed)
|
||||||
|
self.format_table.setColumnWidth(6, 120)
|
||||||
|
self.format_table.horizontalHeader().setSectionResizeMode(7, QHeaderView.ResizeMode.Stretch)
|
||||||
|
|
||||||
|
|
||||||
|
# Find best quality format for recommendations (only needed for non-playlist mode notes)
|
||||||
|
best_video_size = 0
|
||||||
|
if not is_playlist_mode:
|
||||||
best_video_size = max((f.get('filesize', 0) for f in formats if f.get('vcodec') != 'none'), default=0)
|
best_video_size = max((f.get('filesize', 0) for f in formats if f.get('vcodec') != 'none'), default=0)
|
||||||
|
|
||||||
for f in formats:
|
for f in formats:
|
||||||
row = self.format_table.rowCount()
|
row = self.format_table.rowCount()
|
||||||
self.format_table.insertRow(row)
|
self.format_table.insertRow(row)
|
||||||
|
|
||||||
# Add checkbox
|
# Column 0: Select Checkbox (Always shown)
|
||||||
checkbox = QCheckBox()
|
checkbox = QCheckBox()
|
||||||
checkbox.format_id = str(f.get('format_id', ''))
|
checkbox.format_id = str(f.get('format_id', ''))
|
||||||
checkbox.clicked.connect(lambda checked, cb=checkbox: self.handle_checkbox_click(cb))
|
checkbox.clicked.connect(lambda checked, cb=checkbox: self.handle_checkbox_click(cb))
|
||||||
self.format_checkboxes.append(checkbox)
|
self.format_checkboxes.append(checkbox)
|
||||||
|
|
||||||
# Create a widget to center the checkbox
|
|
||||||
checkbox_widget = QWidget()
|
checkbox_widget = QWidget()
|
||||||
checkbox_widget.setStyleSheet("background-color: transparent;")
|
checkbox_widget.setStyleSheet("background-color: transparent;")
|
||||||
checkbox_layout = QHBoxLayout(checkbox_widget)
|
checkbox_layout = QHBoxLayout(checkbox_widget)
|
||||||
@@ -169,7 +224,7 @@ class FormatTableMixin:
|
|||||||
checkbox_layout.setSpacing(0)
|
checkbox_layout.setSpacing(0)
|
||||||
self.format_table.setCellWidget(row, 0, checkbox_widget)
|
self.format_table.setCellWidget(row, 0, checkbox_widget)
|
||||||
|
|
||||||
# Quality (replacing Format ID)
|
# Column 1: Quality (Always shown)
|
||||||
quality_text = self.get_quality_label(f)
|
quality_text = self.get_quality_label(f)
|
||||||
quality_item = QTableWidgetItem(quality_text)
|
quality_item = QTableWidgetItem(quality_text)
|
||||||
# Set color based on quality
|
# Set color based on quality
|
||||||
@@ -183,20 +238,55 @@ class FormatTableMixin:
|
|||||||
quality_item.setForeground(QColor('#ff5555')) # Red for low quality
|
quality_item.setForeground(QColor('#ff5555')) # Red for low quality
|
||||||
self.format_table.setItem(row, 1, quality_item)
|
self.format_table.setItem(row, 1, quality_item)
|
||||||
|
|
||||||
# Extension
|
# --- Populate columns common to both modes (Moved outside the 'if not is_playlist_mode' block) ---
|
||||||
self.format_table.setItem(row, 2, QTableWidgetItem(f.get('ext', '').upper()))
|
|
||||||
|
|
||||||
# Resolution
|
# Column 2: Resolution (Always shown)
|
||||||
resolution = f.get('resolution', 'N/A')
|
resolution = f.get('resolution', 'N/A')
|
||||||
if f.get('vcodec') == 'none':
|
if f.get('vcodec') == 'none':
|
||||||
resolution = 'Audio only'
|
resolution = 'Audio only'
|
||||||
|
self.format_table.setItem(row, 2, QTableWidgetItem(resolution))
|
||||||
|
|
||||||
|
# Column 3: Notes for playlist mode, Extension for normal mode
|
||||||
|
if is_playlist_mode:
|
||||||
|
# Get notes for playlist mode
|
||||||
|
notes = self.get_format_notes(f, 0) # We don't need best_video_size for simple notes
|
||||||
|
notes_item = QTableWidgetItem(notes)
|
||||||
|
if "✨ Recommended" in notes:
|
||||||
|
notes_item.setForeground(QColor('#00ff00')) # Green for recommended
|
||||||
|
elif "💾 Storage friendly" in notes:
|
||||||
|
notes_item.setForeground(QColor('#00ccff')) # Blue for storage friendly
|
||||||
|
elif "📱 Mobile friendly" in notes:
|
||||||
|
notes_item.setForeground(QColor('#ff9900')) # Orange for mobile
|
||||||
|
self.format_table.setItem(row, 3, notes_item)
|
||||||
|
else:
|
||||||
|
# Extension for normal mode (column 2)
|
||||||
|
self.format_table.setItem(row, 2, QTableWidgetItem(f.get('ext', '').upper()))
|
||||||
|
|
||||||
|
# Column 4 in playlist mode, Column 6 in normal mode: Audio Status
|
||||||
|
needs_audio = f.get('acodec') == 'none' and f.get('vcodec') != 'none' # Only mark video-only as needing merge
|
||||||
|
audio_status = "Will merge audio" if needs_audio else ("✓ Has Audio" if f.get('vcodec') != 'none' else "Audio Only")
|
||||||
|
audio_item = QTableWidgetItem(audio_status)
|
||||||
|
if needs_audio:
|
||||||
|
audio_item.setForeground(QColor('#ffa500'))
|
||||||
|
elif audio_status == "Audio Only":
|
||||||
|
audio_item.setForeground(QColor('#cccccc')) # Neutral color for audio only
|
||||||
|
else: # Has Audio (Video+Audio)
|
||||||
|
audio_item.setForeground(QColor('#00cc00')) # Green for included audio
|
||||||
|
# Set item for correct column based on mode
|
||||||
|
audio_column_index = 4 if is_playlist_mode else 6
|
||||||
|
self.format_table.setItem(row, audio_column_index, audio_item)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Populate columns only shown in non-playlist mode ---
|
||||||
|
if not is_playlist_mode:
|
||||||
|
# Column 3: Resolution
|
||||||
self.format_table.setItem(row, 3, QTableWidgetItem(resolution))
|
self.format_table.setItem(row, 3, QTableWidgetItem(resolution))
|
||||||
|
|
||||||
# File Size
|
# Column 4: File Size
|
||||||
filesize = f"{f.get('filesize', 0) / 1024 / 1024:.2f} MB"
|
filesize = f"{f.get('filesize', 0) / 1024 / 1024:.2f} MB"
|
||||||
self.format_table.setItem(row, 4, QTableWidgetItem(filesize))
|
self.format_table.setItem(row, 4, QTableWidgetItem(filesize))
|
||||||
|
|
||||||
# Codec
|
# Column 5: Codec
|
||||||
if f.get('vcodec') == 'none':
|
if f.get('vcodec') == 'none':
|
||||||
codec = f.get('acodec', 'N/A')
|
codec = f.get('acodec', 'N/A')
|
||||||
else:
|
else:
|
||||||
@@ -205,15 +295,7 @@ class FormatTableMixin:
|
|||||||
codec += f" / {f.get('acodec', 'N/A')}"
|
codec += f" / {f.get('acodec', 'N/A')}"
|
||||||
self.format_table.setItem(row, 5, QTableWidgetItem(codec))
|
self.format_table.setItem(row, 5, QTableWidgetItem(codec))
|
||||||
|
|
||||||
# Audio Status
|
# Column 7: Notes
|
||||||
needs_audio = f.get('acodec') == 'none'
|
|
||||||
audio_status = "Will merge audio" if needs_audio else "✓ Has Audio"
|
|
||||||
audio_item = QTableWidgetItem(audio_status)
|
|
||||||
if needs_audio:
|
|
||||||
audio_item.setForeground(QColor('#ffa500'))
|
|
||||||
self.format_table.setItem(row, 6, audio_item)
|
|
||||||
|
|
||||||
# Add Notes column
|
|
||||||
notes = self.get_format_notes(f, best_video_size)
|
notes = self.get_format_notes(f, best_video_size)
|
||||||
notes_item = QTableWidgetItem(notes)
|
notes_item = QTableWidgetItem(notes)
|
||||||
if "✨ Recommended" in notes:
|
if "✨ Recommended" in notes:
|
||||||
|
|||||||
+517
-157
File diff suppressed because it is too large
Load Diff
+110
-128
@@ -17,6 +17,7 @@ from packaging import version
|
|||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
import yt_dlp
|
import yt_dlp
|
||||||
|
from ytsage_gui_dialogs import SubtitleSelectionDialog
|
||||||
|
|
||||||
class VideoInfoMixin:
|
class VideoInfoMixin:
|
||||||
def setup_video_info_section(self):
|
def setup_video_info_section(self):
|
||||||
@@ -55,9 +56,10 @@ class VideoInfoMixin:
|
|||||||
self.views_label = QLabel()
|
self.views_label = QLabel()
|
||||||
self.date_label = QLabel()
|
self.date_label = QLabel()
|
||||||
self.duration_label = QLabel()
|
self.duration_label = QLabel()
|
||||||
|
self.like_count_label = QLabel()
|
||||||
|
|
||||||
# Style the info labels
|
# Style the info labels
|
||||||
for label in [self.channel_label, self.views_label, self.date_label, self.duration_label]:
|
for label in [self.channel_label, self.views_label, self.date_label, self.duration_label, self.like_count_label]:
|
||||||
label.setStyleSheet("""
|
label.setStyleSheet("""
|
||||||
QLabel {
|
QLabel {
|
||||||
color: #cccccc;
|
color: #cccccc;
|
||||||
@@ -70,114 +72,52 @@ class VideoInfoMixin:
|
|||||||
video_info_layout.addWidget(self.title_label)
|
video_info_layout.addWidget(self.title_label)
|
||||||
video_info_layout.addWidget(self.channel_label)
|
video_info_layout.addWidget(self.channel_label)
|
||||||
video_info_layout.addWidget(self.views_label)
|
video_info_layout.addWidget(self.views_label)
|
||||||
|
video_info_layout.addWidget(self.like_count_label)
|
||||||
video_info_layout.addWidget(self.date_label)
|
video_info_layout.addWidget(self.date_label)
|
||||||
video_info_layout.addWidget(self.duration_label)
|
video_info_layout.addWidget(self.duration_label)
|
||||||
|
|
||||||
# Add spacing before subtitle section
|
# Add spacing before subtitle section
|
||||||
video_info_layout.addSpacing(10)
|
video_info_layout.addSpacing(10)
|
||||||
|
|
||||||
# Create a horizontal layout for subtitle controls
|
# --- Subtitle Section ---
|
||||||
subtitle_layout = QHBoxLayout()
|
subtitle_layout = QHBoxLayout()
|
||||||
subtitle_layout.setSpacing(5) # Reduce spacing between elements
|
subtitle_layout.setSpacing(10)
|
||||||
|
|
||||||
# Create subtitle button
|
# Subtitle selection button
|
||||||
self.subtitle_check = QPushButton("Download Subtitles")
|
self.subtitle_select_btn = QPushButton("Select Subtitles...") # Renamed & changed text
|
||||||
self.subtitle_check.setFixedHeight(30)
|
self.subtitle_select_btn.setFixedHeight(30)
|
||||||
self.subtitle_check.setFixedWidth(150) # Set fixed width
|
# self.subtitle_select_btn.setFixedWidth(150) # Let it size naturally or adjust as needed
|
||||||
self.subtitle_check.setCheckable(True)
|
self.subtitle_select_btn.clicked.connect(self.open_subtitle_dialog)
|
||||||
self.subtitle_check.clicked.connect(self.toggle_subtitle_controls)
|
self.subtitle_select_btn.setStyleSheet("""
|
||||||
self.subtitle_check.setStyleSheet("""
|
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #363636;
|
background-color: #1d1e22;
|
||||||
border: 2px solid #3d3d3d;
|
border: 2px solid #1d1e22;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 5px;
|
padding: 5px 10px; /* Adjusted padding */
|
||||||
min-height: 30px;
|
|
||||||
}
|
}
|
||||||
QPushButton:checked {
|
QPushButton:hover { background-color: #2a2d36; }
|
||||||
background-color: #ff0000;
|
/* Optional: Style differently if subtitles ARE selected */
|
||||||
border-color: #cc0000;
|
QPushButton[subtitlesSelected="true"] {
|
||||||
|
border-color: #c90000; /* Indicate selection */
|
||||||
|
}
|
||||||
|
/* Style for disabled state */
|
||||||
|
QPushButton:disabled {
|
||||||
|
background-color: #3d3d3d;
|
||||||
|
color: #888888;
|
||||||
|
border-color: #3d3d3d;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
subtitle_layout.addWidget(self.subtitle_check)
|
self.subtitle_select_btn.setProperty("subtitlesSelected", False) # Custom property for styling
|
||||||
|
subtitle_layout.addWidget(self.subtitle_select_btn)
|
||||||
|
|
||||||
# Create subtitle combo box
|
# Label to show number of selected subtitles
|
||||||
self.subtitle_combo = QComboBox()
|
self.selected_subs_label = QLabel("0 selected")
|
||||||
self.subtitle_combo.setFixedHeight(30)
|
self.selected_subs_label.setStyleSheet("color: #cccccc; padding-left: 5px;")
|
||||||
self.subtitle_combo.setFixedWidth(200)
|
subtitle_layout.addWidget(self.selected_subs_label)
|
||||||
self.subtitle_combo.setVisible(False)
|
|
||||||
self.subtitle_combo.setStyleSheet("""
|
|
||||||
QComboBox {
|
|
||||||
background-color: #363636;
|
|
||||||
border: 2px solid #3d3d3d;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 5px;
|
|
||||||
min-height: 30px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
subtitle_layout.addWidget(self.subtitle_combo)
|
|
||||||
|
|
||||||
# Create merge subtitles checkbox
|
# Add the subtitle layout to the main video info layout
|
||||||
self.merge_subs_checkbox = QCheckBox("Merge Subtitles")
|
|
||||||
self.merge_subs_checkbox.setFixedHeight(30)
|
|
||||||
self.merge_subs_checkbox.setVisible(False)
|
|
||||||
self.merge_subs_checkbox.setStyleSheet("""
|
|
||||||
QCheckBox {
|
|
||||||
color: #ffffff;
|
|
||||||
padding: 5px;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
QCheckBox::indicator {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-radius: 9px;
|
|
||||||
}
|
|
||||||
QCheckBox::indicator:unchecked {
|
|
||||||
border: 2px solid #666666;
|
|
||||||
background: #2b2b2b;
|
|
||||||
border-radius: 9px;
|
|
||||||
}
|
|
||||||
QCheckBox::indicator:checked {
|
|
||||||
border: 2px solid #ff0000;
|
|
||||||
background: #ff0000;
|
|
||||||
border-radius: 9px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
subtitle_layout.addWidget(self.merge_subs_checkbox)
|
|
||||||
|
|
||||||
# Add stretch to push everything to the left
|
|
||||||
subtitle_layout.addStretch()
|
|
||||||
|
|
||||||
# Create a second row for the filter input
|
|
||||||
filter_layout = QHBoxLayout()
|
|
||||||
filter_layout.setSpacing(5)
|
|
||||||
|
|
||||||
# Create subtitle filter input
|
|
||||||
self.subtitle_filter_input = QLineEdit()
|
|
||||||
self.subtitle_filter_input.setFixedHeight(30)
|
|
||||||
self.subtitle_filter_input.setFixedWidth(200)
|
|
||||||
self.subtitle_filter_input.setPlaceholderText("Filter languages (e.g., en, es)")
|
|
||||||
self.subtitle_filter_input.textChanged.connect(self.filter_subtitles)
|
|
||||||
self.subtitle_filter_input.setVisible(False)
|
|
||||||
self.subtitle_filter_input.setStyleSheet("""
|
|
||||||
QLineEdit {
|
|
||||||
background-color: #363636;
|
|
||||||
border: 2px solid #3d3d3d;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 5px;
|
|
||||||
min-height: 30px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
QLineEdit:focus {
|
|
||||||
border-color: #ff0000;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
filter_layout.addWidget(self.subtitle_filter_input)
|
|
||||||
filter_layout.addStretch()
|
|
||||||
|
|
||||||
# Add both layouts to video info
|
|
||||||
video_info_layout.addLayout(subtitle_layout)
|
video_info_layout.addLayout(subtitle_layout)
|
||||||
video_info_layout.addLayout(filter_layout)
|
# --- End Subtitle Section ---
|
||||||
|
|
||||||
# Add stretch at the bottom
|
# Add stretch at the bottom
|
||||||
video_info_layout.addStretch()
|
video_info_layout.addStretch()
|
||||||
@@ -193,11 +133,11 @@ class VideoInfoMixin:
|
|||||||
self.playlist_info_label.setStyleSheet("""
|
self.playlist_info_label.setStyleSheet("""
|
||||||
QLabel {
|
QLabel {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #ff9900;
|
color: #ffffff;
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: #2b2b2b;
|
background-color: #1d1e22;
|
||||||
border: 1px solid #3d3d3d;
|
border: 1px solid #c90000;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
max-height: 30px;
|
max-height: 30px;
|
||||||
@@ -207,9 +147,37 @@ class VideoInfoMixin:
|
|||||||
return self.playlist_info_label
|
return self.playlist_info_label
|
||||||
|
|
||||||
def update_video_info(self, info):
|
def update_video_info(self, info):
|
||||||
|
if hasattr(self, 'is_playlist') and self.is_playlist:
|
||||||
|
# Playlist Mode: Show playlist title and video count
|
||||||
|
self.title_label.setText(self.playlist_info.get('title', 'Unknown Playlist'))
|
||||||
|
|
||||||
|
num_videos = len(getattr(self, 'playlist_entries', []))
|
||||||
|
self.duration_label.setText(f"Total Videos: {num_videos}")
|
||||||
|
|
||||||
|
# Hide video-specific info
|
||||||
|
self.channel_label.setText("")
|
||||||
|
self.views_label.setText("")
|
||||||
|
self.date_label.setText("")
|
||||||
|
self.like_count_label.setText("")
|
||||||
|
self.channel_label.setVisible(False)
|
||||||
|
self.views_label.setVisible(False)
|
||||||
|
self.date_label.setVisible(False)
|
||||||
|
self.like_count_label.setVisible(False)
|
||||||
|
else:
|
||||||
|
# Single Video Mode: Show standard video info
|
||||||
|
# Ensure labels are visible first
|
||||||
|
self.channel_label.setVisible(True)
|
||||||
|
self.views_label.setVisible(True)
|
||||||
|
self.date_label.setVisible(True)
|
||||||
|
self.like_count_label.setVisible(True)
|
||||||
|
|
||||||
# Format view count with commas
|
# Format view count with commas
|
||||||
views = int(info.get('view_count', 0))
|
views = info.get('view_count')
|
||||||
formatted_views = f"{views:,}"
|
formatted_views = f"{views:,}" if views is not None else 'N/A'
|
||||||
|
|
||||||
|
# Format like count with commas
|
||||||
|
likes = info.get('like_count')
|
||||||
|
formatted_likes = f"{likes:,}" if likes is not None else 'N/A'
|
||||||
|
|
||||||
# Format upload date
|
# Format upload date
|
||||||
upload_date = info.get('upload_date', '')
|
upload_date = info.get('upload_date', '')
|
||||||
@@ -229,41 +197,59 @@ class VideoInfoMixin:
|
|||||||
self.title_label.setText(info.get('title', 'Unknown title'))
|
self.title_label.setText(info.get('title', 'Unknown title'))
|
||||||
self.channel_label.setText(f"Channel: {info.get('uploader', 'Unknown channel')}")
|
self.channel_label.setText(f"Channel: {info.get('uploader', 'Unknown channel')}")
|
||||||
self.views_label.setText(f"Views: {formatted_views}")
|
self.views_label.setText(f"Views: {formatted_views}")
|
||||||
|
self.like_count_label.setText(f"Likes: {formatted_likes}")
|
||||||
self.date_label.setText(f"Upload date: {formatted_date}")
|
self.date_label.setText(f"Upload date: {formatted_date}")
|
||||||
self.duration_label.setText(f"Duration: {duration_str}")
|
self.duration_label.setText(f"Duration: {duration_str}")
|
||||||
|
|
||||||
def toggle_subtitle_controls(self):
|
def open_subtitle_dialog(self):
|
||||||
is_checked = self.subtitle_check.isChecked()
|
if not hasattr(self, 'available_subtitles') or not hasattr(self, 'available_automatic_subtitles'):
|
||||||
self.subtitle_combo.setVisible(is_checked)
|
print("Subtitle info not loaded yet.")
|
||||||
self.subtitle_filter_input.setVisible(is_checked)
|
|
||||||
self.merge_subs_checkbox.setVisible(is_checked)
|
|
||||||
|
|
||||||
def update_subtitle_list(self):
|
|
||||||
self.subtitle_combo.clear()
|
|
||||||
|
|
||||||
if not (self.available_subtitles or self.available_automatic_subtitles):
|
|
||||||
self.subtitle_combo.addItem("No subtitles available")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Add subtitle options
|
if not hasattr(self, 'selected_subtitles'):
|
||||||
self.subtitle_combo.addItem("Select subtitle language")
|
self.selected_subtitles = []
|
||||||
|
|
||||||
# Filter and add subtitles
|
dialog = SubtitleSelectionDialog(
|
||||||
filter_text = self.subtitle_filter_input.text().lower()
|
self.available_subtitles,
|
||||||
|
self.available_automatic_subtitles,
|
||||||
|
self.selected_subtitles,
|
||||||
|
self # Parent for the dialog
|
||||||
|
)
|
||||||
|
|
||||||
# Add manual subtitles
|
# Access the main application window (parent of the mixin's widget)
|
||||||
for lang_code, subtitle_info in self.available_subtitles.items():
|
# to find the merge checkbox
|
||||||
if not filter_text or filter_text in lang_code.lower():
|
main_window = self # In this context, self should be the YTSageApp instance
|
||||||
self.subtitle_combo.addItem(f"{lang_code} - Manual")
|
if not isinstance(main_window, QMainWindow):
|
||||||
|
# If the structure is different, this might need adjustment
|
||||||
|
# Maybe self.parentWidget() or similar depending on how Mixin is used
|
||||||
|
print("Warning: Cannot find main window to access merge checkbox.")
|
||||||
|
merge_checkbox = None
|
||||||
|
else:
|
||||||
|
merge_checkbox = getattr(main_window, 'merge_subs_checkbox', None)
|
||||||
|
|
||||||
# Add auto-generated subtitles
|
|
||||||
for lang_code, subtitle_info in self.available_automatic_subtitles.items():
|
|
||||||
if not filter_text or filter_text in lang_code.lower():
|
|
||||||
self.subtitle_combo.addItem(f"{lang_code} - Auto-generated")
|
|
||||||
|
|
||||||
def filter_subtitles(self):
|
if dialog.exec(): # If user clicks OK
|
||||||
self.subtitle_filter = self.subtitle_filter_input.text()
|
self.selected_subtitles = dialog.get_selected_subtitles()
|
||||||
self.update_subtitle_list()
|
print(f"Selected subtitles: {self.selected_subtitles}")
|
||||||
|
# Update UI to reflect selection
|
||||||
|
count = len(self.selected_subtitles)
|
||||||
|
self.selected_subs_label.setText(f"{count} selected")
|
||||||
|
self.subtitle_select_btn.setProperty("subtitlesSelected", count > 0)
|
||||||
|
|
||||||
|
# Enable/disable the merge checkbox in the parent window
|
||||||
|
if merge_checkbox:
|
||||||
|
# Only enable merge checkbox if we're not in Audio Only mode
|
||||||
|
is_audio_only = hasattr(main_window, 'audio_button') and main_window.audio_button.isChecked()
|
||||||
|
# In audio-only mode, we still allow subtitle selection but not merging
|
||||||
|
should_enable = count > 0 and not is_audio_only
|
||||||
|
merge_checkbox.setEnabled(should_enable)
|
||||||
|
else:
|
||||||
|
print("Warning: merge_subs_checkbox not found on parent window.")
|
||||||
|
|
||||||
|
# Re-apply stylesheet to update button border if property changed
|
||||||
|
self.subtitle_select_btn.style().unpolish(self.subtitle_select_btn)
|
||||||
|
self.subtitle_select_btn.style().polish(self.subtitle_select_btn)
|
||||||
|
# No else needed for cancel, state remains unchanged
|
||||||
|
|
||||||
def download_thumbnail(self, url):
|
def download_thumbnail(self, url):
|
||||||
try:
|
try:
|
||||||
@@ -285,10 +271,6 @@ class VideoInfoMixin:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading thumbnail: {str(e)}")
|
print(f"Error loading thumbnail: {str(e)}")
|
||||||
|
|
||||||
def toggle_save_thumbnail(self):
|
|
||||||
self.save_thumbnail = self.save_thumbnail_checkbox.isChecked()
|
|
||||||
print(f"Save thumbnail toggled: {self.save_thumbnail}") # Debug print
|
|
||||||
|
|
||||||
def download_thumbnail_file(self, video_url, path):
|
def download_thumbnail_file(self, video_url, path):
|
||||||
if not self.save_thumbnail:
|
if not self.save_thumbnail:
|
||||||
return False
|
return False
|
||||||
|
|||||||
+29
-29
@@ -1,24 +1,24 @@
|
|||||||
MAIN_STYLE = """
|
MAIN_STYLE = """
|
||||||
QMainWindow {
|
QMainWindow {
|
||||||
background-color: #2b2b2b;
|
background-color: #15181b;
|
||||||
}
|
}
|
||||||
QWidget {
|
QWidget {
|
||||||
background-color: #2b2b2b;
|
background-color: #15181b;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
QLineEdit {
|
QLineEdit {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 2px solid #3d3d3d;
|
border: 2px solid #1b2021;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #363636;
|
background-color: #1b2021;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
selection-background-color: #ff0000;
|
selection-background-color: #c90000;
|
||||||
selection-color: #ffffff;
|
selection-color: #ffffff;
|
||||||
}
|
}
|
||||||
QPushButton {
|
QPushButton {
|
||||||
padding: 8px 15px;
|
padding: 8px 15px;
|
||||||
background-color: #ff0000; /* YouTube red */
|
background-color: #c90000;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -26,33 +26,33 @@ QPushButton {
|
|||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
}
|
}
|
||||||
QPushButton:hover {
|
QPushButton:hover {
|
||||||
background-color: #cc0000; /* Darker red on hover */
|
background-color: #a50000;
|
||||||
}
|
}
|
||||||
QPushButton:pressed {
|
QPushButton:pressed {
|
||||||
background-color: #990000; /* Even darker red when pressed */
|
background-color: #800000;
|
||||||
}
|
}
|
||||||
QPushButton:disabled {
|
QPushButton:disabled {
|
||||||
background-color: #666666; /* Gray when disabled */
|
background-color: #666666;
|
||||||
color: #999999;
|
color: #999999;
|
||||||
}
|
}
|
||||||
QTableWidget {
|
QTableWidget {
|
||||||
border: 2px solid #3d3d3d;
|
border: 2px solid #1b2021;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #363636;
|
background-color: #1b2021;
|
||||||
gridline-color: #3d3d3d;
|
gridline-color: #1b2021;
|
||||||
selection-background-color: #ff0000;
|
selection-background-color: #c90000;
|
||||||
selection-color: #ffffff;
|
selection-color: #ffffff;
|
||||||
}
|
}
|
||||||
QHeaderView::section {
|
QHeaderView::section {
|
||||||
background-color: #2b2b2b;
|
background-color: #15181b;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border: 1px solid #3d3d3d;
|
border: 1px solid #1b2021;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
QScrollBar:vertical {
|
QScrollBar:vertical {
|
||||||
border: none;
|
border: none;
|
||||||
background-color: #2b2b2b;
|
background-color: #15181b;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
@@ -62,27 +62,27 @@ QScrollBar::handle:vertical {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
QScrollBar::handle:vertical:hover {
|
QScrollBar::handle:vertical:hover {
|
||||||
background-color: #ff0000;
|
background-color: #c90000;
|
||||||
}
|
}
|
||||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
||||||
height: 0px;
|
height: 0px;
|
||||||
}
|
}
|
||||||
QProgressBar {
|
QProgressBar {
|
||||||
border: 2px solid #3d3d3d;
|
border: 2px solid #1b2021;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #363636;
|
background-color: #1b2021;
|
||||||
}
|
}
|
||||||
QProgressBar::chunk {
|
QProgressBar::chunk {
|
||||||
background-color: #ff0000;
|
background-color: #c90000;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
QComboBox {
|
QComboBox {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border: 2px solid #3d3d3d;
|
border: 2px solid #1b2021;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #363636;
|
background-color: #1b2021;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
}
|
}
|
||||||
@@ -106,25 +106,25 @@ QCheckBox::indicator {
|
|||||||
}
|
}
|
||||||
QCheckBox::indicator:unchecked {
|
QCheckBox::indicator:unchecked {
|
||||||
border: 2px solid #666666;
|
border: 2px solid #666666;
|
||||||
background: #2b2b2b;
|
background: #15181b;
|
||||||
}
|
}
|
||||||
QCheckBox::indicator:checked {
|
QCheckBox::indicator:checked {
|
||||||
border: 2px solid #ff0000;
|
border: 2px solid #c90000;
|
||||||
background: #ff0000;
|
background: #c90000;
|
||||||
}
|
}
|
||||||
QLabel {
|
QLabel {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
QTextEdit, QPlainTextEdit {
|
QTextEdit, QPlainTextEdit {
|
||||||
background-color: #363636;
|
background-color: #1b2021;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border: 2px solid #3d3d3d;
|
border: 2px solid #1b2021;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
selection-background-color: #ff0000;
|
selection-background-color: #c90000;
|
||||||
selection-color: #ffffff;
|
selection-color: #ffffff;
|
||||||
}
|
}
|
||||||
QMessageBox {
|
QMessageBox {
|
||||||
background-color: #2b2b2b;
|
background-color: #15181b;
|
||||||
}
|
}
|
||||||
QMessageBox QLabel {
|
QMessageBox QLabel {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
|||||||
+14
-34
@@ -4,6 +4,7 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import shutil
|
||||||
from ytsage_ffmpeg import check_ffmpeg_installed, get_ffmpeg_install_path
|
from ytsage_ffmpeg import check_ffmpeg_installed, get_ffmpeg_install_path
|
||||||
|
|
||||||
def check_ffmpeg():
|
def check_ffmpeg():
|
||||||
@@ -49,45 +50,24 @@ def check_ffmpeg():
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def get_yt_dlp_path():
|
def get_yt_dlp_path():
|
||||||
"""Get the appropriate yt-dlp path with enhanced error handling."""
|
"""Get the yt-dlp command or path, prioritizing the system PATH."""
|
||||||
try:
|
try:
|
||||||
if getattr(sys, 'frozen', False):
|
# Use shutil.which to find yt-dlp in the system's PATH
|
||||||
if sys.platform == 'darwin':
|
yt_dlp_executable = shutil.which('yt-dlp')
|
||||||
# For macOS .app bundle
|
|
||||||
if 'Contents/MacOS' in sys.executable:
|
|
||||||
base_path = os.path.dirname(sys.executable)
|
|
||||||
else:
|
|
||||||
# Fallback to user's home directory for macOS
|
|
||||||
base_path = os.path.expanduser('~/Library/Application Support/YTSage')
|
|
||||||
elif sys.platform == 'win32':
|
|
||||||
# For Windows executable
|
|
||||||
app_data = os.getenv('APPDATA')
|
|
||||||
base_path = os.path.join(app_data, 'YTSage') if app_data else os.path.dirname(sys.executable)
|
|
||||||
else:
|
|
||||||
# For Linux AppImage or binary
|
|
||||||
if 'APPIMAGE' in os.environ:
|
|
||||||
xdg_data = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
|
|
||||||
base_path = os.path.join(xdg_data, 'YTSage')
|
|
||||||
else:
|
|
||||||
base_path = os.path.dirname(sys.executable)
|
|
||||||
|
|
||||||
# Create directory if it doesn't exist
|
if yt_dlp_executable:
|
||||||
try:
|
print(f"Found yt-dlp executable in PATH: {yt_dlp_executable}")
|
||||||
os.makedirs(base_path, exist_ok=True)
|
return yt_dlp_executable
|
||||||
except Exception as e:
|
|
||||||
print(f"Error creating directory: {e}")
|
|
||||||
base_path = os.path.dirname(sys.executable)
|
|
||||||
|
|
||||||
return os.path.join(base_path, 'yt-dlp.exe' if sys.platform == 'win32' else 'yt-dlp')
|
|
||||||
else:
|
else:
|
||||||
# For development/script mode
|
# If not found in PATH, assume 'yt-dlp' is the command name
|
||||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
print("yt-dlp not found in PATH. Will attempt to use 'yt-dlp' as the command.")
|
||||||
'yt-dlp.exe' if sys.platform == 'win32' else 'yt-dlp')
|
return 'yt-dlp'
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error determining yt-dlp path: {e}")
|
print(f"Error finding yt-dlp path: {e}")
|
||||||
# Fallback to current directory
|
# Fallback to the command name on any error
|
||||||
return os.path.join(os.getcwd(), 'yt-dlp.exe' if sys.platform == 'win32' else 'yt-dlp')
|
print("An error occurred during yt-dlp path detection. Falling back to command 'yt-dlp'.")
|
||||||
|
return 'yt-dlp'
|
||||||
|
|
||||||
def load_saved_path(main_window_instance):
|
def load_saved_path(main_window_instance):
|
||||||
"""Load saved download path with enhanced error handling."""
|
"""Load saved download path with enhanced error handling."""
|
||||||
|
|||||||
Reference in New Issue
Block a user