Refactor and enhance custom dialogs UI and logic

Refactored custom command execution to use a worker class with signals for better threading and output handling. Improved the CustomOptionsDialog with cookie source selection (file or browser), browser selection UI, and more robust cookie path handling. Enhanced UI styling, help texts, and input validation. Removed command preview from TimeRangeDialog and streamlined dialog layouts for clarity and usability.
This commit is contained in:
Your Name
2025-08-26 19:27:39 +03:00
parent 0db890dd81
commit eaf1786d21
@@ -8,9 +8,10 @@ import threading
from pathlib import Path
from typing import TYPE_CHECKING, cast
from PySide6.QtCore import Q_ARG, QMetaObject, Qt
from PySide6.QtCore import Q_ARG, QMetaObject, Qt, Signal, QObject
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QFileDialog,
@@ -20,6 +21,7 @@ from PySide6.QtWidgets import (
QLineEdit,
QPlainTextEdit,
QPushButton,
QRadioButton,
QTabWidget,
QTextEdit,
QVBoxLayout,
@@ -27,6 +29,7 @@ from PySide6.QtWidgets import (
)
from src.core.ytsage_yt_dlp import get_yt_dlp_path
from src.utils.ytsage_constants import YTDLP_DOCS_URL
try:
import yt_dlp
@@ -39,254 +42,69 @@ if TYPE_CHECKING:
from src.gui.ytsage_gui_main import YTSageApp # only for type hints (no runtime import)
class CustomCommandDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self._parent = self.parent()
self.setWindowTitle("Custom yt-dlp Command")
self.setMinimumSize(600, 400)
class CommandWorker(QObject):
"""Worker class for running yt-dlp commands in a separate thread"""
layout = QVBoxLayout(self)
# Signals for communicating with the main thread
output_received = Signal(str) # For command output lines
command_finished = Signal(bool, int) # For completion (success, exit_code)
error_occurred = Signal(str) # For errors
# Help text
help_text = QLabel(
"Enter custom yt-dlp commands below. The URL will be automatically appended.\n"
"Example: --extract-audio --audio-format mp3 --audio-quality 0\n"
"Note: Download path and output template will be preserved."
)
help_text.setWordWrap(True)
help_text.setStyleSheet("color: #999999; padding: 10px;")
layout.addWidget(help_text)
def __init__(self, command, url, path):
super().__init__()
self.command = command
self.url = url
self.path = path
# Command input
self.command_input = QPlainTextEdit()
self.command_input.setPlaceholderText("Enter yt-dlp arguments...")
self.command_input.setStyleSheet(
"""
QPlainTextEdit {
background-color: #1d1e22;
color: #ffffff;
border: 2px solid #1d1e22;
border-radius: 4px;
padding: 8px;
font-family: Consolas, monospace;
}
"""
)
layout.addWidget(self.command_input)
# Add SponsorBlock checkbox
self.sponsorblock_checkbox = QCheckBox("Remove Sponsor Segments")
self.sponsorblock_checkbox.setStyleSheet(
"""
QCheckBox {
color: #ffffff;
padding: 5px;
margin-left: 20px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 9px;
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
background: #1d1e22;
border-radius: 9px;
}
QCheckBox::indicator:checked {
border: 2px solid #c90000;
background: #c90000;
border-radius: 9px;
}
"""
)
layout.insertWidget(layout.indexOf(self.command_input), self.sponsorblock_checkbox)
# Buttons
button_layout = QHBoxLayout()
self.run_btn = QPushButton("Run Command")
self.run_btn.clicked.connect(self.run_custom_command)
self.close_btn = QPushButton("Close")
self.close_btn.clicked.connect(self.close)
button_layout.addWidget(self.run_btn)
button_layout.addWidget(self.close_btn)
layout.addLayout(button_layout)
# Log output
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setStyleSheet(
"""
QTextEdit {
background-color: #1d1e22;
color: #ffffff;
border: 2px solid #1d1e22;
border-radius: 4px;
padding: 8px;
font-family: Consolas, monospace;
font-size: 12px;
}
"""
)
layout.addWidget(self.log_output)
self.setStyleSheet(
"""
QDialog {
background-color: #15181b;
}
QPushButton {
padding: 8px 15px;
background-color: #c90000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #a50000;
}
"""
)
def run_custom_command(self) -> None:
url = self._parent.url_input.text().strip() # type: ignore[reportAttributeAccessIssue]
if not url:
self.log_output.append("Error: No URL provided")
return
command = self.command_input.toPlainText().strip()
path = self._parent.path_input.text().strip() # type: ignore[reportAttributeAccessIssue]
self.log_output.clear()
self.log_output.append(f"Running command with URL: {url}")
self.run_btn.setEnabled(False)
# Start command in thread
threading.Thread(target=self._run_command_thread, args=(command, url, path), daemon=True).start()
def _run_command_thread(self, command, url, path) -> None:
def run_command(self):
"""Run the yt-dlp command and emit signals for output"""
try:
class CommandLogger:
def debug(self, msg):
self.dialog.log_output.append(msg)
def warning(self, msg):
self.dialog.log_output.append(f"Warning: {msg}")
def error(self, msg):
self.dialog.log_output.append(f"Error: {msg}")
def __init__(self, dialog):
self.dialog = dialog
# Split command into arguments
args = command.split()
args = self.command.split()
# Base options
ydl_opts = {
"logger": CommandLogger(self),
"paths": {"home": path},
"debug_printout": True,
"postprocessors": [],
}
# Build the full command
yt_dlp_path = get_yt_dlp_path()
base_cmd = [yt_dlp_path] + args
# Add SponsorBlock options if enabled
if self.sponsorblock_checkbox.isChecked():
ydl_opts["postprocessors"].extend(
[
{
"key": "SponsorBlock",
"categories": ["sponsor", "selfpromo", "interaction"],
"api": "https://sponsor.ajay.app",
},
{
"key": "ModifyChapters",
"remove_sponsor_segments": [
"sponsor",
"selfpromo",
"interaction",
],
"sponsorblock_chapter_title": "[SponsorBlock]: %(category_names)l",
"force_keyframes": True,
},
]
# Add download path if specified
if self.path:
base_cmd.extend(["-P", self.path])
# Add URL at the end
base_cmd.append(self.url)
# Emit the full command
self.output_received.emit(f"🔧 Full command: {' '.join(str(cmd) for cmd in base_cmd)}")
self.output_received.emit("=" * 50)
# Run the command
proc = subprocess.Popen(
base_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
errors="replace",
)
# Add custom arguments
for i in range(0, len(args), 2):
if i + 1 < len(args):
key = args[i].lstrip("-").replace("-", "_")
value = args[i + 1]
try:
# Try to convert to appropriate type
if value.lower() in ("true", "false"):
value = value.lower() == "true"
elif value.isdigit():
value = int(value)
ydl_opts[key] = value
except:
ydl_opts[key] = value
# Stream output
for line in proc.stdout: # type: ignore[reportOptionalIterable]
if line.strip(): # Only show non-empty lines
self.output_received.emit(line.rstrip())
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
ret = proc.wait()
self.output_received.emit("=" * 50)
self.log_output.append("Command completed successfully")
if ret != 0:
self.output_received.emit(f"❌ Command failed with exit code {ret}")
self.command_finished.emit(False, ret)
else:
self.output_received.emit("✅ Command completed successfully!")
self.command_finished.emit(True, ret)
except Exception as e:
self.log_output.append(f"Error: {str(e)}")
finally:
self.run_btn.setEnabled(True)
class CookieLoginDialog(QDialog):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setWindowTitle("Login with Cookies")
self.setMinimumSize(400, 150)
layout = QVBoxLayout(self)
help_text = QLabel(
"Select the Netscape-format cookies file for logging in.\n"
"This allows downloading of private videos and premium quality audio."
)
help_text.setWordWrap(True)
help_text.setStyleSheet("color: #999999; padding: 10px;")
layout.addWidget(help_text)
# File path input and browse button
path_layout = QHBoxLayout()
self.cookie_path_input = QLineEdit()
self.cookie_path_input.setPlaceholderText("Path to cookies file (Netscape format)")
path_layout.addWidget(self.cookie_path_input)
self.browse_button = QPushButton("Browse")
self.browse_button.clicked.connect(self.browse_cookie_file)
path_layout.addWidget(self.browse_button)
layout.addLayout(path_layout)
# Dialog buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
def browse_cookie_file(self) -> None:
# Open file dialog to select cookie file
selected_files, _ = QFileDialog.getOpenFileName(self, "Select Cookie File", "", "Cookies files (*.txt *.lwp)")
if selected_files:
self.cookie_path_input.setText(selected_files[0])
def get_cookie_file_path(self) -> str:
# Return the selected cookie file path
return self.cookie_path_input.text()
self.output_received.emit("=" * 50)
self.error_occurred.emit(f"❌ Error executing command: {str(e)}")
class CustomOptionsDialog(QDialog):
@@ -294,7 +112,7 @@ class CustomOptionsDialog(QDialog):
super().__init__(parent)
self._parent: YTSageApp = cast("YTSageApp", self.parent()) # cast will help with auto complete and type hint checking.
self.setWindowTitle("Custom Options")
self.setMinimumSize(600, 500)
self.setMinimumSize(550, 400) # Made even shorter
layout = QVBoxLayout(self)
# Create tab widget to organize content
@@ -307,25 +125,92 @@ class CustomOptionsDialog(QDialog):
# Help text
help_text = QLabel(
"Select the Netscape-format cookies file for logging in.\n"
"Choose how to provide cookies for logging in.\n"
"This allows downloading of private videos and premium quality audio."
)
help_text.setWordWrap(True)
help_text.setStyleSheet("color: #999999; padding: 10px;")
cookies_layout.addWidget(help_text)
# Cookie source selection
cookie_source_group = QGroupBox("Cookie Source")
cookie_source_layout = QVBoxLayout(cookie_source_group)
# Radio buttons for cookie source
self.cookie_file_radio = QRadioButton("Use cookie file (Netscape format)")
self.cookie_file_radio.setChecked(True)
self.cookie_file_radio.toggled.connect(self.on_cookie_source_changed)
cookie_source_layout.addWidget(self.cookie_file_radio)
self.cookie_browser_radio = QRadioButton("Extract cookies from browser")
self.cookie_browser_radio.toggled.connect(self.on_cookie_source_changed)
cookie_source_layout.addWidget(self.cookie_browser_radio)
cookies_layout.addWidget(cookie_source_group)
# Cookie file section
self.cookie_file_group = QGroupBox("Cookie File")
file_layout = QVBoxLayout(self.cookie_file_group)
# File path input and browse button
path_layout = QHBoxLayout()
self.cookie_path_input = QLineEdit()
self.cookie_path_input.setPlaceholderText("Path to cookies file (Netscape format)")
if hasattr(self._parent, "cookie_file_path") and self._parent.cookie_file_path:
self.cookie_path_input.setText(self._parent.cookie_file_path.as_posix())
# Convert Path to string properly and validate
cookie_path_str = str(self._parent.cookie_file_path)
# Only set if it looks like a valid path (more than just a drive letter)
if len(cookie_path_str) > 3 and not cookie_path_str.endswith(':'):
self.cookie_path_input.setText(cookie_path_str)
path_layout.addWidget(self.cookie_path_input)
self.browse_button = QPushButton("Browse")
self.browse_button.clicked.connect(self.browse_cookie_file)
path_layout.addWidget(self.browse_button)
cookies_layout.addLayout(path_layout) # Add the horizontal layout to cookies layout
file_layout.addLayout(path_layout)
cookies_layout.addWidget(self.cookie_file_group)
# Browser selection section
self.cookie_browser_group = QGroupBox("Browser Selection")
browser_layout = QVBoxLayout(self.cookie_browser_group)
browser_help = QLabel(
"Select the browser to extract cookies from. Make sure the browser is closed before extraction."
)
browser_help.setWordWrap(True)
browser_help.setStyleSheet("color: #999999; font-size: 11px;")
browser_layout.addWidget(browser_help)
browser_select_layout = QHBoxLayout()
browser_select_layout.addWidget(QLabel("Browser:"))
self.browser_combo = QComboBox()
self.browser_combo.addItems([
"chrome",
"firefox",
"safari",
"edge",
"opera",
"brave",
"chromium",
"vivaldi"
])
browser_select_layout.addWidget(self.browser_combo)
browser_layout.addLayout(browser_select_layout)
# Optional profile field
profile_layout = QHBoxLayout()
profile_layout.addWidget(QLabel("Profile (optional):"))
self.profile_input = QLineEdit()
self.profile_input.setPlaceholderText("Profile name or path (leave empty for default)")
profile_layout.addWidget(self.profile_input)
browser_layout.addLayout(profile_layout)
cookies_layout.addWidget(self.cookie_browser_group)
# Initially hide browser group
self.cookie_browser_group.setVisible(False)
# Status indicator for cookies
self.cookie_status = QLabel("")
@@ -338,80 +223,126 @@ class CustomOptionsDialog(QDialog):
command_tab = QWidget()
command_layout = QVBoxLayout(command_tab)
# Help text
# Improved help text
cmd_help_text = QLabel(
"Enter custom yt-dlp commands below. The URL will be automatically appended.\n"
"Example: --extract-audio --audio-format mp3 --audio-quality 0\n"
"Note: Download path and output template will be preserved."
"Enter your custom yt-dlp command below. The current URL will be automatically appended.<br><br>"
"For complete list of options and usage examples, "
f'<a href="{YTDLP_DOCS_URL}">click here to view the official yt-dlp documentation</a>.<br><br>'
"Note: Download path and output filename template will be automatically handled."
)
cmd_help_text.setWordWrap(True)
cmd_help_text.setStyleSheet("color: #999999; padding: 10px;")
command_layout.addWidget(cmd_help_text)
# Add SponsorBlock checkbox
self.sponsorblock_checkbox = QCheckBox("Remove Sponsor Segments")
self.sponsorblock_checkbox.setStyleSheet(
cmd_help_text.setOpenExternalLinks(True) # Enable clicking links
cmd_help_text.setTextFormat(Qt.TextFormat.RichText) # Enable HTML rendering
cmd_help_text.setStyleSheet(
"""
QCheckBox {
color: #ffffff;
padding: 5px;
margin-left: 0px;
QLabel {
color: #cccccc;
font-size: 12px;
padding: 10px;
background-color: #1a1d20;
border-radius: 6px;
line-height: 1.4;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 9px;
QLabel a {
color: #4da6ff;
text-decoration: underline;
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
background: #1d1e22;
border-radius: 9px;
}
QCheckBox::indicator:checked {
border: 2px solid #c90000;
background: #c90000;
border-radius: 9px;
QLabel a:hover {
color: #66b3ff;
}
"""
)
command_layout.addWidget(self.sponsorblock_checkbox)
command_layout.addWidget(cmd_help_text)
# Command input label
input_label = QLabel("yt-dlp Arguments:")
input_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 10px;")
command_layout.addWidget(input_label)
# Command input
self.command_input = QPlainTextEdit()
self.command_input.setPlaceholderText("Enter yt-dlp arguments...")
self.command_input.setPlaceholderText(
"Enter yt-dlp arguments here...\n\n"
"e.g. --extract-audio --audio-format mp3"
)
self.command_input.setMinimumHeight(80) # Reduced further from 100
self.command_input.setStyleSheet(
"""
QPlainTextEdit {
background-color: #1d1e22;
color: #ffffff;
border: 2px solid #1d1e22;
border-radius: 4px;
padding: 8px;
font-family: Consolas, monospace;
border: 2px solid #2a2d36;
border-radius: 6px;
padding: 12px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
}
QPlainTextEdit:focus {
border-color: #c90000;
}
"""
)
command_layout.addWidget(self.command_input)
# Button layout
button_layout = QHBoxLayout()
button_layout.setSpacing(10)
clear_btn = QPushButton("Clear")
clear_btn.clicked.connect(lambda: self.command_input.clear())
clear_btn.setStyleSheet(
"""
QPushButton {
padding: 8px 15px;
background-color: #444444;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
min-width: 80px;
}
QPushButton:hover {
background-color: #555555;
}
"""
)
button_layout.addWidget(clear_btn)
button_layout.addStretch() # Push run button to the right
# Run command button
self.run_btn = QPushButton("Run Command")
self.run_btn.clicked.connect(self.run_custom_command)
command_layout.addWidget(self.run_btn)
self.run_btn.setDefault(True)
button_layout.addWidget(self.run_btn)
command_layout.addLayout(button_layout)
# Output label
output_label = QLabel("Command Output:")
output_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #ffffff; margin-top: 15px;")
command_layout.addWidget(output_label)
# Log output
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setPlaceholderText("Command output will appear here...")
self.log_output.setMinimumHeight(100) # Reduced further from 120
self.log_output.setStyleSheet(
"""
QTextEdit {
background-color: #1d1e22;
color: #ffffff;
border: 2px solid #1d1e22;
border-radius: 4px;
padding: 8px;
font-family: Consolas, monospace;
border: 2px solid #2a2d36;
border-radius: 6px;
padding: 12px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
}
QTextEdit:focus {
border-color: #c90000;
}
"""
)
command_layout.addWidget(self.log_output)
@@ -454,6 +385,61 @@ class CustomOptionsDialog(QDialog):
QLabel {
color: #ffffff;
}
QGroupBox {
border: 1px solid #3d3d3d;
border-radius: 4px;
margin-top: 1.5ex;
color: #ffffff;
padding: 10px;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left;
padding: 0 5px;
}
QRadioButton {
color: #ffffff;
padding: 5px;
}
QRadioButton::indicator {
width: 18px;
height: 18px;
border-radius: 9px;
}
QRadioButton::indicator:unchecked {
border: 2px solid #666666;
background: #1d1e22;
border-radius: 9px;
}
QRadioButton::indicator:checked {
border: 2px solid #c90000;
background: #c90000;
border-radius: 9px;
}
QComboBox {
padding: 8px;
border: 2px solid #1b2021;
border-radius: 4px;
background-color: #1b2021;
color: #ffffff;
min-width: 150px;
}
QComboBox::drop-down {
border: none;
width: 20px;
}
QComboBox::down-arrow {
border: none;
width: 12px;
height: 12px;
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTMgNEw2IDdMOSA0IiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+Cjwvc3ZnPgo=);
}
QComboBox QAbstractItemView {
background-color: #1d1e22;
color: #ffffff;
border: 1px solid #3d3d3d;
selection-background-color: #c90000;
}
QLineEdit {
padding: 8px;
border: 2px solid #1b2021;
@@ -475,138 +461,139 @@ class CustomOptionsDialog(QDialog):
"""
)
# Initialize dialog with current settings (after all widgets and styles are set)
self._initialize_cookie_settings()
def _initialize_cookie_settings(self) -> None:
"""Initialize the dialog with current cookie settings from parent"""
if hasattr(self._parent, "browser_cookies_option") and self._parent.browser_cookies_option:
# Browser cookies are active
self.cookie_browser_radio.setChecked(True)
browser_parts = self._parent.browser_cookies_option.split(':')
browser = browser_parts[0]
profile = browser_parts[1] if len(browser_parts) > 1 else ""
# Set browser selection
index = self.browser_combo.findText(browser)
if index >= 0:
self.browser_combo.setCurrentIndex(index)
# Set profile if any
self.profile_input.setText(profile)
self.cookie_status.setText(f"Browser cookies active: {self._parent.browser_cookies_option}")
self.cookie_status.setStyleSheet("color: #00cc00; font-style: italic;")
elif hasattr(self._parent, "cookie_file_path") and self._parent.cookie_file_path:
# File cookies are active - ensure file radio is selected and update status
self.cookie_file_radio.setChecked(True)
self.cookie_status.setText(f"Cookie file active: {self._parent.cookie_file_path.name}")
self.cookie_status.setStyleSheet("color: #00cc00; font-style: italic;")
else:
# No cookies configured - ensure file radio is selected by default
self.cookie_file_radio.setChecked(True)
def on_cookie_source_changed(self) -> None:
"""Handle cookie source radio button changes"""
if self.cookie_file_radio.isChecked():
self.cookie_file_group.setVisible(True)
self.cookie_browser_group.setVisible(False)
self.cookie_status.setText("")
else:
self.cookie_file_group.setVisible(False)
self.cookie_browser_group.setVisible(True)
self.cookie_status.setText("Browser cookies will be extracted when applied")
self.cookie_status.setStyleSheet("color: #ffaa00; font-style: italic;")
def browse_cookie_file(self) -> None:
# Open file dialog to select cookie file
selected_files, _ = QFileDialog.getOpenFileName(self, "Select Cookie File", "", "Cookies files (*.txt *.lwp)")
if selected_files:
self.cookie_path_input.setText(selected_files[0])
# Ensure we have a valid full path
cookie_path = Path(selected_files).resolve()
self.cookie_path_input.setText(str(cookie_path))
self.cookie_status.setText("Cookie file selected - Click OK to apply")
self.cookie_status.setStyleSheet("color: #00cc00; font-style: italic;")
def get_cookie_file_path(self) -> Path | None:
# Return the selected cookie file path if it's not empty
path = Path(self.cookie_path_input.text().strip())
if path and path.exists():
# Return the selected cookie file path if it's not empty and using file mode
if self.cookie_file_radio.isChecked():
path_text = self.cookie_path_input.text().strip()
if path_text:
path = Path(path_text)
if path.exists() and path.is_file():
return path
else:
# File doesn't exist or is not a file - still return path for user feedback
return path if len(path_text) > 3 else None # Avoid single letters like 'C'
return None
def get_browser_cookies_option(self) -> str | None:
"""Returns the --cookies-from-browser option string if browser mode is selected"""
if self.cookie_browser_radio.isChecked():
browser = self.browser_combo.currentText()
profile = self.profile_input.text().strip()
if profile:
return f"{browser}:{profile}"
else:
return browser
return None
def is_using_browser_cookies(self) -> bool:
"""Returns True if browser cookies mode is selected"""
return self.cookie_browser_radio.isChecked()
def run_custom_command(self) -> None:
url = self._parent.url_input.text().strip()
if not url:
self.log_output.append("Error: No URL provided")
self.log_output.append("Error: No URL provided. Please enter a URL in the main window.")
return
command = self.command_input.toPlainText().strip()
if not command:
self.log_output.append("❌ Error: No command provided. Please enter yt-dlp arguments.")
return
# Get download path from parent
path = self._parent.last_path
self.log_output.clear()
self.log_output.append(f"Running command with URL: {url}")
self.log_output.append("🚀 Executing custom yt-dlp command")
self.log_output.append(f"📍 URL: {url}")
self.log_output.append(f"⚙️ Arguments: {command}")
if path:
self.log_output.append(f"📁 Download path: {path}")
self.log_output.append("=" * 50)
self.run_btn.setEnabled(False)
self.run_btn.setText("Running...")
# Start command in thread
threading.Thread(target=self._run_command_thread, args=(command, url, path), daemon=True).start()
# Create worker and thread
self.worker = CommandWorker(command, url, path)
self.worker_thread = threading.Thread(target=self.worker.run_command, daemon=True)
def _run_command_thread(self, command, url, path) -> None:
try:
# Connect worker signals to our slots
self.worker.output_received.connect(self.on_output_received)
self.worker.command_finished.connect(self.on_command_finished)
self.worker.error_occurred.connect(self.on_error_occurred)
class CommandLogger:
def debug(self, msg):
QMetaObject.invokeMethod(
self.dialog.log_output,
b"append",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, msg),
)
# Start the thread
self.worker_thread.start()
def warning(self, msg):
QMetaObject.invokeMethod(
self.dialog.log_output,
b"append",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, f"Warning: {msg}"),
)
def on_output_received(self, text: str):
"""Slot for receiving output from the worker"""
self.log_output.append(text)
def error(self, msg):
QMetaObject.invokeMethod(
self.dialog.log_output,
b"append",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, f"Error: {msg}"),
)
def on_command_finished(self, success: bool, exit_code: int):
"""Slot for when command finishes"""
self.run_btn.setEnabled(True)
self.run_btn.setText("Run Command")
def __init__(self, dialog):
self.dialog = dialog
# Split command into arguments
args = command.split()
# Add SponsorBlock if selected
yt_dlp_path = get_yt_dlp_path()
base_cmd = [yt_dlp_path] + args + [url]
if self.sponsorblock_checkbox.isChecked():
base_cmd.extend(["--sponsorblock-remove", "sponsor,selfpromo,interaction"])
# Show the full command
QMetaObject.invokeMethod(
self.log_output,
b"append",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, f"Full command: {' '.join(base_cmd)}"),
)
# Run the command
proc = subprocess.Popen(
base_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
errors="replace",
)
# Stream output
for line in proc.stdout: # type: ignore[reportOptionalIterable]
QMetaObject.invokeMethod(
self.log_output,
b"append",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, line.rstrip()),
)
ret = proc.wait()
if ret != 0:
QMetaObject.invokeMethod(
self.log_output,
b"append",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, f"Command exited with code {ret}"),
)
else:
QMetaObject.invokeMethod(
self.log_output,
b"append",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, "Command completed successfully"),
)
except Exception as e:
QMetaObject.invokeMethod(
self.log_output,
b"append",
Qt.ConnectionType.QueuedConnection,
Q_ARG(str, f"Error: {str(e)}"),
)
finally:
# Re-enable the run button
QMetaObject.invokeMethod(
self.run_btn,
b"setEnabled",
Qt.ConnectionType.QueuedConnection,
Q_ARG(bool, True),
)
def on_error_occurred(self, error_msg: str):
"""Slot for handling errors"""
self.log_output.append(error_msg)
self.run_btn.setEnabled(True)
self.run_btn.setText("Run Command")
class TimeRangeDialog(QDialog):
@@ -677,31 +664,6 @@ class TimeRangeDialog(QDialog):
)
layout.addWidget(self.force_keyframes)
# Format preview
preview_group = QGroupBox("Command Preview")
preview_layout = QVBoxLayout()
self.preview_label = QLabel('--download-sections "*-"')
self.preview_label.setStyleSheet(
"""
QLabel {
background-color: #1d1e22;
color: #ffffff;
border: 1px solid #3d3d3d;
border-radius: 4px;
padding: 8px;
font-family: Consolas, monospace;
}
"""
)
preview_layout.addWidget(self.preview_label)
preview_group.setLayout(preview_layout)
layout.addWidget(preview_group)
# Connect signals for live preview updates
self.start_time_input.textChanged.connect(self.update_preview)
self.end_time_input.textChanged.connect(self.update_preview)
self.force_keyframes.stateChanged.connect(self.update_preview)
# Buttons
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
button_box.accepted.connect(self.accept)
@@ -751,26 +713,7 @@ class TimeRangeDialog(QDialog):
)
# Initialize preview
self.update_preview()
def update_preview(self) -> None:
start = self.start_time_input.text().strip()
end = self.end_time_input.text().strip()
if start and end:
time_range = f"*{start}-{end}"
elif start:
time_range = f"*{start}-"
elif end:
time_range = f"*-{end}"
else:
time_range = "*-" # Full video
preview = f'--download-sections "{time_range}"'
if self.force_keyframes.isChecked():
preview += " --force-keyframes-at-cuts"
self.preview_label.setText(preview)
# self.update_preview() # Removed preview functionality
def get_download_sections(self) -> str | None:
"""Returns the download sections command arguments or None if no selection made"""