From 6bd6ba8dd1cb9fda08c675e80af0f01f091f6aeb Mon Sep 17 00:00:00 2001 From: Your Name <110548351+oop7@users.noreply.github.com> Date: Wed, 4 Jun 2025 18:46:39 +0300 Subject: [PATCH] Merge cookie/custom dialogs; add trim dialog --- ytsage_gui_dialogs.py | 501 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 499 insertions(+), 2 deletions(-) diff --git a/ytsage_gui_dialogs.py b/ytsage_gui_dialogs.py index 592b7cc..91306b8 100644 --- a/ytsage_gui_dialogs.py +++ b/ytsage_gui_dialogs.py @@ -5,7 +5,7 @@ from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, QProgressBar, QLabel, QFileDialog, QHeaderView, QStyle, QStyleFactory, QComboBox, QTextEdit, QDialog, QPlainTextEdit, QCheckBox, QButtonGroup, QListWidget, - QListWidgetItem, QDialogButtonBox, QScrollArea, QGroupBox) + QListWidgetItem, QDialogButtonBox, QScrollArea, QGroupBox, QTabWidget) from PySide6.QtCore import Qt, Signal, QObject, QThread, QProcess from PySide6.QtGui import QIcon, QPalette, QColor, QPixmap import requests @@ -1296,4 +1296,501 @@ class DownloadSettingsDialog(QDialog): # Renamed class def get_selected_unit_index(self): """Returns the index of the selected speed limit unit.""" - return self.speed_limit_unit.currentIndex() \ No newline at end of file + return self.speed_limit_unit.currentIndex() + +# === New CustomOptions Dialog combining Cookies and Custom Commands === +class CustomOptionsDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.parent = parent + self.setWindowTitle('Custom Options') + self.setMinimumSize(600, 500) + + 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( + "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;") + cookies_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)") + if hasattr(parent, 'cookie_file_path') and parent.cookie_file_path: + self.cookie_path_input.setText(parent.cookie_file_path) + 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 + + # 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) + + # 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." + ) + 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(""" + QCheckBox { + color: #ffffff; + padding: 5px; + margin-left: 0px; + } + 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; + } + """) + command_layout.addWidget(self.sponsorblock_checkbox) + + # 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; + } + """) + command_layout.addWidget(self.command_input) + + # Run command button + self.run_btn = QPushButton("Run Command") + self.run_btn.clicked.connect(self.run_custom_command) + command_layout.addWidget(self.run_btn) + + # 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; + } + """) + command_layout.addWidget(self.log_output) + + # Add tabs to the tab widget + self.tab_widget.addTab(cookies_tab, "Login with Cookies") + self.tab_widget.addTab(command_tab, "Custom Command") + + # Dialog buttons + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.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; + } + 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; + } + """) + + def browse_cookie_file(self): + # Open file dialog to select cookie file + file_dialog = QFileDialog(self) + file_dialog.setFileMode(QFileDialog.ExistingFile) + file_dialog.setNameFilter("Cookies files (*.txt *.lwp)") # Assuming common cookie file extensions + if file_dialog.exec(): + selected_files = file_dialog.selectedFiles() + if selected_files: + self.cookie_path_input.setText(selected_files[0]) + 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): + # Return the selected cookie file path if it's not empty + path = self.cookie_path_input.text().strip() + if path and os.path.exists(path): + return path + return None + + def run_custom_command(self): + url = self.parent.url_input.text().strip() + if not url: + self.log_output.append("Error: No URL provided") + return + + command = self.command_input.toPlainText().strip() + + # 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.run_btn.setEnabled(False) + + # Start command in thread + import threading + threading.Thread(target=self._run_command_thread, + args=(command, url, path), + daemon=True).start() + + def _run_command_thread(self, command, url, path): + try: + class CommandLogger: + def debug(self, msg): + from PySide6.QtCore import QMetaObject, Qt, Q_ARG + QMetaObject.invokeMethod( + self.dialog.log_output, "append", Qt.ConnectionType.QueuedConnection, + Q_ARG(str, msg) + ) + def warning(self, msg): + from PySide6.QtCore import QMetaObject, Qt, Q_ARG + QMetaObject.invokeMethod( + self.dialog.log_output, "append", Qt.ConnectionType.QueuedConnection, + Q_ARG(str, f"Warning: {msg}") + ) + def error(self, msg): + from PySide6.QtCore import QMetaObject, Qt, Q_ARG + QMetaObject.invokeMethod( + self.dialog.log_output, "append", Qt.ConnectionType.QueuedConnection, + Q_ARG(str, f"Error: {msg}") + ) + def __init__(self, dialog): + self.dialog = dialog + + # Split command into arguments + args = command.split() + + # Base options + ydl_opts = { + 'logger': CommandLogger(self), + 'paths': {'home': path}, + 'debug_printout': True, + 'postprocessors': [] + } + + # Add SponsorBlock if selected + if self.sponsorblock_checkbox.isChecked(): + ydl_opts['postprocessors'].append({ + 'key': 'SponsorBlock', + 'categories': ['sponsor', 'intro', 'outro', 'selfpromo', 'preview', 'filler'] + }) + + # Parse additional options + yt_dlp_path = get_yt_dlp_path() + base_cmd = [yt_dlp_path] + args + [url] + + # Show the full command + from PySide6.QtCore import QMetaObject, Qt, Q_ARG + QMetaObject.invokeMethod( + self.log_output, "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: + QMetaObject.invokeMethod( + self.log_output, "append", Qt.ConnectionType.QueuedConnection, + Q_ARG(str, line.rstrip()) + ) + + ret = proc.wait() + if ret != 0: + QMetaObject.invokeMethod( + self.log_output, "append", Qt.ConnectionType.QueuedConnection, + Q_ARG(str, f"Command exited with code {ret}") + ) + else: + QMetaObject.invokeMethod( + self.log_output, "append", Qt.ConnectionType.QueuedConnection, + Q_ARG(str, "Command completed successfully") + ) + except Exception as e: + from PySide6.QtCore import QMetaObject, Qt, Q_ARG + QMetaObject.invokeMethod( + self.log_output, "append", Qt.ConnectionType.QueuedConnection, + Q_ARG(str, f"Error: {str(e)}") + ) + finally: + # Re-enable the run button + QMetaObject.invokeMethod( + self.run_btn, "setEnabled", Qt.ConnectionType.QueuedConnection, + Q_ARG(bool, True) + ) +# === End of CustomOptionsDialog === + +# === Time Range Selection Dialog === +class TimeRangeDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.parent = 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) + + # 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.Ok | QDialogButtonBox.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() + + def update_preview(self): + 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): + """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): + """Returns whether to force keyframes at cuts""" + return self.force_keyframes.isChecked() \ No newline at end of file