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:
@@ -8,9 +8,10 @@ import threading
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, cast
|
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 (
|
from PySide6.QtWidgets import (
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
|
QComboBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
QDialogButtonBox,
|
QDialogButtonBox,
|
||||||
QFileDialog,
|
QFileDialog,
|
||||||
@@ -20,6 +21,7 @@ from PySide6.QtWidgets import (
|
|||||||
QLineEdit,
|
QLineEdit,
|
||||||
QPlainTextEdit,
|
QPlainTextEdit,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
|
QRadioButton,
|
||||||
QTabWidget,
|
QTabWidget,
|
||||||
QTextEdit,
|
QTextEdit,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
@@ -27,6 +29,7 @@ from PySide6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
||||||
|
from src.utils.ytsage_constants import YTDLP_DOCS_URL
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import yt_dlp
|
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)
|
from src.gui.ytsage_gui_main import YTSageApp # only for type hints (no runtime import)
|
||||||
|
|
||||||
|
|
||||||
class CustomCommandDialog(QDialog):
|
class CommandWorker(QObject):
|
||||||
def __init__(self, parent=None) -> None:
|
"""Worker class for running yt-dlp commands in a separate thread"""
|
||||||
super().__init__(parent)
|
|
||||||
self._parent = self.parent()
|
|
||||||
self.setWindowTitle("Custom yt-dlp Command")
|
|
||||||
self.setMinimumSize(600, 400)
|
|
||||||
|
|
||||||
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
|
def __init__(self, command, url, path):
|
||||||
help_text = QLabel(
|
super().__init__()
|
||||||
"Enter custom yt-dlp commands below. The URL will be automatically appended.\n"
|
self.command = command
|
||||||
"Example: --extract-audio --audio-format mp3 --audio-quality 0\n"
|
self.url = url
|
||||||
"Note: Download path and output template will be preserved."
|
self.path = path
|
||||||
)
|
|
||||||
help_text.setWordWrap(True)
|
|
||||||
help_text.setStyleSheet("color: #999999; padding: 10px;")
|
|
||||||
layout.addWidget(help_text)
|
|
||||||
|
|
||||||
# Command input
|
def run_command(self):
|
||||||
self.command_input = QPlainTextEdit()
|
"""Run the yt-dlp command and emit signals for output"""
|
||||||
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:
|
|
||||||
try:
|
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
|
# Split command into arguments
|
||||||
args = command.split()
|
args = self.command.split()
|
||||||
|
|
||||||
# Base options
|
# Build the full command
|
||||||
ydl_opts = {
|
yt_dlp_path = get_yt_dlp_path()
|
||||||
"logger": CommandLogger(self),
|
base_cmd = [yt_dlp_path] + args
|
||||||
"paths": {"home": path},
|
|
||||||
"debug_printout": True,
|
|
||||||
"postprocessors": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add SponsorBlock options if enabled
|
# Add download path if specified
|
||||||
if self.sponsorblock_checkbox.isChecked():
|
if self.path:
|
||||||
ydl_opts["postprocessors"].extend(
|
base_cmd.extend(["-P", self.path])
|
||||||
[
|
|
||||||
{
|
# Add URL at the end
|
||||||
"key": "SponsorBlock",
|
base_cmd.append(self.url)
|
||||||
"categories": ["sponsor", "selfpromo", "interaction"],
|
|
||||||
"api": "https://sponsor.ajay.app",
|
# Emit the full command
|
||||||
},
|
self.output_received.emit(f"🔧 Full command: {' '.join(str(cmd) for cmd in base_cmd)}")
|
||||||
{
|
self.output_received.emit("=" * 50)
|
||||||
"key": "ModifyChapters",
|
|
||||||
"remove_sponsor_segments": [
|
# Run the command
|
||||||
"sponsor",
|
proc = subprocess.Popen(
|
||||||
"selfpromo",
|
base_cmd,
|
||||||
"interaction",
|
stdout=subprocess.PIPE,
|
||||||
],
|
stderr=subprocess.STDOUT,
|
||||||
"sponsorblock_chapter_title": "[SponsorBlock]: %(category_names)l",
|
text=True,
|
||||||
"force_keyframes": True,
|
encoding="utf-8",
|
||||||
},
|
errors="replace",
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add custom arguments
|
# Stream output
|
||||||
for i in range(0, len(args), 2):
|
for line in proc.stdout: # type: ignore[reportOptionalIterable]
|
||||||
if i + 1 < len(args):
|
if line.strip(): # Only show non-empty lines
|
||||||
key = args[i].lstrip("-").replace("-", "_")
|
self.output_received.emit(line.rstrip())
|
||||||
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
|
|
||||||
|
|
||||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
ret = proc.wait()
|
||||||
ydl.download([url])
|
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:
|
except Exception as e:
|
||||||
self.log_output.append(f"Error: {str(e)}")
|
self.output_received.emit("=" * 50)
|
||||||
finally:
|
self.error_occurred.emit(f"❌ Error executing command: {str(e)}")
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
class CustomOptionsDialog(QDialog):
|
class CustomOptionsDialog(QDialog):
|
||||||
@@ -294,7 +112,7 @@ class CustomOptionsDialog(QDialog):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._parent: YTSageApp = cast("YTSageApp", self.parent()) # cast will help with auto complete and type hint checking.
|
self._parent: YTSageApp = cast("YTSageApp", self.parent()) # cast will help with auto complete and type hint checking.
|
||||||
self.setWindowTitle("Custom Options")
|
self.setWindowTitle("Custom Options")
|
||||||
self.setMinimumSize(600, 500)
|
self.setMinimumSize(550, 400) # Made even shorter
|
||||||
layout = QVBoxLayout(self)
|
layout = QVBoxLayout(self)
|
||||||
|
|
||||||
# Create tab widget to organize content
|
# Create tab widget to organize content
|
||||||
@@ -307,25 +125,92 @@ class CustomOptionsDialog(QDialog):
|
|||||||
|
|
||||||
# Help text
|
# Help text
|
||||||
help_text = QLabel(
|
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."
|
"This allows downloading of private videos and premium quality audio."
|
||||||
)
|
)
|
||||||
help_text.setWordWrap(True)
|
help_text.setWordWrap(True)
|
||||||
help_text.setStyleSheet("color: #999999; padding: 10px;")
|
help_text.setStyleSheet("color: #999999; padding: 10px;")
|
||||||
cookies_layout.addWidget(help_text)
|
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
|
# File path input and browse button
|
||||||
path_layout = QHBoxLayout()
|
path_layout = QHBoxLayout()
|
||||||
self.cookie_path_input = QLineEdit()
|
self.cookie_path_input = QLineEdit()
|
||||||
self.cookie_path_input.setPlaceholderText("Path to cookies file (Netscape format)")
|
self.cookie_path_input.setPlaceholderText("Path to cookies file (Netscape format)")
|
||||||
if hasattr(self._parent, "cookie_file_path") and self._parent.cookie_file_path:
|
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)
|
path_layout.addWidget(self.cookie_path_input)
|
||||||
|
|
||||||
self.browse_button = QPushButton("Browse")
|
self.browse_button = QPushButton("Browse")
|
||||||
self.browse_button.clicked.connect(self.browse_cookie_file)
|
self.browse_button.clicked.connect(self.browse_cookie_file)
|
||||||
path_layout.addWidget(self.browse_button)
|
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
|
# Status indicator for cookies
|
||||||
self.cookie_status = QLabel("")
|
self.cookie_status = QLabel("")
|
||||||
@@ -338,80 +223,126 @@ class CustomOptionsDialog(QDialog):
|
|||||||
command_tab = QWidget()
|
command_tab = QWidget()
|
||||||
command_layout = QVBoxLayout(command_tab)
|
command_layout = QVBoxLayout(command_tab)
|
||||||
|
|
||||||
# Help text
|
# Improved help text
|
||||||
cmd_help_text = QLabel(
|
cmd_help_text = QLabel(
|
||||||
"Enter custom yt-dlp commands below. The URL will be automatically appended.\n"
|
"Enter your custom yt-dlp command below. The current URL will be automatically appended.<br><br>"
|
||||||
"Example: --extract-audio --audio-format mp3 --audio-quality 0\n"
|
"For complete list of options and usage examples, "
|
||||||
"Note: Download path and output template will be preserved."
|
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.setWordWrap(True)
|
||||||
cmd_help_text.setStyleSheet("color: #999999; padding: 10px;")
|
cmd_help_text.setOpenExternalLinks(True) # Enable clicking links
|
||||||
command_layout.addWidget(cmd_help_text)
|
cmd_help_text.setTextFormat(Qt.TextFormat.RichText) # Enable HTML rendering
|
||||||
|
cmd_help_text.setStyleSheet(
|
||||||
# Add SponsorBlock checkbox
|
|
||||||
self.sponsorblock_checkbox = QCheckBox("Remove Sponsor Segments")
|
|
||||||
self.sponsorblock_checkbox.setStyleSheet(
|
|
||||||
"""
|
"""
|
||||||
QCheckBox {
|
QLabel {
|
||||||
color: #ffffff;
|
color: #cccccc;
|
||||||
padding: 5px;
|
font-size: 12px;
|
||||||
margin-left: 0px;
|
padding: 10px;
|
||||||
|
background-color: #1a1d20;
|
||||||
|
border-radius: 6px;
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
QCheckBox::indicator {
|
QLabel a {
|
||||||
width: 18px;
|
color: #4da6ff;
|
||||||
height: 18px;
|
text-decoration: underline;
|
||||||
border-radius: 9px;
|
|
||||||
}
|
}
|
||||||
QCheckBox::indicator:unchecked {
|
QLabel a:hover {
|
||||||
border: 2px solid #666666;
|
color: #66b3ff;
|
||||||
background: #1d1e22;
|
|
||||||
border-radius: 9px;
|
|
||||||
}
|
|
||||||
QCheckBox::indicator:checked {
|
|
||||||
border: 2px solid #c90000;
|
|
||||||
background: #c90000;
|
|
||||||
border-radius: 9px;
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
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
|
# Command input
|
||||||
self.command_input = QPlainTextEdit()
|
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(
|
self.command_input.setStyleSheet(
|
||||||
"""
|
"""
|
||||||
QPlainTextEdit {
|
QPlainTextEdit {
|
||||||
background-color: #1d1e22;
|
background-color: #1d1e22;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border: 2px solid #1d1e22;
|
border: 2px solid #2a2d36;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
padding: 8px;
|
padding: 12px;
|
||||||
font-family: Consolas, monospace;
|
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)
|
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
|
# Run command button
|
||||||
self.run_btn = QPushButton("Run Command")
|
self.run_btn = QPushButton("Run Command")
|
||||||
self.run_btn.clicked.connect(self.run_custom_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
|
# Log output
|
||||||
self.log_output = QTextEdit()
|
self.log_output = QTextEdit()
|
||||||
self.log_output.setReadOnly(True)
|
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(
|
self.log_output.setStyleSheet(
|
||||||
"""
|
"""
|
||||||
QTextEdit {
|
QTextEdit {
|
||||||
background-color: #1d1e22;
|
background-color: #1d1e22;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border: 2px solid #1d1e22;
|
border: 2px solid #2a2d36;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
padding: 8px;
|
padding: 12px;
|
||||||
font-family: Consolas, monospace;
|
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
QTextEdit:focus {
|
||||||
|
border-color: #c90000;
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
command_layout.addWidget(self.log_output)
|
command_layout.addWidget(self.log_output)
|
||||||
@@ -454,6 +385,61 @@ class CustomOptionsDialog(QDialog):
|
|||||||
QLabel {
|
QLabel {
|
||||||
color: #ffffff;
|
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 {
|
QLineEdit {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 2px solid #1b2021;
|
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:
|
def browse_cookie_file(self) -> None:
|
||||||
# Open file dialog to select cookie file
|
# Open file dialog to select cookie file
|
||||||
selected_files, _ = QFileDialog.getOpenFileName(self, "Select Cookie File", "", "Cookies files (*.txt *.lwp)")
|
selected_files, _ = QFileDialog.getOpenFileName(self, "Select Cookie File", "", "Cookies files (*.txt *.lwp)")
|
||||||
|
|
||||||
if selected_files:
|
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.setText("Cookie file selected - Click OK to apply")
|
||||||
self.cookie_status.setStyleSheet("color: #00cc00; font-style: italic;")
|
self.cookie_status.setStyleSheet("color: #00cc00; font-style: italic;")
|
||||||
|
|
||||||
def get_cookie_file_path(self) -> Path | None:
|
def get_cookie_file_path(self) -> Path | None:
|
||||||
# Return the selected cookie file path if it's not empty
|
# Return the selected cookie file path if it's not empty and using file mode
|
||||||
path = Path(self.cookie_path_input.text().strip())
|
if self.cookie_file_radio.isChecked():
|
||||||
if path and path.exists():
|
path_text = self.cookie_path_input.text().strip()
|
||||||
|
if path_text:
|
||||||
|
path = Path(path_text)
|
||||||
|
if path.exists() and path.is_file():
|
||||||
return path
|
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
|
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:
|
def run_custom_command(self) -> None:
|
||||||
url = self._parent.url_input.text().strip()
|
url = self._parent.url_input.text().strip()
|
||||||
if not url:
|
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
|
return
|
||||||
|
|
||||||
command = self.command_input.toPlainText().strip()
|
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
|
# Get download path from parent
|
||||||
path = self._parent.last_path
|
path = self._parent.last_path
|
||||||
|
|
||||||
self.log_output.clear()
|
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.setEnabled(False)
|
||||||
|
self.run_btn.setText("Running...")
|
||||||
|
|
||||||
# Start command in thread
|
# Create worker and thread
|
||||||
threading.Thread(target=self._run_command_thread, args=(command, url, path), daemon=True).start()
|
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:
|
# Connect worker signals to our slots
|
||||||
try:
|
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:
|
# Start the thread
|
||||||
def debug(self, msg):
|
self.worker_thread.start()
|
||||||
QMetaObject.invokeMethod(
|
|
||||||
self.dialog.log_output,
|
|
||||||
b"append",
|
|
||||||
Qt.ConnectionType.QueuedConnection,
|
|
||||||
Q_ARG(str, msg),
|
|
||||||
)
|
|
||||||
|
|
||||||
def warning(self, msg):
|
def on_output_received(self, text: str):
|
||||||
QMetaObject.invokeMethod(
|
"""Slot for receiving output from the worker"""
|
||||||
self.dialog.log_output,
|
self.log_output.append(text)
|
||||||
b"append",
|
|
||||||
Qt.ConnectionType.QueuedConnection,
|
|
||||||
Q_ARG(str, f"Warning: {msg}"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def error(self, msg):
|
def on_command_finished(self, success: bool, exit_code: int):
|
||||||
QMetaObject.invokeMethod(
|
"""Slot for when command finishes"""
|
||||||
self.dialog.log_output,
|
self.run_btn.setEnabled(True)
|
||||||
b"append",
|
self.run_btn.setText("Run Command")
|
||||||
Qt.ConnectionType.QueuedConnection,
|
|
||||||
Q_ARG(str, f"Error: {msg}"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, dialog):
|
def on_error_occurred(self, error_msg: str):
|
||||||
self.dialog = dialog
|
"""Slot for handling errors"""
|
||||||
|
self.log_output.append(error_msg)
|
||||||
# Split command into arguments
|
self.run_btn.setEnabled(True)
|
||||||
args = command.split()
|
self.run_btn.setText("Run Command")
|
||||||
|
|
||||||
# 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),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TimeRangeDialog(QDialog):
|
class TimeRangeDialog(QDialog):
|
||||||
@@ -677,31 +664,6 @@ class TimeRangeDialog(QDialog):
|
|||||||
)
|
)
|
||||||
layout.addWidget(self.force_keyframes)
|
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
|
# Buttons
|
||||||
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||||
button_box.accepted.connect(self.accept)
|
button_box.accepted.connect(self.accept)
|
||||||
@@ -751,26 +713,7 @@ class TimeRangeDialog(QDialog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Initialize preview
|
# Initialize preview
|
||||||
self.update_preview()
|
# self.update_preview() # Removed preview functionality
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def get_download_sections(self) -> str | None:
|
def get_download_sections(self) -> str | None:
|
||||||
"""Returns the download sections command arguments or None if no selection made"""
|
"""Returns the download sections command arguments or None if no selection made"""
|
||||||
|
|||||||
Reference in New Issue
Block a user