16b50934ab
Proxy settings (main and geo-verification) are now loaded from and saved to the ConfigManager. The dialogs and main app have been updated to initialize and persist proxy values, and user feedback is improved for proxy changes and clearing.
934 lines
34 KiB
Python
934 lines
34 KiB
Python
"""
|
|
Custom functionality dialogs for YTSage application.
|
|
Contains dialogs for custom commands, cookies, time ranges, and other special features.
|
|
"""
|
|
|
|
import subprocess
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, cast
|
|
|
|
from PySide6.QtCore import Q_ARG, QMetaObject, QObject, Qt, Signal
|
|
from PySide6.QtWidgets import (
|
|
QCheckBox,
|
|
QComboBox,
|
|
QDialog,
|
|
QDialogButtonBox,
|
|
QFileDialog,
|
|
QGroupBox,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QLineEdit,
|
|
QPlainTextEdit,
|
|
QPushButton,
|
|
QRadioButton,
|
|
QTabWidget,
|
|
QTextEdit,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
|
from src.utils.ytsage_constants import YTDLP_DOCS_URL
|
|
from src.utils.ytsage_config_manager import ConfigManager
|
|
|
|
if TYPE_CHECKING:
|
|
from src.gui.ytsage_gui_main import YTSageApp # only for type hints (no runtime import)
|
|
|
|
|
|
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:
|
|
# Split command into arguments
|
|
args = self.command.split()
|
|
|
|
# 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)
|
|
|
|
# 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",
|
|
)
|
|
|
|
# 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.output_received.emit("=" * 50)
|
|
self.error_occurred.emit(f"❌ Error executing command: {str(e)}")
|
|
|
|
|
|
class CustomOptionsDialog(QDialog):
|
|
def __init__(self, parent=None) -> None:
|
|
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(550, 400) # Made even shorter
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Create tab widget to organize content
|
|
self.tab_widget = QTabWidget()
|
|
layout.addWidget(self.tab_widget)
|
|
|
|
# === Cookies Tab ===
|
|
cookies_tab = QWidget()
|
|
cookies_layout = QVBoxLayout(cookies_tab)
|
|
|
|
# Help text
|
|
help_text = QLabel(
|
|
"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:
|
|
# 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)
|
|
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("")
|
|
self.cookie_status.setStyleSheet("color: #999999; font-style: italic;")
|
|
cookies_layout.addWidget(self.cookie_status)
|
|
|
|
cookies_layout.addStretch()
|
|
|
|
# === Custom Command Tab ===
|
|
command_tab = QWidget()
|
|
command_layout = QVBoxLayout(command_tab)
|
|
|
|
# Improved help text
|
|
cmd_help_text = QLabel(
|
|
"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.setOpenExternalLinks(True) # Enable clicking links
|
|
cmd_help_text.setTextFormat(Qt.TextFormat.RichText) # Enable HTML rendering
|
|
cmd_help_text.setStyleSheet(
|
|
"""
|
|
QLabel {
|
|
color: #cccccc;
|
|
font-size: 12px;
|
|
padding: 10px;
|
|
background-color: #1a1d20;
|
|
border-radius: 6px;
|
|
line-height: 1.4;
|
|
}
|
|
QLabel a {
|
|
color: #4da6ff;
|
|
text-decoration: underline;
|
|
}
|
|
QLabel a:hover {
|
|
color: #66b3ff;
|
|
}
|
|
"""
|
|
)
|
|
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 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 #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)
|
|
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 #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)
|
|
|
|
# === Proxy Tab ===
|
|
proxy_tab = QWidget()
|
|
proxy_layout = QVBoxLayout(proxy_tab)
|
|
|
|
# Help text
|
|
proxy_help_text = QLabel(
|
|
"Configure proxy settings for network connections and geo-verification.\n"
|
|
"Proxy can help bypass regional restrictions and improve download performance."
|
|
)
|
|
proxy_help_text.setWordWrap(True)
|
|
proxy_help_text.setStyleSheet("color: #999999; padding: 10px;")
|
|
proxy_layout.addWidget(proxy_help_text)
|
|
|
|
# Main Proxy section
|
|
main_proxy_group = QGroupBox("Main Proxy")
|
|
main_proxy_layout = QVBoxLayout(main_proxy_group)
|
|
|
|
main_proxy_help = QLabel("Use the specified HTTP/HTTPS/SOCKS proxy for all connections.")
|
|
main_proxy_help.setWordWrap(True)
|
|
main_proxy_help.setStyleSheet("color: #999999; font-size: 11px;")
|
|
main_proxy_layout.addWidget(main_proxy_help)
|
|
|
|
# Main proxy input
|
|
main_proxy_input_layout = QHBoxLayout()
|
|
main_proxy_input_layout.addWidget(QLabel("Proxy URL:"))
|
|
self.proxy_url_input = QLineEdit()
|
|
self.proxy_url_input.setPlaceholderText("e.g., http://proxy.example.com:8080 or socks5://user:pass@127.0.0.1:1080")
|
|
self.proxy_url_input.textChanged.connect(self.validate_proxy_inputs)
|
|
main_proxy_input_layout.addWidget(self.proxy_url_input)
|
|
main_proxy_layout.addLayout(main_proxy_input_layout)
|
|
|
|
# Example text
|
|
example_label = QLabel("Examples: http://proxy.com:8080, https://proxy.com:8080, socks5://127.0.0.1:1080")
|
|
example_label.setStyleSheet("color: #888888; font-size: 10px; font-style: italic;")
|
|
main_proxy_layout.addWidget(example_label)
|
|
|
|
proxy_layout.addWidget(main_proxy_group)
|
|
|
|
# Geo-verification Proxy section
|
|
geo_proxy_group = QGroupBox("Geo-verification Proxy")
|
|
geo_proxy_layout = QVBoxLayout(geo_proxy_group)
|
|
|
|
geo_proxy_help = QLabel(
|
|
"Use this proxy to verify IP address for geo-restricted sites. "
|
|
"The main proxy (if set) is used for actual downloading."
|
|
)
|
|
geo_proxy_help.setWordWrap(True)
|
|
geo_proxy_help.setStyleSheet("color: #999999; font-size: 11px;")
|
|
geo_proxy_layout.addWidget(geo_proxy_help)
|
|
|
|
# Geo proxy input
|
|
geo_proxy_input_layout = QHBoxLayout()
|
|
geo_proxy_input_layout.addWidget(QLabel("Geo Proxy URL:"))
|
|
self.geo_proxy_url_input = QLineEdit()
|
|
self.geo_proxy_url_input.setPlaceholderText("e.g., http://us-proxy.example.com:8080")
|
|
self.geo_proxy_url_input.textChanged.connect(self.validate_proxy_inputs)
|
|
geo_proxy_input_layout.addWidget(self.geo_proxy_url_input)
|
|
geo_proxy_layout.addLayout(geo_proxy_input_layout)
|
|
|
|
proxy_layout.addWidget(geo_proxy_group)
|
|
|
|
# Proxy status indicator
|
|
self.proxy_status = QLabel("")
|
|
self.proxy_status.setStyleSheet("color: #999999; font-style: italic;")
|
|
proxy_layout.addWidget(self.proxy_status)
|
|
|
|
# Clear buttons
|
|
clear_layout = QHBoxLayout()
|
|
clear_main_proxy_btn = QPushButton("Clear Main Proxy")
|
|
clear_main_proxy_btn.clicked.connect(lambda: self.proxy_url_input.clear())
|
|
clear_main_proxy_btn.setStyleSheet(
|
|
"""
|
|
QPushButton {
|
|
padding: 6px 12px;
|
|
background-color: #444444;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #555555;
|
|
}
|
|
"""
|
|
)
|
|
clear_layout.addWidget(clear_main_proxy_btn)
|
|
|
|
clear_geo_proxy_btn = QPushButton("Clear Geo Proxy")
|
|
clear_geo_proxy_btn.clicked.connect(lambda: self.geo_proxy_url_input.clear())
|
|
clear_geo_proxy_btn.setStyleSheet(
|
|
"""
|
|
QPushButton {
|
|
padding: 6px 12px;
|
|
background-color: #444444;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #555555;
|
|
}
|
|
"""
|
|
)
|
|
clear_layout.addWidget(clear_geo_proxy_btn)
|
|
|
|
clear_layout.addStretch()
|
|
proxy_layout.addLayout(clear_layout)
|
|
|
|
proxy_layout.addStretch()
|
|
|
|
# Add tabs to the tab widget
|
|
self.tab_widget.addTab(cookies_tab, "Login with Cookies")
|
|
self.tab_widget.addTab(command_tab, "Custom Command")
|
|
self.tab_widget.addTab(proxy_tab, "Proxy")
|
|
|
|
# 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)
|
|
|
|
# Apply global styles
|
|
self.setStyleSheet(
|
|
"""
|
|
QDialog {
|
|
background-color: #15181b;
|
|
}
|
|
QTabWidget::pane {
|
|
border: 1px solid #3d3d3d;
|
|
background-color: #15181b;
|
|
}
|
|
QTabBar::tab {
|
|
background-color: #1d1e22;
|
|
color: #ffffff;
|
|
padding: 8px 12px;
|
|
border: 1px solid #3d3d3d;
|
|
border-bottom: none;
|
|
border-top-left-radius: 4px;
|
|
border-top-right-radius: 4px;
|
|
}
|
|
QTabBar::tab:selected {
|
|
background-color: #c90000;
|
|
}
|
|
QTabBar::tab:hover:!selected {
|
|
background-color: #2a2d36;
|
|
}
|
|
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;
|
|
border-radius: 4px;
|
|
background-color: #1b2021;
|
|
color: #ffffff;
|
|
}
|
|
QPushButton {
|
|
padding: 8px 15px;
|
|
background-color: #c90000;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #a50000;
|
|
}
|
|
"""
|
|
)
|
|
|
|
# Initialize dialog with current settings (after all widgets and styles are set)
|
|
self._initialize_cookie_settings()
|
|
self._initialize_proxy_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 _initialize_proxy_settings(self) -> None:
|
|
"""Initialize the dialog with current proxy settings from config"""
|
|
# Load proxy settings from config
|
|
proxy_url = ConfigManager.get("proxy_url")
|
|
geo_proxy_url = ConfigManager.get("geo_proxy_url")
|
|
|
|
# Set proxy field values if they exist
|
|
if proxy_url:
|
|
self.proxy_url_input.setText(proxy_url)
|
|
|
|
if geo_proxy_url:
|
|
self.geo_proxy_url_input.setText(geo_proxy_url)
|
|
|
|
# Update validation status
|
|
self.validate_proxy_inputs()
|
|
|
|
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:
|
|
# 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 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 get_proxy_url(self) -> str | None:
|
|
"""Returns the main proxy URL if specified"""
|
|
proxy_url = self.proxy_url_input.text().strip()
|
|
return proxy_url if proxy_url else None
|
|
|
|
def get_geo_proxy_url(self) -> str | None:
|
|
"""Returns the geo-verification proxy URL if specified"""
|
|
geo_proxy_url = self.geo_proxy_url_input.text().strip()
|
|
return geo_proxy_url if geo_proxy_url else None
|
|
|
|
def validate_proxy_url(self, url: str) -> bool:
|
|
"""Basic validation for proxy URL format"""
|
|
if not url:
|
|
return True # Empty is OK
|
|
|
|
# Check if it starts with a valid scheme
|
|
valid_schemes = ['http://', 'https://', 'socks5://', 'socks4://']
|
|
if not any(url.lower().startswith(scheme) for scheme in valid_schemes):
|
|
return False
|
|
|
|
# Basic URL format check (contains at least host:port)
|
|
try:
|
|
# Remove the scheme to check host:port part
|
|
for scheme in valid_schemes:
|
|
if url.lower().startswith(scheme):
|
|
host_port = url[len(scheme):]
|
|
break
|
|
|
|
# Skip user:pass@ part if present
|
|
if '@' in host_port:
|
|
host_port = host_port.split('@')[1]
|
|
|
|
# Should have at least host:port
|
|
if ':' in host_port:
|
|
host, port = host_port.split(':', 1)
|
|
if host and port.isdigit():
|
|
return True
|
|
|
|
return False
|
|
except:
|
|
return False
|
|
|
|
def validate_proxy_inputs(self) -> None:
|
|
"""Validate proxy inputs and update status"""
|
|
main_proxy = self.proxy_url_input.text().strip()
|
|
geo_proxy = self.geo_proxy_url_input.text().strip()
|
|
|
|
if not main_proxy and not geo_proxy:
|
|
# Check if there are saved settings
|
|
saved_main = ConfigManager.get("proxy_url")
|
|
saved_geo = ConfigManager.get("geo_proxy_url")
|
|
|
|
if saved_main or saved_geo:
|
|
status_parts = []
|
|
if saved_main:
|
|
status_parts.append(f"Saved main proxy: {saved_main}")
|
|
if saved_geo:
|
|
status_parts.append(f"Saved geo proxy: {saved_geo}")
|
|
self.proxy_status.setText(" | ".join(status_parts))
|
|
self.proxy_status.setStyleSheet("color: #888888; font-style: italic;")
|
|
else:
|
|
self.proxy_status.setText("")
|
|
return
|
|
|
|
issues = []
|
|
|
|
if main_proxy and not self.validate_proxy_url(main_proxy):
|
|
issues.append("Invalid main proxy URL format")
|
|
|
|
if geo_proxy and not self.validate_proxy_url(geo_proxy):
|
|
issues.append("Invalid geo proxy URL format")
|
|
|
|
if issues:
|
|
self.proxy_status.setText(" | ".join(issues))
|
|
self.proxy_status.setStyleSheet("color: #ff6666; font-style: italic;")
|
|
else:
|
|
status_parts = []
|
|
if main_proxy:
|
|
status_parts.append("Main proxy configured")
|
|
if geo_proxy:
|
|
status_parts.append("Geo proxy configured")
|
|
|
|
self.proxy_status.setText(" | ".join(status_parts))
|
|
self.proxy_status.setStyleSheet("color: #00cc00; font-style: italic;")
|
|
|
|
def run_custom_command(self) -> None:
|
|
url = self._parent.url_input.text().strip()
|
|
if not url:
|
|
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("🚀 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...")
|
|
|
|
# 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 on_output_received(self, text: str):
|
|
"""Slot for receiving output from the worker"""
|
|
self.log_output.append(text)
|
|
|
|
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 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):
|
|
def __init__(self, parent=None) -> None:
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Download Video Section")
|
|
self.setMinimumWidth(400)
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Help text explaining the feature
|
|
help_text = QLabel(
|
|
"Download only specific parts of a video by specifying time ranges.\n"
|
|
"Use HH:MM:SS format or seconds. Leave start or end empty to download from beginning or to end."
|
|
)
|
|
help_text.setWordWrap(True)
|
|
help_text.setStyleSheet("color: #999999; padding: 10px;")
|
|
layout.addWidget(help_text)
|
|
|
|
# Time range section
|
|
time_group = QGroupBox("Time Range")
|
|
time_layout = QVBoxLayout()
|
|
|
|
# Start time row
|
|
start_layout = QHBoxLayout()
|
|
start_layout.addWidget(QLabel("Start Time:"))
|
|
self.start_time_input = QLineEdit()
|
|
self.start_time_input.setPlaceholderText("00:00:00 (or leave empty for start)")
|
|
start_layout.addWidget(self.start_time_input)
|
|
time_layout.addLayout(start_layout)
|
|
|
|
# End time row
|
|
end_layout = QHBoxLayout()
|
|
end_layout.addWidget(QLabel("End Time:"))
|
|
self.end_time_input = QLineEdit()
|
|
self.end_time_input.setPlaceholderText("00:10:00 (or leave empty for end)")
|
|
end_layout.addWidget(self.end_time_input)
|
|
time_layout.addLayout(end_layout)
|
|
|
|
time_group.setLayout(time_layout)
|
|
layout.addWidget(time_group)
|
|
|
|
# Force keyframes option
|
|
self.force_keyframes = QCheckBox("Force keyframes at cuts (better accuracy, slower)")
|
|
self.force_keyframes.setChecked(True)
|
|
self.force_keyframes.setStyleSheet(
|
|
"""
|
|
QCheckBox {
|
|
color: #ffffff;
|
|
padding: 5px;
|
|
}
|
|
QCheckBox::indicator {
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 4px;
|
|
}
|
|
QCheckBox::indicator:unchecked {
|
|
border: 2px solid #666666;
|
|
background: #1d1e22;
|
|
border-radius: 4px;
|
|
}
|
|
QCheckBox::indicator:checked {
|
|
border: 2px solid #c90000;
|
|
background: #c90000;
|
|
border-radius: 4px;
|
|
}
|
|
"""
|
|
)
|
|
layout.addWidget(self.force_keyframes)
|
|
|
|
# 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)
|
|
|
|
# Apply styling
|
|
self.setStyleSheet(
|
|
"""
|
|
QDialog {
|
|
background-color: #15181b;
|
|
}
|
|
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;
|
|
}
|
|
QLabel {
|
|
color: #ffffff;
|
|
}
|
|
QLineEdit {
|
|
padding: 8px;
|
|
border: 2px solid #1b2021;
|
|
border-radius: 4px;
|
|
background-color: #1b2021;
|
|
color: #ffffff;
|
|
}
|
|
QPushButton {
|
|
padding: 8px 15px;
|
|
background-color: #c90000;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #a50000;
|
|
}
|
|
"""
|
|
)
|
|
|
|
# Initialize 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"""
|
|
start = self.start_time_input.text().strip()
|
|
end = self.end_time_input.text().strip()
|
|
|
|
if not start and not end:
|
|
return None # No selection made
|
|
|
|
if start and end:
|
|
time_range = f"*{start}-{end}"
|
|
elif start:
|
|
time_range = f"*{start}-"
|
|
elif end:
|
|
time_range = f"*-{end}"
|
|
else:
|
|
return None # Shouldn't happen but just in case
|
|
|
|
return time_range
|
|
|
|
def get_force_keyframes(self) -> bool:
|
|
"""Returns whether to force keyframes at cuts"""
|
|
return self.force_keyframes.isChecked()
|