diff --git a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py
index 23103f2..778fbc9 100644
--- a/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py
+++ b/src/gui/ytsage_gui_dialogs/ytsage_dialogs_custom.py
@@ -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)
-
- layout = QVBoxLayout(self)
-
- # 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)
-
- # 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:
+class CommandWorker(QObject):
+ """Worker class for running yt-dlp commands in a separate thread"""
+
+ # 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
+
+ def __init__(self, command, url, path):
+ super().__init__()
+ self.command = command
+ self.url = url
+ self.path = path
+
+ 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 download path if specified
+ if self.path:
+ base_cmd.extend(["-P", self.path])
+
+ # Add URL at the end
+ base_cmd.append(self.url)
- # 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,
- },
- ]
- )
+ # Emit the full command
+ self.output_received.emit(f"🔧 Full command: {' '.join(str(cmd) for cmd in base_cmd)}")
+ self.output_received.emit("=" * 50)
- # 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
+ # Run the command
+ proc = subprocess.Popen(
+ base_cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ encoding="utf-8",
+ errors="replace",
+ )
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
- ydl.download([url])
-
- self.log_output.append("Command completed successfully")
+ # Stream output
+ for line in proc.stdout: # type: ignore[reportOptionalIterable]
+ if line.strip(): # Only show non-empty lines
+ self.output_received.emit(line.rstrip())
+ ret = proc.wait()
+ self.output_received.emit("=" * 50)
+
+ 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.
"
+ "For complete list of options and usage examples, "
+ f'click here to view the official yt-dlp documentation.
'
+ "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 path
+ # 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)
+
+ # 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)
+
+ # Start the thread
+ self.worker_thread.start()
- def _run_command_thread(self, command, url, path) -> None:
- try:
+ def on_output_received(self, text: str):
+ """Slot for receiving output from the worker"""
+ self.log_output.append(text)
- class CommandLogger:
- def debug(self, msg):
- QMetaObject.invokeMethod(
- self.dialog.log_output,
- b"append",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, 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 warning(self, msg):
- QMetaObject.invokeMethod(
- self.dialog.log_output,
- b"append",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, f"Warning: {msg}"),
- )
-
- def error(self, msg):
- QMetaObject.invokeMethod(
- self.dialog.log_output,
- b"append",
- Qt.ConnectionType.QueuedConnection,
- Q_ARG(str, f"Error: {msg}"),
- )
-
- 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"""