Refactor and enhance yt-dlp setup dialog UI

Moved ytsage_yt_dlp.py to src/core/ and improved the YtdlpSetupDialog UI with better layout, styling, and accessibility. Replaced print statements with logger calls, added a check_ytdlp_installed() utility, and refined icon path resolution. These changes improve user experience and maintainability.
This commit is contained in:
Your Name
2025-08-16 15:05:51 +03:00
parent 0cb40f4338
commit 1fcde5d6b9
+141 -50
View File
@@ -7,9 +7,10 @@ import requests
from pathlib import Path from pathlib import Path
from PySide6.QtWidgets import (QDialog, QVBoxLayout, QLabel, QPushButton, from PySide6.QtWidgets import (QDialog, QVBoxLayout, QLabel, QPushButton,
QProgressBar, QRadioButton, QHBoxLayout, QProgressBar, QRadioButton, QHBoxLayout,
QMessageBox, QFileDialog) QMessageBox, QFileDialog, QWidget)
from PySide6.QtCore import QThread, Signal, Qt from PySide6.QtCore import QThread, Signal, Qt
from PySide6.QtGui import QIcon from PySide6.QtGui import QIcon
from .ytsage_logging import logger
# Define binary URLs # Define binary URLs
YTDLP_URLS = { YTDLP_URLS = {
@@ -105,14 +106,20 @@ class YtdlpSetupDialog(QDialog):
super().__init__(parent) super().__init__(parent)
self.os_type = get_os_type() self.os_type = get_os_type()
self.setWindowTitle("yt-dlp Setup Required") self.setWindowTitle("yt-dlp Setup Required")
self.resize(500, 240) self.setMinimumWidth(520)
self.setMinimumHeight(350)
self.resize(520, 380)
# Set the window icon to match the main app # Set the window icon to match the main app
if parent: if parent and parent.windowIcon():
self.setWindowIcon(parent.windowIcon()) self.setWindowIcon(parent.windowIcon())
else: else:
# Try to load the icon directly if parent not available # Try to load the icon directly if parent not available
icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Icon', 'icon.png') # Navigate from src/core/ to project root, then to assets/Icon/
current_dir = os.path.dirname(os.path.abspath(__file__)) # core/
src_dir = os.path.dirname(current_dir) # src/
project_root = os.path.dirname(src_dir) # project root
icon_path = os.path.join(project_root, 'assets', 'Icon', 'icon.png')
if os.path.exists(icon_path): if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path)) self.setWindowIcon(QIcon(icon_path))
@@ -125,15 +132,19 @@ class YtdlpSetupDialog(QDialog):
color: #ffffff; color: #ffffff;
} }
QLabel { QLabel {
color: #ffffff; color: #cccccc;
line-height: 1.4;
} }
QPushButton { QPushButton {
padding: 8px 15px; padding: 10px 20px;
background-color: #c90000; background-color: #c90000;
border: none; border: none;
border-radius: 4px; border-radius: 6px;
color: white; color: white;
font-weight: bold; font-weight: bold;
font-size: 13px;
min-width: 120px;
min-height: 20px;
} }
QPushButton:hover { QPushButton:hover {
background-color: #a50000; background-color: #a50000;
@@ -141,19 +152,30 @@ class YtdlpSetupDialog(QDialog):
QPushButton:pressed { QPushButton:pressed {
background-color: #800000; background-color: #800000;
} }
QPushButton:disabled {
background-color: #666666;
color: #999999;
}
QProgressBar { QProgressBar {
border: 2px solid #1b2021; border: 2px solid #1d1e22;
border-radius: 4px; border-radius: 6px;
text-align: center; text-align: center;
color: white; color: white;
background-color: #1d1e22;
height: 25px;
font-weight: bold;
} }
QProgressBar::chunk { QProgressBar::chunk {
background-color: #c90000; background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,
border-radius: 2px; stop: 0 #e60000, stop: 0.5 #ff3333, stop: 1 #c90000);
border-radius: 4px;
margin: 1px;
} }
QRadioButton { QRadioButton {
color: #ffffff; color: #ffffff;
spacing: 10px; spacing: 10px;
padding: 8px;
font-size: 13px;
} }
QRadioButton::indicator { QRadioButton::indicator {
width: 18px; width: 18px;
@@ -172,8 +194,16 @@ class YtdlpSetupDialog(QDialog):
def init_ui(self): def init_ui(self):
layout = QVBoxLayout() layout = QVBoxLayout()
layout.setSpacing(15)
layout.setContentsMargins(25, 25, 25, 25)
# Information label # Header title
title_label = QLabel("yt-dlp Setup Required")
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; padding: 5px 0;")
title_label.setAlignment(Qt.AlignCenter)
layout.addWidget(title_label)
# Information label with improved styling
if self.os_type == "windows": if self.os_type == "windows":
os_name = "Windows" os_name = "Windows"
elif self.os_type == "macos": elif self.os_type == "macos":
@@ -181,16 +211,20 @@ class YtdlpSetupDialog(QDialog):
else: else:
os_name = "Linux" os_name = "Linux"
info_label = QLabel(f"<b>YTSage requires yt-dlp to download videos</b><br><br>" info_label = QLabel(f"YTSage requires yt-dlp to download videos.<br><br>"
f"yt-dlp was not found in the app's local directory. " f"yt-dlp was not found in the app's local directory. "
f"YTSage needs to set up yt-dlp for your {os_name} system.<br><br>" f"YTSage needs to set up yt-dlp for your {os_name} system.<br><br>"
f"Please choose an option below:") f"Please choose an option below:")
info_label.setAlignment(Qt.AlignCenter) info_label.setAlignment(Qt.AlignCenter)
info_label.setWordWrap(True) info_label.setWordWrap(True)
info_label.setStyleSheet("font-size: 13px; color: #cccccc; padding: 5px; line-height: 1.4;")
layout.addWidget(info_label) layout.addWidget(info_label)
# Radio buttons for choices # Radio buttons with minimal spacing
option_layout = QHBoxLayout() option_widget = QWidget()
option_layout = QVBoxLayout(option_widget)
option_layout.setSpacing(8)
option_layout.setContentsMargins(0, 0, 0, 0)
self.auto_radio = QRadioButton("Download automatically (Recommended)") self.auto_radio = QRadioButton("Download automatically (Recommended)")
self.auto_radio.setChecked(True) self.auto_radio.setChecked(True)
@@ -198,20 +232,45 @@ class YtdlpSetupDialog(QDialog):
option_layout.addWidget(self.auto_radio) option_layout.addWidget(self.auto_radio)
option_layout.addWidget(self.manual_radio) option_layout.addWidget(self.manual_radio)
layout.addLayout(option_layout) layout.addWidget(option_widget)
# Progress bar (hidden initially) # Progress bar with proper sizing
self.progress_bar = QProgressBar() self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False) self.progress_bar.setVisible(False)
self.progress_bar.setFixedHeight(20) # Fixed height for consistency
self.progress_bar.setStyleSheet("""
QProgressBar {
border: 1px solid #3d3d3d;
border-radius: 8px;
background-color: #1d1e22;
text-align: center;
color: #ffffff;
font-size: 12px;
font-weight: bold;
height: 20px;
}
QProgressBar::chunk {
background-color: #c90000;
border-radius: 6px;
margin: 1px;
}
""")
layout.addWidget(self.progress_bar) layout.addWidget(self.progress_bar)
# Status label # Status label with better spacing
self.status_label = QLabel("") self.status_label = QLabel("")
self.status_label.setAlignment(Qt.AlignCenter) self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet("font-size: 12px; color: #cccccc; padding: 8px 0;")
self.status_label.setWordWrap(True)
layout.addWidget(self.status_label) layout.addWidget(self.status_label)
# Button layout # Add stretch to push buttons to bottom
layout.addStretch()
# Button layout with improved spacing
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
button_layout.setSpacing(15)
button_layout.setContentsMargins(0, 10, 0, 0) # Add top margin for buttons
self.setup_button = QPushButton("Setup yt-dlp") self.setup_button = QPushButton("Setup yt-dlp")
self.setup_button.clicked.connect(self.setup_ytdlp) self.setup_button.clicked.connect(self.setup_ytdlp)
@@ -319,7 +378,7 @@ class YtdlpSetupDialog(QDialog):
) )
if file_path: if file_path:
print(f"DEBUG: User selected file: {file_path}") logger.debug(f"User selected file: {file_path}")
# Verify the selected file # Verify the selected file
try: try:
# Set up startupinfo to hide console window on Windows # Set up startupinfo to hide console window on Windows
@@ -330,7 +389,7 @@ class YtdlpSetupDialog(QDialog):
startupinfo.wShowWindow = 0 # SW_HIDE startupinfo.wShowWindow = 0 # SW_HIDE
# Try to run yt-dlp --version # Try to run yt-dlp --version
print(f"DEBUG: Verifying file with --version command") logger.debug(f"Verifying file with --version command")
result = subprocess.run( result = subprocess.run(
[file_path, "--version"], [file_path, "--version"],
capture_output=True, capture_output=True,
@@ -338,38 +397,38 @@ class YtdlpSetupDialog(QDialog):
check=False, check=False,
startupinfo=startupinfo startupinfo=startupinfo
) )
print(f"DEBUG: Version check result: {result.returncode}, Output: {result.stdout.strip()}") logger.debug(f"Version check result: {result.returncode}, Output: {result.stdout.strip()}")
if result.returncode == 0: if result.returncode == 0:
# File is valid, copy it to our app's bin directory # File is valid, copy it to our app's bin directory
try: try:
# Ensure the bin directory exists # Ensure the bin directory exists
install_dir = ensure_install_dir_exists() install_dir = ensure_install_dir_exists()
print(f"DEBUG: Install directory: {install_dir}") logger.debug(f"Install directory: {install_dir}")
# Determine the target filename based on OS # Determine the target filename based on OS
if self.os_type == "windows": if self.os_type == "windows":
target_path = os.path.join(install_dir, "yt-dlp.exe") target_path = os.path.join(install_dir, "yt-dlp.exe")
else: else:
target_path = os.path.join(install_dir, "yt-dlp") target_path = os.path.join(install_dir, "yt-dlp")
print(f"DEBUG: Target path: {target_path}") logger.debug(f"Target path: {target_path}")
# Copy the file # Copy the file
shutil.copy2(file_path, target_path) shutil.copy2(file_path, target_path)
print(f"DEBUG: File copied successfully") logger.debug(f"File copied successfully")
# Set executable permissions on Unix systems # Set executable permissions on Unix systems
if self.os_type != "windows": if self.os_type != "windows":
os.chmod(target_path, 0o755) os.chmod(target_path, 0o755)
print(f"DEBUG: Permissions set on Unix system") logger.debug(f"Permissions set on Unix system")
# Return the path of the copied file # Return the path of the copied file
self.status_label.setText(f"yt-dlp successfully copied to {target_path}") self.status_label.setText(f"yt-dlp successfully copied to {target_path}")
print(f"DEBUG: Emitting setup_complete signal with path: {target_path}") logger.debug(f"Emitting setup_complete signal with path: {target_path}")
self.setup_complete.emit(target_path) self.setup_complete.emit(target_path)
self.accept() self.accept()
except Exception as copy_error: except Exception as copy_error:
print(f"DEBUG: Error copying file: {str(copy_error)}") logger.debug(f"Error copying file: {str(copy_error)}")
error_dialog = QMessageBox(self) error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Critical) error_dialog.setIcon(QMessageBox.Critical)
error_dialog.setWindowTitle("Setup Error") error_dialog.setWindowTitle("Setup Error")
@@ -396,7 +455,7 @@ class YtdlpSetupDialog(QDialog):
""") """)
error_dialog.exec() error_dialog.exec()
else: else:
print(f"DEBUG: File verification failed with return code: {result.returncode}") logger.debug(f"File verification failed with return code: {result.returncode}")
error_dialog = QMessageBox(self) error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Warning) error_dialog.setIcon(QMessageBox.Warning)
error_dialog.setWindowTitle("Invalid Executable") error_dialog.setWindowTitle("Invalid Executable")
@@ -423,7 +482,7 @@ class YtdlpSetupDialog(QDialog):
""") """)
error_dialog.exec() error_dialog.exec()
except Exception as e: except Exception as e:
print(f"DEBUG: Exception during verification: {str(e)}") logger.debug(f"Exception during verification: {str(e)}")
error_dialog = QMessageBox(self) error_dialog = QMessageBox(self)
error_dialog.setIcon(QMessageBox.Critical) error_dialog.setIcon(QMessageBox.Critical)
error_dialog.setWindowTitle("Error") error_dialog.setWindowTitle("Error")
@@ -462,9 +521,9 @@ def check_ytdlp_binary():
if sys.platform != 'win32' and not os.access(exe_path, os.X_OK): if sys.platform != 'win32' and not os.access(exe_path, os.X_OK):
try: try:
os.chmod(exe_path, 0o755) os.chmod(exe_path, 0o755)
print(f"Fixed permissions on yt-dlp at {exe_path}") logger.info(f"Fixed permissions on yt-dlp at {exe_path}")
except Exception as e: except Exception as e:
print(f"Warning: Could not set executable permissions on {exe_path}: {e}") logger.warning(f"Could not set executable permissions on {exe_path}: {e}")
return None return None
return exe_path return exe_path
@@ -488,21 +547,53 @@ def check_ytdlp_binary():
) )
if result.returncode == 0 and result.stdout.strip(): if result.returncode == 0 and result.stdout.strip():
yt_dlp_path = result.stdout.strip().split('\n')[0] yt_dlp_path = result.stdout.strip().split('\n')[0]
print(f"Found yt-dlp in PATH: {yt_dlp_path}") logger.info(f"Found yt-dlp in PATH: {yt_dlp_path}")
return yt_dlp_path return yt_dlp_path
else: else:
# On Unix systems, use 'which' command # On Unix systems, use 'which' command
result = subprocess.run(['which', 'yt-dlp'], capture_output=True, text=True, check=False) result = subprocess.run(['which', 'yt-dlp'], capture_output=True, text=True, check=False)
if result.returncode == 0 and result.stdout.strip(): if result.returncode == 0 and result.stdout.strip():
yt_dlp_path = result.stdout.strip() yt_dlp_path = result.stdout.strip()
print(f"Found yt-dlp in PATH: {yt_dlp_path}") logger.info(f"Found yt-dlp in PATH: {yt_dlp_path}")
return yt_dlp_path return yt_dlp_path
except Exception as e: except Exception as e:
print(f"Error checking for yt-dlp in PATH: {e}") logger.error(f"Error checking for yt-dlp in PATH: {e}")
# We're only interested in our app-specific installation or system PATH # We're only interested in our app-specific installation or system PATH
return None return None
def check_ytdlp_installed():
"""
Check if yt-dlp is installed and accessible.
Returns:
bool: True if yt-dlp is found and working, False otherwise
"""
try:
ytdlp_path = check_ytdlp_binary()
if ytdlp_path:
# Try to run yt-dlp --version to verify it's working
try:
# Create startupinfo to hide console on Windows
startupinfo = None
if sys.platform == 'win32' and hasattr(subprocess, 'STARTUPINFO'):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = 0 # SW_HIDE
result = subprocess.run(
[ytdlp_path, '--version'],
capture_output=True,
text=True,
timeout=5,
startupinfo=startupinfo
)
return result.returncode == 0
except Exception:
return False
return False
except Exception:
return False
def get_yt_dlp_path(): def get_yt_dlp_path():
""" """
Get the yt-dlp path, either from the app's bin directory or system PATH. Get the yt-dlp path, either from the app's bin directory or system PATH.
@@ -513,11 +604,11 @@ def get_yt_dlp_path():
# First check if we have yt-dlp in our app's bin directory or system PATH # First check if we have yt-dlp in our app's bin directory or system PATH
ytdlp_path = check_ytdlp_binary() ytdlp_path = check_ytdlp_binary()
if ytdlp_path: if ytdlp_path:
print(f"Using yt-dlp from: {ytdlp_path}") logger.info(f"Using yt-dlp from: {ytdlp_path}")
return ytdlp_path return ytdlp_path
# If not found anywhere, fall back to the command name as a last resort # If not found anywhere, fall back to the command name as a last resort
print("yt-dlp not found in app directory or PATH, falling back to command name") logger.info("yt-dlp not found in app directory or PATH, falling back to command name")
return "yt-dlp" return "yt-dlp"
def setup_ytdlp(parent_widget=None): def setup_ytdlp(parent_widget=None):
@@ -526,14 +617,14 @@ def setup_ytdlp(parent_widget=None):
Returns: Returns:
str: Path to yt-dlp binary str: Path to yt-dlp binary
""" """
print("DEBUG: Starting yt-dlp setup dialog") logger.debug("Starting yt-dlp setup dialog")
dialog = YtdlpSetupDialog(parent_widget) dialog = YtdlpSetupDialog(parent_widget)
# Store the setup result from the signal # Store the setup result from the signal
setup_result = {"path": None} setup_result = {"path": None}
def on_setup_complete(path): def on_setup_complete(path):
print(f"DEBUG: Received setup_complete signal with path: {path}") logger.debug(f"Received setup_complete signal with path: {path}")
setup_result["path"] = path setup_result["path"] = path
# Connect to the setup_complete signal # Connect to the setup_complete signal
@@ -541,33 +632,33 @@ def setup_ytdlp(parent_widget=None):
# Show the dialog # Show the dialog
result = dialog.exec() result = dialog.exec()
print(f"DEBUG: Dialog result: {result} (Accepted={QDialog.Accepted})") logger.debug(f"Dialog result: {result} (Accepted={QDialog.Accepted})")
if result == QDialog.Accepted: if result == QDialog.Accepted:
# First check if we received a path from the signal # First check if we received a path from the signal
if setup_result["path"] and os.path.exists(setup_result["path"]): if setup_result["path"] and os.path.exists(setup_result["path"]):
print(f"DEBUG: Using path from signal: {setup_result['path']}") logger.debug(f"Using path from signal: {setup_result['path']}")
return setup_result["path"] return setup_result["path"]
# Get the expected path for verification as fallback # Get the expected path for verification as fallback
expected_path = get_ytdlp_executable_path() expected_path = get_ytdlp_executable_path()
print(f"DEBUG: Expected yt-dlp path: {expected_path}") logger.debug(f"Expected yt-dlp path: {expected_path}")
# Verify the path exists after dialog is accepted # Verify the path exists after dialog is accepted
if os.path.exists(expected_path): if os.path.exists(expected_path):
print(f"DEBUG: yt-dlp successfully found at expected path: {expected_path}") logger.debug(f"yt-dlp successfully found at expected path: {expected_path}")
return expected_path return expected_path
else: else:
print(f"DEBUG: Expected path does not exist, trying alternate detection") logger.debug(f"Expected path does not exist, trying alternate detection")
# Try to use the get_yt_dlp_path function to find yt-dlp elsewhere # Try to use the get_yt_dlp_path function to find yt-dlp elsewhere
yt_dlp_path = get_yt_dlp_path() yt_dlp_path = get_yt_dlp_path()
print(f"DEBUG: Alternate detection result: {yt_dlp_path}") logger.debug(f"Alternate detection result: {yt_dlp_path}")
if yt_dlp_path != "yt-dlp" and os.path.exists(yt_dlp_path): if yt_dlp_path != "yt-dlp" and os.path.exists(yt_dlp_path):
print(f"DEBUG: yt-dlp found at alternate location: {yt_dlp_path}") logger.debug(f"yt-dlp found at alternate location: {yt_dlp_path}")
return yt_dlp_path return yt_dlp_path
# Something went wrong, show an error message # Something went wrong, show an error message
print(f"DEBUG: Setup failed, showing error dialog") logger.debug(f"Setup failed, showing error dialog")
if parent_widget: if parent_widget:
error_dialog = QMessageBox(parent_widget) error_dialog = QMessageBox(parent_widget)
error_dialog.setIcon(QMessageBox.Warning) error_dialog.setIcon(QMessageBox.Warning)
@@ -596,10 +687,10 @@ def setup_ytdlp(parent_widget=None):
} }
""") """)
error_dialog.exec() error_dialog.exec()
print(f"WARNING: yt-dlp setup failed, path does not exist: {expected_path}") logger.warning(f"yt-dlp setup failed, path does not exist: {expected_path}")
else: else:
print("DEBUG: User cancelled the setup dialog") logger.debug("User cancelled the setup dialog")
# User cancelled or setup failed, return the fallback command # User cancelled or setup failed, return the fallback command
print("DEBUG: Returning fallback command 'yt-dlp'") logger.debug("Returning fallback command 'yt-dlp'")
return "yt-dlp" return "yt-dlp"