Refactor/code cleanup (#37)
* fix imports remove unused imports use full import path sort import (1. Standard Library, 2. Third-Party, 3. Local) in alphabetic. * - remove: Method 3 from src.core.ytsage_downlader:cleanup_subtitle_file - it could delete the subtitle file of other movies if it present in same directory as it scane recursively. - refactor: migrate from os.path to pathlib.Path for path handling - Replaced os.path methods with pathlib.Path to improve readability, - avoid repeatation. - cross-platform compatibility, and maintain cleaner code. - improve: enhance code readability - Standardized string literals to use double quotes for consistency - Removed unnecessary spaces to maintain cleaner formatting - Applied code formatting for better readability and maintainability * - add: ytsage_constants.py file for one place to store all constants. - imporve: return type hint for function. - remove: src/gui/ytsage_gui_dialogs.py file to avoid repetation - src/gui/dialogs is renamed to src/gui/ytsage_gui_dialogs for same naming convection. (future import will remains same) - use of src/gui/ytsage_gui_dialogs/__init__.py to import the dilogs modules. - change: variable self.parent to self._parent so it does not overwrite the parent() - add type hint checking. * - refactor: QMetaObject.invokeMethod to Signal - I encounter error with incokeMethod. Could not solve it. - So, Changed it to Signal to match app code language. - implement: the ytsage_constants.py to code - remove: unnecessary logic - remove: repetitive code logic. - update: yt-dlp logic for src\gui\ytsage_gui_dialogs\ytsage_dialogs_update:_update_binary - yt-dlp update logic will use `yt-dlp -U` * refactor: remove unused imports and streamline code formatting across multiple files
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Dialog modules for YTSage GUI.
|
||||
|
||||
This package contains all dialog classes organized by functionality:
|
||||
|
||||
- ytsage_dialogs_base: Base utility dialogs
|
||||
- ytsage_dialogs_settings: Settings configuration dialogs
|
||||
- ytsage_dialogs_update: Update-related dialogs and threads
|
||||
- ytsage_dialogs_ffmpeg: FFmpeg installation dialogs
|
||||
- ytsage_dialogs_selection: Subtitle and playlist selection dialogs
|
||||
- ytsage_dialogs_custom: Custom functionality dialogs
|
||||
"""
|
||||
|
||||
# Re-export all dialog classes for backward compatibility
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_base import AboutDialog, LogWindow
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_custom import (
|
||||
CookieLoginDialog,
|
||||
CustomCommandDialog,
|
||||
CustomOptionsDialog,
|
||||
TimeRangeDialog,
|
||||
)
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_ffmpeg import FFmpegCheckDialog, FFmpegInstallThread
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_selection import (
|
||||
PlaylistSelectionDialog,
|
||||
SponsorBlockCategoryDialog,
|
||||
SubtitleSelectionDialog,
|
||||
)
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_settings import AutoUpdateSettingsDialog, DownloadSettingsDialog
|
||||
from src.gui.ytsage_gui_dialogs.ytsage_dialogs_update import AutoUpdateThread, UpdateThread, VersionCheckThread, YTDLPUpdateDialog
|
||||
|
||||
__all__ = [
|
||||
# Base dialogs
|
||||
"LogWindow",
|
||||
"AboutDialog",
|
||||
|
||||
# Settings dialogs
|
||||
"DownloadSettingsDialog",
|
||||
"AutoUpdateSettingsDialog",
|
||||
|
||||
# Update dialogs and threads
|
||||
"VersionCheckThread",
|
||||
"UpdateThread",
|
||||
"YTDLPUpdateDialog",
|
||||
"AutoUpdateThread",
|
||||
|
||||
# FFmpeg dialogs
|
||||
"FFmpegInstallThread",
|
||||
"FFmpegCheckDialog",
|
||||
|
||||
# Selection dialogs
|
||||
"SubtitleSelectionDialog",
|
||||
"PlaylistSelectionDialog",
|
||||
"SponsorBlockCategoryDialog",
|
||||
|
||||
# Custom functionality dialogs
|
||||
"CustomCommandDialog",
|
||||
"CookieLoginDialog",
|
||||
"CustomOptionsDialog",
|
||||
"TimeRangeDialog",
|
||||
]
|
||||
@@ -0,0 +1,497 @@
|
||||
"""
|
||||
Base dialogs for YTSage application.
|
||||
Contains basic utility dialogs like LogWindow and AboutDialog.
|
||||
"""
|
||||
|
||||
from PySide6.QtCore import Qt, QThread, QTimer, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QTextEdit,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from src.core.ytsage_ffmpeg import get_ffmpeg_path
|
||||
from src.core.ytsage_utils import _version_cache, check_ffmpeg, get_ffmpeg_version, get_ytdlp_version, refresh_version_cache
|
||||
from src.core.ytsage_yt_dlp import check_ytdlp_installed, get_yt_dlp_path
|
||||
|
||||
|
||||
class LogWindow(QDialog):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("yt-dlp Log")
|
||||
self.setMinimumSize(700, 500)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
self.log_text = QTextEdit()
|
||||
self.log_text.setReadOnly(True)
|
||||
self.log_text.setStyleSheet(
|
||||
"""
|
||||
QTextEdit {
|
||||
background-color: #2b2b2b;
|
||||
color: #ffffff;
|
||||
font-family: Consolas, monospace;
|
||||
font-size: 12px;
|
||||
border: 2px solid #3d3d3d;
|
||||
border-radius: 4px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
layout.addWidget(self.log_text)
|
||||
|
||||
def append_log(self, message) -> None:
|
||||
self.log_text.append(message)
|
||||
# Auto-scroll to bottom
|
||||
scrollbar = self.log_text.verticalScrollBar()
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
|
||||
|
||||
class AboutDialog(QDialog):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self._parent = parent # Store parent to access version etc.
|
||||
self.setWindowTitle("About YTSage")
|
||||
self.setMinimumSize(460, 420) # Slightly increased to accommodate paths
|
||||
self.resize(460, 440) # Slightly increased initial size
|
||||
self.setMaximumSize(500, 480) # Reasonable maximum size
|
||||
|
||||
# Set window flags to make dialog independent of parent movement
|
||||
self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.WindowCloseButtonHint)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(15) # Reduced spacing
|
||||
layout.setContentsMargins(20, 20, 20, 20) # Reduced margins
|
||||
layout.setSizeConstraint(QVBoxLayout.SizeConstraint.SetMinAndMaxSize)
|
||||
|
||||
# App Information Section
|
||||
app_info_widget = self._create_app_info_section()
|
||||
layout.addWidget(app_info_widget)
|
||||
|
||||
# Separator - subtle and compact
|
||||
separator = QWidget()
|
||||
separator.setFixedHeight(1)
|
||||
separator.setStyleSheet("background-color: #2a2a2a; margin: 8px 20px;")
|
||||
layout.addWidget(separator)
|
||||
|
||||
# System Information Section
|
||||
system_info_widget = self._create_system_info_section()
|
||||
layout.addWidget(system_info_widget)
|
||||
|
||||
# Close Button - compact positioning
|
||||
layout.addSpacing(10) # Reduced space before button
|
||||
button_layout = QHBoxLayout()
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
|
||||
button_box.accepted.connect(self.accept)
|
||||
|
||||
# Center the button
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(button_box)
|
||||
button_layout.addStretch()
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Apply overall styling - improved consistency
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
QDialog {
|
||||
background-color: #15181b;
|
||||
color: #ffffff;
|
||||
}
|
||||
QLabel {
|
||||
color: #cccccc;
|
||||
}
|
||||
QPushButton {
|
||||
padding: 10px 30px;
|
||||
background-color: #c90000;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
min-width: 80px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #a50000;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #800000;
|
||||
}
|
||||
QGroupBox {
|
||||
font-weight: bold;
|
||||
border: 1px solid #333333;
|
||||
border-radius: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
background-color: #15181b;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 15px;
|
||||
padding: 0 10px 0 10px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def _create_app_info_section(self) -> QWidget:
|
||||
"""Create the application information section - compact version"""
|
||||
widget = QWidget()
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setSpacing(6) # Reduced spacing
|
||||
|
||||
# Title and Version - more compact
|
||||
title_label = QLabel(
|
||||
"<span style='color: #c90000; font-size: 28px; font-weight: 300; letter-spacing: 2px;'>YTSage</span>"
|
||||
)
|
||||
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(title_label)
|
||||
|
||||
version_label = QLabel(
|
||||
f"<span style='color: #cccccc; font-size: 13px; font-weight: normal;'>Version {getattr(self._parent, 'version', '4.7.0')}</span>"
|
||||
)
|
||||
version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(version_label)
|
||||
|
||||
# Description - more compact
|
||||
description_label = QLabel("A simple GUI frontend for the powerful yt-dlp video downloader.")
|
||||
description_label.setWordWrap(True)
|
||||
description_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
description_label.setStyleSheet("color: #ffffff; font-size: 11px; margin: 6px 0;")
|
||||
layout.addWidget(description_label)
|
||||
|
||||
# Author and Links - compact single line
|
||||
info_layout = QHBoxLayout()
|
||||
info_layout.setSpacing(15)
|
||||
|
||||
author_label = QLabel(
|
||||
"By: <a href='https://github.com/oop7/' style='color: #c90000; text-decoration: none; font-size: 10px;'>oop7</a>"
|
||||
)
|
||||
author_label.setOpenExternalLinks(True)
|
||||
info_layout.addWidget(author_label)
|
||||
|
||||
repo_label = QLabel(
|
||||
"GitHub: <a href='https://github.com/oop7/YTSage/' style='color: #c90000; text-decoration: none; font-size: 10px;'>YTSage</a>"
|
||||
)
|
||||
repo_label.setOpenExternalLinks(True)
|
||||
info_layout.addWidget(repo_label)
|
||||
|
||||
# Center the info layout
|
||||
info_container = QHBoxLayout()
|
||||
info_container.addStretch()
|
||||
info_container.addLayout(info_layout)
|
||||
info_container.addStretch()
|
||||
|
||||
layout.addLayout(info_container)
|
||||
|
||||
return widget
|
||||
|
||||
def _create_system_info_section(self) -> QWidget:
|
||||
"""Create the system information section with compact design"""
|
||||
# Create main container with compact styling
|
||||
container = QWidget()
|
||||
container.setStyleSheet(
|
||||
"""
|
||||
QWidget {
|
||||
border: 1px solid #333333;
|
||||
border-radius: 8px;
|
||||
background-color: #15181b;
|
||||
margin-top: 5px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
main_layout = QVBoxLayout(container)
|
||||
main_layout.setSpacing(8) # Compact spacing
|
||||
main_layout.setContentsMargins(15, 10, 15, 10)
|
||||
|
||||
# Create header with title and refresh button on same line
|
||||
header_layout = QHBoxLayout()
|
||||
header_layout.setContentsMargins(0, 0, 0, 5)
|
||||
|
||||
# System Information title
|
||||
title_label = QLabel("System Information")
|
||||
title_label.setStyleSheet(
|
||||
"""
|
||||
QLabel {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
header_layout.addWidget(title_label)
|
||||
|
||||
# Add stretch to push refresh button to the right
|
||||
header_layout.addStretch()
|
||||
|
||||
# Create refresh button
|
||||
self.refresh_btn = QPushButton("🔄")
|
||||
self.refresh_btn.setFixedSize(16, 16)
|
||||
self.refresh_btn.setStyleSheet(
|
||||
"""
|
||||
QPushButton {
|
||||
padding: 0px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: #cccccc;
|
||||
font-size: 10px;
|
||||
font-weight: normal;
|
||||
margin: 0px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
color: #ffffff;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
color: #c90000;
|
||||
background-color: rgba(201, 0, 0, 0.1);
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.refresh_btn.clicked.connect(self.refresh_version_info)
|
||||
header_layout.addWidget(self.refresh_btn)
|
||||
|
||||
main_layout.addLayout(header_layout)
|
||||
|
||||
# Compact status grid layout
|
||||
self.status_container = QVBoxLayout()
|
||||
self.status_container.setSpacing(6) # Tight spacing
|
||||
self.status_container.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
main_layout.addLayout(self.status_container)
|
||||
|
||||
# Show loading message initially
|
||||
self._show_loading_message()
|
||||
|
||||
# Populate system information asynchronously
|
||||
QTimer.singleShot(100, self.update_system_info)
|
||||
|
||||
return container
|
||||
|
||||
def _show_loading_message(self) -> None:
|
||||
"""Show a compact loading message while system information is being gathered."""
|
||||
loading_label = QLabel("🔄 Loading system information...")
|
||||
loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
loading_label.setStyleSheet(
|
||||
"""
|
||||
QLabel {
|
||||
color: #cccccc;
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
padding: 10px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
self.status_container.addWidget(loading_label)
|
||||
|
||||
def _create_status_item(self, icon, name, status_text, version_text, path_text=None, cache_status="") -> QWidget:
|
||||
"""Create a compact status item widget"""
|
||||
item_widget = QWidget()
|
||||
# Adjust height based on whether we have path info
|
||||
item_height = 50 if path_text else 35
|
||||
item_widget.setMaximumHeight(item_height)
|
||||
item_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
|
||||
# Main layout
|
||||
item_layout = QVBoxLayout(item_widget)
|
||||
item_layout.setContentsMargins(8, 4, 8, 4)
|
||||
item_layout.setSpacing(2)
|
||||
|
||||
# First row: Icon, name, status, version
|
||||
first_row = QHBoxLayout()
|
||||
first_row.setSpacing(8)
|
||||
|
||||
# Icon and name - compact
|
||||
name_label = QLabel(f"{icon} <b>{name}</b>")
|
||||
name_label.setStyleSheet("font-size: 12px; color: #ffffff; font-weight: bold;")
|
||||
name_label.setMinimumWidth(80)
|
||||
first_row.addWidget(name_label)
|
||||
|
||||
# Status - compact
|
||||
status_label = QLabel(status_text)
|
||||
status_label.setStyleSheet("font-size: 11px; font-weight: bold;")
|
||||
status_label.setMinimumWidth(70)
|
||||
first_row.addWidget(status_label)
|
||||
|
||||
# Version info - improved readability
|
||||
version_info = version_text
|
||||
if cache_status:
|
||||
version_info += cache_status
|
||||
|
||||
version_label = QLabel(version_info)
|
||||
version_label.setStyleSheet("font-size: 11px; color: #cccccc;") # Increased from 10px
|
||||
version_label.setWordWrap(False)
|
||||
first_row.addWidget(version_label)
|
||||
|
||||
# Add stretch to push everything left
|
||||
first_row.addStretch()
|
||||
|
||||
item_layout.addLayout(first_row)
|
||||
|
||||
# Second row: Path (if provided)
|
||||
if path_text:
|
||||
path_label = QLabel(f"📁 {path_text}")
|
||||
path_label.setStyleSheet("font-size: 10px; color: #aaaaaa; margin-left: 12px;") # Increased from 9px, better color
|
||||
path_label.setWordWrap(False)
|
||||
# Truncate very long paths
|
||||
if len(str(path_text)) > 60:
|
||||
truncated_path = "..." + str(path_text)[-57:]
|
||||
path_label.setText(f"📁 {truncated_path}")
|
||||
item_layout.addWidget(path_label)
|
||||
|
||||
# Subtle background with minimal border
|
||||
item_widget.setStyleSheet(
|
||||
"""
|
||||
QWidget {
|
||||
background-color: rgba(45, 45, 45, 0.3);
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 4px;
|
||||
margin: 1px;
|
||||
}
|
||||
QWidget:hover {
|
||||
background-color: rgba(60, 60, 60, 0.4);
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
return item_widget
|
||||
|
||||
def update_system_info(self) -> None:
|
||||
"""Update the system information display with compact layout."""
|
||||
# Clear existing items
|
||||
for i in reversed(range(self.status_container.count())):
|
||||
child = self.status_container.itemAt(i).widget()
|
||||
if child:
|
||||
child.deleteLater()
|
||||
|
||||
# yt-dlp Status - compact version with path
|
||||
ytdlp_found = check_ytdlp_installed()
|
||||
ytdlp_status_text = (
|
||||
"<span style='color: #4CAF50;'>✓ Detected</span>" if ytdlp_found else "<span style='color: #F44336;'>✗ Missing</span>"
|
||||
)
|
||||
ytdlp_version = get_ytdlp_version()
|
||||
|
||||
# Get yt-dlp path
|
||||
ytdlp_path = get_yt_dlp_path() if ytdlp_found else None
|
||||
ytdlp_path_text = ytdlp_path if ytdlp_path and ytdlp_path != "yt-dlp" else None
|
||||
|
||||
# Simplified cache status
|
||||
ytdlp_cache = _version_cache.get("ytdlp", {})
|
||||
last_check = ytdlp_cache.get("last_check", 0)
|
||||
cache_status = ""
|
||||
if last_check > 0:
|
||||
from datetime import datetime
|
||||
|
||||
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
|
||||
cache_status = f" <span style='color: #888; font-size: 10px;'>({cache_time})</span>" # Increased from 9px
|
||||
|
||||
ytdlp_item = self._create_status_item(
|
||||
"🎥",
|
||||
"yt-dlp",
|
||||
ytdlp_status_text,
|
||||
ytdlp_version + cache_status,
|
||||
ytdlp_path_text,
|
||||
)
|
||||
self.status_container.addWidget(ytdlp_item)
|
||||
|
||||
# FFmpeg Status - compact version with path
|
||||
ffmpeg_found = check_ffmpeg()
|
||||
ffmpeg_status_text = (
|
||||
"<span style='color: #4CAF50;'>✓ Detected</span>"
|
||||
if ffmpeg_found
|
||||
else "<span style='color: #F44336;'>✗ Missing</span>"
|
||||
)
|
||||
ffmpeg_version = get_ffmpeg_version() if ffmpeg_found else "Not Available"
|
||||
|
||||
# Get FFmpeg path
|
||||
ffmpeg_path_text = None
|
||||
if ffmpeg_found:
|
||||
ffmpeg_path = get_ffmpeg_path()
|
||||
ffmpeg_path_text = ffmpeg_path if ffmpeg_path else None
|
||||
|
||||
# Simplified cache status for FFmpeg
|
||||
ffmpeg_cache = _version_cache.get("ffmpeg", {})
|
||||
last_check = ffmpeg_cache.get("last_check", 0)
|
||||
cache_status = ""
|
||||
if last_check > 0 and ffmpeg_found:
|
||||
from datetime import datetime
|
||||
|
||||
cache_time = datetime.fromtimestamp(last_check).strftime("%H:%M")
|
||||
cache_status = f" <span style='color: #888; font-size: 10px;'>({cache_time})</span>" # Increased from 9px
|
||||
|
||||
ffmpeg_item = self._create_status_item(
|
||||
"🎬",
|
||||
"FFmpeg",
|
||||
ffmpeg_status_text,
|
||||
ffmpeg_version + cache_status,
|
||||
ffmpeg_path_text,
|
||||
)
|
||||
self.status_container.addWidget(ffmpeg_item)
|
||||
|
||||
def refresh_version_info(self) -> None:
|
||||
"""Refresh version information manually."""
|
||||
self.refresh_btn.setText("🔄 Refreshing...")
|
||||
self.refresh_btn.setEnabled(False)
|
||||
|
||||
# Perform refresh in a separate thread to avoid blocking UI
|
||||
class RefreshThread(QThread):
|
||||
finished = Signal(bool)
|
||||
|
||||
def run(self):
|
||||
success = refresh_version_cache(force=True)
|
||||
self.finished.emit(success)
|
||||
|
||||
self.refresh_thread = RefreshThread()
|
||||
self.refresh_thread.finished.connect(self.on_refresh_finished)
|
||||
self.refresh_thread.start()
|
||||
|
||||
def on_refresh_finished(self, success) -> None:
|
||||
"""Handle refresh completion."""
|
||||
self.refresh_btn.setText("🔄 Refresh")
|
||||
self.refresh_btn.setEnabled(True)
|
||||
|
||||
if success:
|
||||
self.update_system_info()
|
||||
else:
|
||||
# Show error message with proper styling
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setIcon(QMessageBox.Icon.Warning)
|
||||
msg_box.setWindowTitle("Refresh Failed")
|
||||
msg_box.setText("Failed to refresh version information.")
|
||||
msg_box.setWindowIcon(self.windowIcon())
|
||||
msg_box.setStyleSheet(
|
||||
"""
|
||||
QMessageBox {
|
||||
background-color: #15181b;
|
||||
color: #ffffff;
|
||||
}
|
||||
QMessageBox QLabel {
|
||||
color: #ffffff;
|
||||
}
|
||||
QMessageBox QPushButton {
|
||||
padding: 8px 15px;
|
||||
background-color: #c90000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
min-width: 80px;
|
||||
}
|
||||
QMessageBox QPushButton:hover {
|
||||
background-color: #a50000;
|
||||
}
|
||||
"""
|
||||
)
|
||||
msg_box.exec()
|
||||
@@ -0,0 +1,796 @@
|
||||
"""
|
||||
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, Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFileDialog,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPlainTextEdit,
|
||||
QPushButton,
|
||||
QTabWidget,
|
||||
QTextEdit,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
|
||||
try:
|
||||
import yt_dlp
|
||||
|
||||
YT_DLP_AVAILABLE = True
|
||||
except ImportError:
|
||||
YT_DLP_AVAILABLE = False
|
||||
|
||||
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:
|
||||
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()
|
||||
|
||||
# Base options
|
||||
ydl_opts = {
|
||||
"logger": CommandLogger(self),
|
||||
"paths": {"home": path},
|
||||
"debug_printout": True,
|
||||
"postprocessors": [],
|
||||
}
|
||||
|
||||
# Add SponsorBlock options if enabled
|
||||
if self.sponsorblock_checkbox.isChecked():
|
||||
ydl_opts["postprocessors"].extend(
|
||||
[
|
||||
{
|
||||
"key": "SponsorBlock",
|
||||
"categories": ["sponsor", "selfpromo", "interaction"],
|
||||
"api": "https://sponsor.ajay.app",
|
||||
},
|
||||
{
|
||||
"key": "ModifyChapters",
|
||||
"remove_sponsor_segments": [
|
||||
"sponsor",
|
||||
"selfpromo",
|
||||
"interaction",
|
||||
],
|
||||
"sponsorblock_chapter_title": "[SponsorBlock]: %(category_names)l",
|
||||
"force_keyframes": True,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Add 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
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
ydl.download([url])
|
||||
|
||||
self.log_output.append("Command completed successfully")
|
||||
|
||||
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()
|
||||
|
||||
|
||||
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(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(self._parent, "cookie_file_path") and self._parent.cookie_file_path:
|
||||
self.cookie_path_input.setText(self._parent.cookie_file_path.as_posix())
|
||||
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.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;
|
||||
}
|
||||
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) -> 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])
|
||||
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 None
|
||||
|
||||
def run_custom_command(self) -> None:
|
||||
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
|
||||
threading.Thread(target=self._run_command_thread, args=(command, url, path), daemon=True).start()
|
||||
|
||||
def _run_command_thread(self, command, url, path) -> None:
|
||||
try:
|
||||
|
||||
class CommandLogger:
|
||||
def debug(self, msg):
|
||||
QMetaObject.invokeMethod(
|
||||
self.dialog.log_output,
|
||||
b"append",
|
||||
Qt.ConnectionType.QueuedConnection,
|
||||
Q_ARG(str, msg),
|
||||
)
|
||||
|
||||
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),
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
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) -> 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:
|
||||
"""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()
|
||||
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
FFmpeg installation dialogs for YTSage application.
|
||||
Contains dialogs and threads for checking and installing FFmpeg.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import webbrowser
|
||||
from io import StringIO
|
||||
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout
|
||||
|
||||
from src.core.ytsage_ffmpeg import auto_install_ffmpeg, check_ffmpeg_installed
|
||||
from src.utils.ytsage_constants import ICON_PATH
|
||||
|
||||
|
||||
class FFmpegInstallThread(QThread):
|
||||
finished = Signal(bool)
|
||||
progress = Signal(str)
|
||||
|
||||
def run(self) -> None:
|
||||
# Redirect stdout to capture progress messages
|
||||
output = StringIO()
|
||||
with contextlib.redirect_stdout(output):
|
||||
success = auto_install_ffmpeg()
|
||||
|
||||
# Process captured output and emit progress signals
|
||||
for line in output.getvalue().splitlines():
|
||||
self.progress.emit(line)
|
||||
|
||||
self.finished.emit(success)
|
||||
|
||||
|
||||
class FFmpegCheckDialog(QDialog):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("FFmpeg Installation")
|
||||
self.setMinimumWidth(450)
|
||||
self.setMinimumHeight(200)
|
||||
self.resize(450, 220)
|
||||
|
||||
# Set the window icon to match the main app
|
||||
if parent and parent.windowIcon():
|
||||
self.setWindowIcon(parent.windowIcon())
|
||||
else:
|
||||
# Try to load the icon directly if parent not available
|
||||
# icon_path logic moved to src\utils\ytsage_constants.py
|
||||
|
||||
if ICON_PATH.exists():
|
||||
self.setWindowIcon(QIcon(ICON_PATH.as_posix()))
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(15)
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
|
||||
# Header with title and improved spacing
|
||||
header_text = QLabel("FFmpeg Installation")
|
||||
header_text.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; padding: 5px 0;")
|
||||
header_text.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(header_text)
|
||||
|
||||
# Message
|
||||
self.message_label = QLabel("YTSage needs FFmpeg to process videos.\n\n" "Choose an installation option below:")
|
||||
self.message_label.setWordWrap(True)
|
||||
self.message_label.setStyleSheet("font-size: 13px; color: #cccccc; padding: 10px 0; line-height: 1.4;")
|
||||
self.message_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.message_label)
|
||||
|
||||
# Progress label with improved styling and compact height
|
||||
self.progress_label = QLabel("")
|
||||
self.progress_label.setWordWrap(True)
|
||||
self.progress_label.setMinimumHeight(60) # Smaller but visible area
|
||||
self.progress_label.setMaximumHeight(80) # Limit maximum height
|
||||
self.progress_label.setStyleSheet(
|
||||
"""
|
||||
QLabel {
|
||||
background-color: #1d1e22;
|
||||
color: #cccccc;
|
||||
border: 1px solid #3d3d3d;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.progress_label.hide()
|
||||
layout.addWidget(self.progress_label)
|
||||
|
||||
# Add minimal stretch - just enough to push buttons down slightly
|
||||
layout.addSpacing(10)
|
||||
|
||||
# Buttons container - simple approach that should work
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(15) # Simple spacing
|
||||
|
||||
# Install button
|
||||
self.install_btn = QPushButton("Install FFmpeg")
|
||||
self.install_btn.clicked.connect(self.start_installation)
|
||||
button_layout.addWidget(self.install_btn)
|
||||
|
||||
# Manual install button
|
||||
self.manual_btn = QPushButton("Manual Guide")
|
||||
self.manual_btn.clicked.connect(lambda: webbrowser.open("https://github.com/oop7/ffmpeg-install-guide"))
|
||||
button_layout.addWidget(self.manual_btn)
|
||||
|
||||
# Close button
|
||||
self.close_btn = QPushButton("Close")
|
||||
self.close_btn.clicked.connect(self.close)
|
||||
button_layout.addWidget(self.close_btn)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Style the dialog to match app theme
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
QDialog {
|
||||
background-color: #15181b;
|
||||
color: #ffffff;
|
||||
}
|
||||
QLabel {
|
||||
color: #cccccc;
|
||||
}
|
||||
QPushButton {
|
||||
padding: 10px 20px;
|
||||
background-color: #c90000;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
min-height: 20px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #a50000;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #800000;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #666666;
|
||||
color: #999999;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
# Initialize installation thread
|
||||
self.install_thread = None
|
||||
|
||||
def start_installation(self) -> None:
|
||||
self.install_btn.setEnabled(False)
|
||||
self.manual_btn.setEnabled(False)
|
||||
self.close_btn.setEnabled(False)
|
||||
|
||||
# Check if FFmpeg is already installed
|
||||
if check_ffmpeg_installed():
|
||||
self.message_label.setText("FFmpeg is already installed!")
|
||||
self.progress_label.setText("Installation complete. You can close this dialog and continue using YTSage.")
|
||||
self.progress_label.show()
|
||||
self.install_btn.hide()
|
||||
self.manual_btn.hide()
|
||||
self.close_btn.setEnabled(True)
|
||||
return
|
||||
|
||||
self.message_label.setText("Installing FFmpeg... Please wait")
|
||||
self.progress_label.show()
|
||||
|
||||
self.install_thread = FFmpegInstallThread()
|
||||
self.install_thread.finished.connect(self.installation_finished)
|
||||
self.install_thread.progress.connect(self.update_progress)
|
||||
self.install_thread.start()
|
||||
|
||||
def update_progress(self, message) -> None:
|
||||
self.progress_label.setText(message)
|
||||
|
||||
def installation_finished(self, success) -> None:
|
||||
if success:
|
||||
self.message_label.setText("FFmpeg has been installed successfully!")
|
||||
self.progress_label.setText("Installation complete. You can now close this dialog and continue using YTSage.")
|
||||
self.install_btn.hide()
|
||||
self.manual_btn.hide()
|
||||
else:
|
||||
self.message_label.setText("FFmpeg installation encountered an issue.")
|
||||
self.progress_label.setText("Please try using the manual installation guide instead.")
|
||||
self.install_btn.setEnabled(True)
|
||||
self.manual_btn.setEnabled(True)
|
||||
|
||||
self.close_btn.setEnabled(True)
|
||||
@@ -0,0 +1,679 @@
|
||||
"""
|
||||
Selection dialogs for YTSage application.
|
||||
Contains dialogs for selecting subtitles, playlist videos, and SponsorBlock categories.
|
||||
"""
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
|
||||
class SubtitleSelectionDialog(QDialog):
|
||||
def __init__(self, available_manual, available_auto, previously_selected, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Select Subtitles")
|
||||
self.setMinimumWidth(400)
|
||||
self.setMinimumHeight(300)
|
||||
|
||||
self.available_manual = available_manual
|
||||
self.available_auto = available_auto
|
||||
self.previously_selected = set(previously_selected) # Use a set for quick lookups
|
||||
self.selected_subtitles = list(previously_selected) # Initialize with previous selection
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# Filter input
|
||||
self.filter_input = QLineEdit()
|
||||
self.filter_input.setPlaceholderText("Filter languages (e.g., en, es)...")
|
||||
self.filter_input.textChanged.connect(self.filter_list)
|
||||
self.filter_input.setStyleSheet(
|
||||
"""
|
||||
QLineEdit {
|
||||
background-color: #363636;
|
||||
border: 2px solid #3d3d3d;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
min-height: 30px;
|
||||
color: white;
|
||||
}
|
||||
QLineEdit:focus {
|
||||
border-color: #ff0000;
|
||||
}
|
||||
"""
|
||||
)
|
||||
layout.addWidget(self.filter_input)
|
||||
|
||||
# Scroll Area for the list
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setStyleSheet("QScrollArea { border: none; }") # Remove border around scroll area
|
||||
layout.addWidget(scroll_area)
|
||||
|
||||
# Container widget for list items (needed for scroll area)
|
||||
self.list_container = QWidget()
|
||||
self.list_layout = QVBoxLayout(self.list_container)
|
||||
self.list_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.list_layout.setSpacing(2) # Compact spacing
|
||||
self.list_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Align items to top
|
||||
scroll_area.setWidget(self.list_container)
|
||||
|
||||
# Populate the list initially
|
||||
self.populate_list()
|
||||
|
||||
# OK and Cancel buttons
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
|
||||
# Style the buttons
|
||||
for button in button_box.buttons():
|
||||
button.setStyleSheet(
|
||||
"""
|
||||
QPushButton {
|
||||
background-color: #363636;
|
||||
border: 2px solid #3d3d3d;
|
||||
border-radius: 4px;
|
||||
padding: 5px 15px; /* Adjust padding */
|
||||
min-height: 30px; /* Ensure consistent height */
|
||||
color: white;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #444444;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #555555;
|
||||
}
|
||||
"""
|
||||
)
|
||||
# Style the OK button specifically if needed
|
||||
if button_box.buttonRole(button) == QDialogButtonBox.ButtonRole.AcceptRole:
|
||||
button.setStyleSheet(
|
||||
button.styleSheet()
|
||||
+ "QPushButton { background-color: #ff0000; border-color: #cc0000; } QPushButton:hover { background-color: #cc0000; }"
|
||||
)
|
||||
|
||||
layout.addWidget(button_box)
|
||||
|
||||
def populate_list(self, filter_text="") -> None:
|
||||
# Clear existing checkboxes from layout
|
||||
while self.list_layout.count():
|
||||
item = self.list_layout.takeAt(0)
|
||||
widget = item.widget()
|
||||
if widget is not None:
|
||||
widget.deleteLater()
|
||||
|
||||
filter_text = filter_text.lower()
|
||||
combined_subs = {}
|
||||
|
||||
# Add manual subs
|
||||
for lang_code, sub_info in self.available_manual.items():
|
||||
if not filter_text or filter_text in lang_code.lower():
|
||||
combined_subs[lang_code] = f"{lang_code} - Manual"
|
||||
|
||||
# Add auto subs (only if no manual exists and matches filter)
|
||||
for lang_code, sub_info in self.available_auto.items():
|
||||
if lang_code not in combined_subs: # Don't overwrite manual
|
||||
if not filter_text or filter_text in lang_code.lower():
|
||||
combined_subs[lang_code] = f"{lang_code} - Auto-generated"
|
||||
|
||||
if not combined_subs:
|
||||
no_subs_label = QLabel("No subtitles available" + (f" matching '{filter_text}'" if filter_text else ""))
|
||||
no_subs_label.setStyleSheet("color: #aaaaaa; padding: 10px;")
|
||||
self.list_layout.addWidget(no_subs_label)
|
||||
return
|
||||
|
||||
# Sort by language code
|
||||
sorted_lang_codes = sorted(combined_subs.keys())
|
||||
|
||||
for lang_code in sorted_lang_codes:
|
||||
item_text = combined_subs[lang_code]
|
||||
checkbox = QCheckBox(item_text)
|
||||
checkbox.setProperty("subtitle_id", item_text) # Store the identifier
|
||||
checkbox.setChecked(item_text in self.previously_selected) # Check if previously selected
|
||||
checkbox.stateChanged.connect(self.update_selection)
|
||||
checkbox.setStyleSheet(
|
||||
"""
|
||||
QCheckBox {
|
||||
color: #ffffff;
|
||||
padding: 5px;
|
||||
}
|
||||
QCheckBox::indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px; /* Square checkboxes */
|
||||
}
|
||||
QCheckBox::indicator:unchecked {
|
||||
border: 2px solid #666666;
|
||||
background: #2b2b2b;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
border: 2px solid #ff0000;
|
||||
background: #ff0000;
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.list_layout.addWidget(checkbox)
|
||||
|
||||
self.list_layout.addStretch() # Pushes items up if list is short
|
||||
|
||||
def filter_list(self) -> None:
|
||||
self.populate_list(self.filter_input.text())
|
||||
|
||||
def update_selection(self, state) -> None:
|
||||
sender = self.sender()
|
||||
subtitle_id = sender.property("subtitle_id")
|
||||
if state == Qt.CheckState.Checked.value:
|
||||
if subtitle_id not in self.previously_selected:
|
||||
self.previously_selected.add(subtitle_id)
|
||||
else:
|
||||
if subtitle_id in self.previously_selected:
|
||||
self.previously_selected.remove(subtitle_id)
|
||||
|
||||
def get_selected_subtitles(self) -> list:
|
||||
# Return the final set as a list
|
||||
return list(self.previously_selected)
|
||||
|
||||
def accept(self) -> None:
|
||||
# Update the final list before closing
|
||||
self.selected_subtitles = self.get_selected_subtitles()
|
||||
super().accept()
|
||||
|
||||
|
||||
class PlaylistSelectionDialog(QDialog):
|
||||
def __init__(self, playlist_entries, previously_selected_string, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Select Playlist Videos")
|
||||
self.setMinimumWidth(500)
|
||||
self.setMinimumHeight(400) # Allow more vertical space
|
||||
|
||||
self.playlist_entries = playlist_entries
|
||||
self.checkboxes = []
|
||||
|
||||
# Main layout
|
||||
main_layout = QVBoxLayout(self)
|
||||
|
||||
# Top buttons (Select/Deselect All)
|
||||
button_layout = QHBoxLayout()
|
||||
select_all_btn = QPushButton("Select All")
|
||||
deselect_all_btn = QPushButton("Deselect All")
|
||||
select_all_btn.clicked.connect(self._select_all)
|
||||
deselect_all_btn.clicked.connect(self._deselect_all)
|
||||
# Style the buttons to match the subtitle dialog
|
||||
select_all_btn.setStyleSheet(
|
||||
"""
|
||||
QPushButton {
|
||||
background-color: #363636;
|
||||
border: 2px solid #3d3d3d;
|
||||
border-radius: 4px;
|
||||
padding: 5px 15px;
|
||||
min-height: 30px;
|
||||
color: white;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #444444;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #555555;
|
||||
}
|
||||
"""
|
||||
)
|
||||
deselect_all_btn.setStyleSheet(select_all_btn.styleSheet())
|
||||
button_layout.addWidget(select_all_btn)
|
||||
button_layout.addWidget(deselect_all_btn)
|
||||
button_layout.addStretch()
|
||||
main_layout.addLayout(button_layout)
|
||||
|
||||
# Scrollable area for checkboxes
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setStyleSheet("QScrollArea { border: none; }") # Remove border around scroll area
|
||||
scroll_widget = QWidget()
|
||||
self.list_layout = QVBoxLayout(scroll_widget) # Layout for checkboxes
|
||||
self.list_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.list_layout.setSpacing(2) # Compact spacing
|
||||
self.list_layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Align items to top
|
||||
scroll_area.setWidget(scroll_widget)
|
||||
main_layout.addWidget(scroll_area)
|
||||
|
||||
# Populate checkboxes
|
||||
self._populate_list(previously_selected_string)
|
||||
|
||||
# Dialog buttons (OK/Cancel)
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
|
||||
# Style the buttons to match subtitle dialog
|
||||
for button in button_box.buttons():
|
||||
button.setStyleSheet(
|
||||
"""
|
||||
QPushButton {
|
||||
background-color: #363636;
|
||||
border: 2px solid #3d3d3d;
|
||||
border-radius: 4px;
|
||||
padding: 5px 15px;
|
||||
min-height: 30px;
|
||||
color: white;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #444444;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #555555;
|
||||
}
|
||||
"""
|
||||
)
|
||||
# Style the OK button specifically if needed
|
||||
if button_box.buttonRole(button) == QDialogButtonBox.ButtonRole.AcceptRole:
|
||||
button.setStyleSheet(
|
||||
button.styleSheet()
|
||||
+ "QPushButton { background-color: #ff0000; border-color: #cc0000; } QPushButton:hover { background-color: #cc0000; }"
|
||||
)
|
||||
|
||||
main_layout.addWidget(button_box)
|
||||
|
||||
# Apply styling to match subtitle dialog
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
QDialog { background-color: #15181b; }
|
||||
QCheckBox {
|
||||
color: #ffffff;
|
||||
padding: 5px;
|
||||
}
|
||||
QCheckBox::indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QCheckBox::indicator:unchecked {
|
||||
border: 2px solid #666666;
|
||||
background: #2b2b2b;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
border: 2px solid #ff0000;
|
||||
background: #ff0000;
|
||||
}
|
||||
QWidget { background-color: #15181b; }
|
||||
"""
|
||||
)
|
||||
|
||||
def _parse_selection_string(self, selection_string) -> set:
|
||||
"""Parses a yt-dlp playlist selection string (e.g., '1-3,5,7-9') into a set of 1-based indices."""
|
||||
selected_indices = set()
|
||||
if not selection_string:
|
||||
# If no previous selection, assume all are selected initially
|
||||
return set(range(1, len(self.playlist_entries) + 1))
|
||||
|
||||
parts = selection_string.split(",")
|
||||
for part in parts:
|
||||
part = part.strip()
|
||||
if "-" in part:
|
||||
try:
|
||||
start, end = map(int, part.split("-"))
|
||||
if start <= end:
|
||||
selected_indices.update(range(start, end + 1))
|
||||
except ValueError:
|
||||
pass # Ignore invalid ranges
|
||||
else:
|
||||
try:
|
||||
selected_indices.add(int(part))
|
||||
except ValueError:
|
||||
pass # Ignore invalid numbers
|
||||
return selected_indices
|
||||
|
||||
def _populate_list(self, previously_selected_string) -> None:
|
||||
"""Populates the scroll area with checkboxes for each video."""
|
||||
selected_indices = self._parse_selection_string(previously_selected_string)
|
||||
|
||||
# Clear existing checkboxes if any (e.g., if repopulating)
|
||||
while self.list_layout.count():
|
||||
child = self.list_layout.takeAt(0)
|
||||
if child.widget():
|
||||
child.widget().deleteLater()
|
||||
self.checkboxes.clear()
|
||||
|
||||
for index, entry in enumerate(self.playlist_entries):
|
||||
if not entry:
|
||||
continue # Skip None entries if yt-dlp returns them
|
||||
|
||||
video_index = index + 1 # yt-dlp uses 1-based indexing
|
||||
title = entry.get("title", f"Video {video_index}")
|
||||
# Shorten title if too long
|
||||
display_title = (title[:70] + "...") if len(title) > 73 else title
|
||||
|
||||
checkbox = QCheckBox(f"{video_index}. {display_title}")
|
||||
checkbox.setChecked(video_index in selected_indices)
|
||||
checkbox.setProperty("video_index", video_index) # Store index
|
||||
checkbox.setStyleSheet(
|
||||
"""
|
||||
QCheckBox {
|
||||
color: #ffffff;
|
||||
padding: 5px;
|
||||
}
|
||||
QCheckBox::indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QCheckBox::indicator:unchecked {
|
||||
border: 2px solid #666666;
|
||||
background: #2b2b2b;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
border: 2px solid #ff0000;
|
||||
background: #ff0000;
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.list_layout.addWidget(checkbox)
|
||||
self.checkboxes.append(checkbox)
|
||||
self.list_layout.addStretch() # Push checkboxes to the top
|
||||
|
||||
def _select_all(self) -> None:
|
||||
for checkbox in self.checkboxes:
|
||||
checkbox.setChecked(True)
|
||||
|
||||
def _deselect_all(self) -> None:
|
||||
for checkbox in self.checkboxes:
|
||||
checkbox.setChecked(False)
|
||||
|
||||
def _condense_indices(self, indices: list[int]) -> str:
|
||||
"""Condenses a list of 1-based indices into a yt-dlp selection string."""
|
||||
if not indices:
|
||||
return ""
|
||||
|
||||
# Remove duplicates and sort in one step
|
||||
indices = sorted(set(indices))
|
||||
|
||||
ranges = []
|
||||
start = end = indices[0]
|
||||
|
||||
for num in indices[1:]:
|
||||
if num == end + 1:
|
||||
end = num
|
||||
else:
|
||||
ranges.append(f"{start}-{end}" if start != end else str(start))
|
||||
start = end = num
|
||||
|
||||
# Append the last range
|
||||
ranges.append(f"{start}-{end}" if start != end else str(start))
|
||||
|
||||
return ",".join(ranges)
|
||||
|
||||
def get_selected_items_string(self) -> str | None:
|
||||
"""Returns the selection string based on checked boxes."""
|
||||
selected_indices = [cb.property("video_index") for cb in self.checkboxes if cb.isChecked()]
|
||||
|
||||
# Check if all items are selected
|
||||
if len(selected_indices) == len(self.playlist_entries):
|
||||
return None # yt-dlp default is all items, so return None or empty string
|
||||
|
||||
return self._condense_indices(selected_indices)
|
||||
|
||||
|
||||
class SponsorBlockCategoryDialog(QDialog):
|
||||
"""Dialog for selecting SponsorBlock categories to remove from videos."""
|
||||
|
||||
# Default SponsorBlock categories with descriptions
|
||||
SPONSORBLOCK_CATEGORIES = {
|
||||
"sponsor": {
|
||||
"name": "Sponsor",
|
||||
"description": "Paid promotion, paid referrals and direct advertisements",
|
||||
"default": True,
|
||||
},
|
||||
"selfpromo": {
|
||||
"name": "Unpaid/Self Promotion",
|
||||
"description": "Unpaid promotion of creators' own content",
|
||||
"default": True,
|
||||
},
|
||||
"interaction": {
|
||||
"name": "Interaction Reminder",
|
||||
"description": "Asking viewers to like, subscribe, or follow social media",
|
||||
"default": True,
|
||||
},
|
||||
"intro": {
|
||||
"name": "Intro",
|
||||
"description": "Video introduction that can be skipped",
|
||||
"default": False,
|
||||
},
|
||||
"outro": {
|
||||
"name": "Outro/End Cards",
|
||||
"description": "Credits or when the video ends",
|
||||
"default": False,
|
||||
},
|
||||
"preview": {
|
||||
"name": "Preview/Recap",
|
||||
"description": "Quick recap of previous videos or preview of what's coming up",
|
||||
"default": False,
|
||||
},
|
||||
"music_offtopic": {
|
||||
"name": "Non-Music Section",
|
||||
"description": "Only for music videos. Marks non-music sections",
|
||||
"default": False,
|
||||
},
|
||||
"filler": {
|
||||
"name": "Filler Tangent",
|
||||
"description": "Tangential scenes added only for filler or humor",
|
||||
"default": False,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, previously_selected=None, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("SponsorBlock Categories")
|
||||
self.setMinimumWidth(500)
|
||||
self.setMinimumHeight(400)
|
||||
|
||||
# Set the window icon to match the main app
|
||||
if parent:
|
||||
self.setWindowIcon(parent.windowIcon())
|
||||
|
||||
self.previously_selected = set(previously_selected) if previously_selected else set()
|
||||
self.checkboxes = {}
|
||||
|
||||
self.init_ui()
|
||||
self.apply_styling()
|
||||
|
||||
def init_ui(self) -> None:
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Title and description
|
||||
title_label = QLabel("SponsorBlock Categories")
|
||||
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #ffffff; margin: 10px;")
|
||||
layout.addWidget(title_label)
|
||||
|
||||
desc_label = QLabel(
|
||||
"Select which types of video segments to automatically remove during download.\n"
|
||||
"SponsorBlock uses community-submitted data to identify these segments."
|
||||
)
|
||||
desc_label.setWordWrap(True)
|
||||
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
|
||||
layout.addWidget(desc_label)
|
||||
|
||||
# Scroll area for categories
|
||||
scroll_area = QScrollArea()
|
||||
scroll_area.setWidgetResizable(True)
|
||||
scroll_area.setStyleSheet("QScrollArea { border: none; }")
|
||||
|
||||
scroll_widget = QWidget()
|
||||
scroll_layout = QVBoxLayout(scroll_widget)
|
||||
scroll_layout.setContentsMargins(10, 0, 10, 0)
|
||||
scroll_layout.setSpacing(8)
|
||||
|
||||
# Add category checkboxes
|
||||
for category_id, category_info in self.SPONSORBLOCK_CATEGORIES.items():
|
||||
# Create a container widget for each category
|
||||
category_widget = QWidget()
|
||||
category_layout = QVBoxLayout(category_widget)
|
||||
category_layout.setContentsMargins(0, 0, 0, 0)
|
||||
category_layout.setSpacing(2)
|
||||
|
||||
# Create checkbox with just the name
|
||||
checkbox = QCheckBox(category_info["name"])
|
||||
checkbox.setProperty("category_id", category_id)
|
||||
|
||||
# Determine if this category should be checked
|
||||
if self.previously_selected:
|
||||
# Use previously selected categories
|
||||
is_checked = category_id in self.previously_selected
|
||||
else:
|
||||
# Use default values for first time
|
||||
is_checked = category_info["default"]
|
||||
|
||||
checkbox.setChecked(is_checked)
|
||||
|
||||
checkbox.setStyleSheet(
|
||||
"""
|
||||
QCheckBox {
|
||||
color: #ffffff;
|
||||
padding: 4px;
|
||||
spacing: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QCheckBox::indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
QCheckBox::indicator:unchecked {
|
||||
border: 2px solid #666666;
|
||||
background: #2b2b2b;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
border: 2px solid #ff0000;
|
||||
background: #ff0000;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
# Create description label
|
||||
desc_label = QLabel(category_info["description"])
|
||||
desc_label.setStyleSheet("color: #aaaaaa; font-size: 11px; margin-left: 28px; margin-bottom: 8px;")
|
||||
desc_label.setWordWrap(True)
|
||||
|
||||
category_layout.addWidget(checkbox)
|
||||
category_layout.addWidget(desc_label)
|
||||
|
||||
self.checkboxes[category_id] = checkbox
|
||||
scroll_layout.addWidget(category_widget)
|
||||
|
||||
scroll_layout.addStretch()
|
||||
scroll_area.setWidget(scroll_widget)
|
||||
layout.addWidget(scroll_area)
|
||||
|
||||
# Quick selection buttons
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
select_defaults_btn = QPushButton("Select Defaults")
|
||||
select_defaults_btn.clicked.connect(self.select_defaults)
|
||||
select_defaults_btn.setStyleSheet(self._get_button_style())
|
||||
|
||||
select_all_btn = QPushButton("Select All")
|
||||
select_all_btn.clicked.connect(self.select_all)
|
||||
select_all_btn.setStyleSheet(self._get_button_style())
|
||||
|
||||
deselect_all_btn = QPushButton("Deselect All")
|
||||
deselect_all_btn.clicked.connect(self.deselect_all)
|
||||
deselect_all_btn.setStyleSheet(self._get_button_style())
|
||||
|
||||
button_layout.addWidget(select_defaults_btn)
|
||||
button_layout.addWidget(select_all_btn)
|
||||
button_layout.addWidget(deselect_all_btn)
|
||||
button_layout.addStretch()
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Dialog buttons
|
||||
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
button_box.accepted.connect(self.accept)
|
||||
button_box.rejected.connect(self.reject)
|
||||
|
||||
# Style the dialog buttons
|
||||
for button in button_box.buttons():
|
||||
button.setStyleSheet(self._get_button_style())
|
||||
if button_box.buttonRole(button) == QDialogButtonBox.ButtonRole.AcceptRole:
|
||||
button.setStyleSheet(
|
||||
button.styleSheet()
|
||||
+ "QPushButton { background-color: #ff0000; border-color: #cc0000; } "
|
||||
+ "QPushButton:hover { background-color: #cc0000; }"
|
||||
)
|
||||
|
||||
layout.addWidget(button_box)
|
||||
|
||||
def _get_button_style(self) -> str:
|
||||
"""Returns the standard button style for this dialog."""
|
||||
return """
|
||||
QPushButton {
|
||||
background-color: #363636;
|
||||
border: 2px solid #3d3d3d;
|
||||
border-radius: 4px;
|
||||
padding: 5px 15px;
|
||||
min-height: 30px;
|
||||
color: white;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #444444;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #555555;
|
||||
}
|
||||
"""
|
||||
|
||||
def apply_styling(self) -> None:
|
||||
"""Apply the dialog styling to match the rest of the application."""
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
QDialog {
|
||||
background-color: #15181b;
|
||||
color: #ffffff;
|
||||
}
|
||||
QLabel {
|
||||
color: #ffffff;
|
||||
}
|
||||
QWidget {
|
||||
background-color: #15181b;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def select_defaults(self) -> None:
|
||||
"""Select only the default categories."""
|
||||
for category_id, checkbox in self.checkboxes.items():
|
||||
default_value = self.SPONSORBLOCK_CATEGORIES[category_id]["default"]
|
||||
checkbox.setChecked(default_value)
|
||||
|
||||
def select_all(self) -> None:
|
||||
"""Select all categories."""
|
||||
for checkbox in self.checkboxes.values():
|
||||
checkbox.setChecked(True)
|
||||
|
||||
def deselect_all(self) -> None:
|
||||
"""Deselect all categories."""
|
||||
for checkbox in self.checkboxes.values():
|
||||
checkbox.setChecked(False)
|
||||
|
||||
def get_selected_categories(self) -> list:
|
||||
"""Returns a list of selected category IDs."""
|
||||
selected = []
|
||||
for category_id, checkbox in self.checkboxes.items():
|
||||
if checkbox.isChecked():
|
||||
selected.append(category_id)
|
||||
return selected
|
||||
|
||||
def get_selected_categories_string(self) -> str:
|
||||
"""Returns a comma-separated string of selected categories for yt-dlp."""
|
||||
selected = self.get_selected_categories()
|
||||
return ",".join(selected) if selected else ""
|
||||
@@ -0,0 +1,743 @@
|
||||
"""
|
||||
Settings-related dialogs for YTSage application.
|
||||
Contains dialogs for configuring download settings and auto-update preferences.
|
||||
"""
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (
|
||||
QButtonGroup,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFileDialog,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QRadioButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
|
||||
from src.core.ytsage_logging import logger
|
||||
from src.core.ytsage_utils import (
|
||||
check_and_update_ytdlp_auto,
|
||||
get_auto_update_settings,
|
||||
get_ytdlp_version,
|
||||
update_auto_update_settings,
|
||||
)
|
||||
|
||||
|
||||
class DownloadSettingsDialog(QDialog):
|
||||
def __init__(self, current_path, current_limit, current_unit_index, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Download Settings")
|
||||
self.setMinimumWidth(450)
|
||||
self.setMinimumHeight(400)
|
||||
self.current_path = current_path
|
||||
self.current_limit = current_limit if current_limit is not None else ""
|
||||
self.current_unit_index = current_unit_index
|
||||
|
||||
# Apply main app styling
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
QDialog {
|
||||
background-color: #15181b;
|
||||
color: #ffffff;
|
||||
}
|
||||
QWidget {
|
||||
background-color: #15181b;
|
||||
color: #ffffff;
|
||||
}
|
||||
QLabel {
|
||||
color: #ffffff;
|
||||
}
|
||||
QGroupBox {
|
||||
color: #ffffff;
|
||||
border: 2px solid #1b2021;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
font-weight: bold;
|
||||
background-color: #15181b;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 10px;
|
||||
padding: 0 5px 0 5px;
|
||||
color: #ffffff;
|
||||
}
|
||||
QLineEdit {
|
||||
padding: 8px;
|
||||
border: 2px solid #1b2021;
|
||||
border-radius: 4px;
|
||||
background-color: #1b2021;
|
||||
color: #ffffff;
|
||||
selection-background-color: #c90000;
|
||||
selection-color: #ffffff;
|
||||
}
|
||||
QPushButton {
|
||||
padding: 8px 15px;
|
||||
background-color: #c90000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
min-height: 20px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #a50000;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #800000;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #666666;
|
||||
color: #999999;
|
||||
}
|
||||
QCheckBox {
|
||||
spacing: 5px;
|
||||
color: #ffffff;
|
||||
}
|
||||
QCheckBox::indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
}
|
||||
QCheckBox::indicator:unchecked {
|
||||
border: 2px solid #666666;
|
||||
background: #15181b;
|
||||
}
|
||||
QCheckBox::indicator:checked {
|
||||
border: 2px solid #c90000;
|
||||
background: #c90000;
|
||||
}
|
||||
QRadioButton {
|
||||
spacing: 5px;
|
||||
color: #ffffff;
|
||||
}
|
||||
QRadioButton::indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
}
|
||||
QRadioButton::indicator:unchecked {
|
||||
border: 2px solid #666666;
|
||||
background: #15181b;
|
||||
}
|
||||
QRadioButton::indicator:checked {
|
||||
border: 2px solid #c90000;
|
||||
background: #c90000;
|
||||
}
|
||||
QComboBox {
|
||||
padding: 5px;
|
||||
border: 2px solid #1b2021;
|
||||
border-radius: 4px;
|
||||
background-color: #1b2021;
|
||||
color: #ffffff;
|
||||
min-height: 20px;
|
||||
}
|
||||
QComboBox::drop-down {
|
||||
border: none;
|
||||
width: 20px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
border: 2px solid #1b2021;
|
||||
border-radius: 4px;
|
||||
background-color: #15181b;
|
||||
color: #ffffff;
|
||||
selection-background-color: #c90000;
|
||||
selection-color: #ffffff;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# --- Download Path Section ---
|
||||
path_group_box = QGroupBox("Download Path")
|
||||
path_layout = QVBoxLayout()
|
||||
|
||||
self.path_display = QLabel(self.current_path)
|
||||
self.path_display.setWordWrap(True)
|
||||
self.path_display.setStyleSheet(
|
||||
"QLabel { color: #ffffff; padding: 5px; border: 1px solid #1b2021; border-radius: 4px; background-color: #1b2021; }"
|
||||
)
|
||||
path_layout.addWidget(self.path_display)
|
||||
|
||||
browse_button = QPushButton("Browse...")
|
||||
browse_button.clicked.connect(self.browse_new_path)
|
||||
path_layout.addWidget(browse_button)
|
||||
|
||||
path_group_box.setLayout(path_layout)
|
||||
layout.addWidget(path_group_box)
|
||||
|
||||
# --- Speed Limit Section ---
|
||||
speed_group_box = QGroupBox("Speed Limit")
|
||||
speed_layout = QHBoxLayout()
|
||||
|
||||
self.speed_limit_input = QLineEdit(str(self.current_limit))
|
||||
self.speed_limit_input.setPlaceholderText("None")
|
||||
speed_layout.addWidget(self.speed_limit_input)
|
||||
|
||||
self.speed_limit_unit = QComboBox()
|
||||
self.speed_limit_unit.addItems(["KB/s", "MB/s"])
|
||||
self.speed_limit_unit.setCurrentIndex(self.current_unit_index)
|
||||
speed_layout.addWidget(self.speed_limit_unit)
|
||||
|
||||
speed_group_box.setLayout(speed_layout)
|
||||
layout.addWidget(speed_group_box)
|
||||
|
||||
# --- Auto-Update yt-dlp Section ---
|
||||
auto_update_group_box = QGroupBox("Auto-Update yt-dlp")
|
||||
auto_update_layout = QVBoxLayout()
|
||||
|
||||
# Load current auto-update settings
|
||||
auto_settings = get_auto_update_settings()
|
||||
|
||||
# Enable/Disable auto-update checkbox
|
||||
self.auto_update_enabled = QCheckBox("Enable automatic yt-dlp updates")
|
||||
self.auto_update_enabled.setChecked(auto_settings["enabled"])
|
||||
auto_update_layout.addWidget(self.auto_update_enabled)
|
||||
|
||||
# Frequency options
|
||||
frequency_label = QLabel("Update frequency:")
|
||||
frequency_label.setStyleSheet("color: #ffffff; margin-top: 10px;")
|
||||
auto_update_layout.addWidget(frequency_label)
|
||||
|
||||
self.startup_radio = QRadioButton("Check on every startup (minimum 1 hour between checks)")
|
||||
self.daily_radio = QRadioButton("Check daily")
|
||||
self.weekly_radio = QRadioButton("Check weekly")
|
||||
|
||||
# Set current selection based on saved settings
|
||||
current_frequency = auto_settings["frequency"]
|
||||
if current_frequency == "startup":
|
||||
self.startup_radio.setChecked(True)
|
||||
elif current_frequency == "daily":
|
||||
self.daily_radio.setChecked(True)
|
||||
else: # weekly
|
||||
self.weekly_radio.setChecked(True)
|
||||
|
||||
auto_update_layout.addWidget(self.startup_radio)
|
||||
auto_update_layout.addWidget(self.daily_radio)
|
||||
auto_update_layout.addWidget(self.weekly_radio)
|
||||
|
||||
# Test update button
|
||||
test_update_layout = QHBoxLayout()
|
||||
test_update_button = QPushButton("Check for Updates Now")
|
||||
test_update_button.clicked.connect(self.test_update_check)
|
||||
test_update_layout.addWidget(test_update_button)
|
||||
test_update_layout.addStretch()
|
||||
auto_update_layout.addLayout(test_update_layout)
|
||||
|
||||
auto_update_group_box.setLayout(auto_update_layout)
|
||||
layout.addWidget(auto_update_group_box)
|
||||
|
||||
# Dialog buttons (OK/Cancel)
|
||||
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_new_path(self) -> None:
|
||||
new_path = QFileDialog.getExistingDirectory(self, "Select Download Directory", self.current_path)
|
||||
if new_path:
|
||||
self.current_path = new_path
|
||||
self.path_display.setText(self.current_path)
|
||||
|
||||
def get_selected_path(self) -> str:
|
||||
"""Returns the confirmed path after the dialog is accepted."""
|
||||
return self.current_path
|
||||
|
||||
def get_selected_speed_limit(self) -> str | None:
|
||||
"""Returns the entered speed limit value (as string or None)."""
|
||||
limit_str = self.speed_limit_input.text().strip()
|
||||
if not limit_str:
|
||||
return None
|
||||
try:
|
||||
float(limit_str) # Check if convertible to float
|
||||
return limit_str
|
||||
except ValueError:
|
||||
logger.info("Invalid speed limit input in dialog")
|
||||
return None
|
||||
|
||||
def get_selected_unit_index(self) -> int:
|
||||
"""Returns the index of the selected speed limit unit."""
|
||||
return self.speed_limit_unit.currentIndex()
|
||||
|
||||
def _create_styled_message_box(self, icon, title, text) -> QMessageBox:
|
||||
"""Create a styled QMessageBox that matches the app theme."""
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setIcon(icon)
|
||||
msg_box.setWindowTitle(title)
|
||||
msg_box.setText(text)
|
||||
msg_box.setWindowIcon(self.windowIcon())
|
||||
msg_box.setStyleSheet(
|
||||
"""
|
||||
QMessageBox {
|
||||
background-color: #15181b;
|
||||
color: #ffffff;
|
||||
}
|
||||
QMessageBox QLabel {
|
||||
color: #ffffff;
|
||||
}
|
||||
QMessageBox QPushButton {
|
||||
padding: 8px 15px;
|
||||
background-color: #c90000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
min-width: 80px;
|
||||
}
|
||||
QMessageBox QPushButton:hover {
|
||||
background-color: #a50000;
|
||||
}
|
||||
QMessageBox QPushButton:pressed {
|
||||
background-color: #800000;
|
||||
}
|
||||
"""
|
||||
)
|
||||
return msg_box
|
||||
|
||||
def test_update_check(self) -> None:
|
||||
"""Test the update check functionality."""
|
||||
try:
|
||||
# Get current version
|
||||
current_version = get_ytdlp_version()
|
||||
if "Error" in current_version:
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Warning,
|
||||
"Update Check",
|
||||
"Could not determine current yt-dlp version.",
|
||||
)
|
||||
msg_box.exec()
|
||||
return
|
||||
|
||||
# Get latest version from PyPI
|
||||
response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10)
|
||||
response.raise_for_status()
|
||||
latest_version = response.json()["info"]["version"]
|
||||
|
||||
# Clean up version strings
|
||||
current_version = current_version.replace("_", ".")
|
||||
latest_version = latest_version.replace("_", ".")
|
||||
|
||||
from packaging import version as version_parser
|
||||
|
||||
if version_parser.parse(latest_version) > version_parser.parse(current_version):
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Information,
|
||||
"Update Check",
|
||||
f"Update available!\n\nCurrent: {current_version}\nLatest: {latest_version}\n\nUse the 'Update yt-dlp' button in the main window to update.",
|
||||
)
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Information,
|
||||
"Update Check",
|
||||
f"yt-dlp is up to date!\n\nCurrent version: {current_version}",
|
||||
)
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Warning,
|
||||
"Update Check",
|
||||
f"Error checking for updates: {str(e)}",
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
def get_auto_update_settings(self) -> tuple[bool, str]:
|
||||
"""Returns the auto-update settings from the dialog."""
|
||||
enabled = self.auto_update_enabled.isChecked()
|
||||
|
||||
if self.startup_radio.isChecked():
|
||||
frequency = "startup"
|
||||
elif self.daily_radio.isChecked():
|
||||
frequency = "daily"
|
||||
else: # weekly_radio is checked
|
||||
frequency = "weekly"
|
||||
|
||||
return enabled, frequency
|
||||
|
||||
def accept(self) -> None:
|
||||
"""Override accept to save auto-update settings."""
|
||||
try:
|
||||
# Save auto-update settings
|
||||
enabled, frequency = self.get_auto_update_settings()
|
||||
|
||||
if update_auto_update_settings(enabled, frequency):
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Settings Saved",
|
||||
"Auto-update settings have been saved successfully!",
|
||||
)
|
||||
else:
|
||||
QMessageBox.warning(self, "Error", "Failed to save auto-update settings.")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Error saving auto-update settings: {str(e)}")
|
||||
|
||||
# Call the parent accept method to close the dialog
|
||||
super().accept()
|
||||
|
||||
|
||||
class AutoUpdateSettingsDialog(QDialog):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Auto-Update Settings")
|
||||
self.setMinimumWidth(400)
|
||||
self.setMinimumHeight(300)
|
||||
|
||||
# Set the window icon to match the main app
|
||||
if parent:
|
||||
self.setWindowIcon(parent.windowIcon())
|
||||
|
||||
self.init_ui()
|
||||
self.load_current_settings()
|
||||
self.apply_styling()
|
||||
|
||||
def init_ui(self) -> None:
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Title
|
||||
title_label = QLabel("<h2>🔄 Auto-Update Settings</h2>")
|
||||
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(title_label)
|
||||
|
||||
# Description
|
||||
desc_label = QLabel("Configure automatic updates for yt-dlp to ensure you always have the latest features and bug fixes.")
|
||||
desc_label.setWordWrap(True)
|
||||
desc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
desc_label.setStyleSheet("color: #cccccc; margin: 10px; font-size: 11px;")
|
||||
layout.addWidget(desc_label)
|
||||
|
||||
# Enable/Disable auto-update
|
||||
self.enable_checkbox = QCheckBox("Enable automatic yt-dlp updates")
|
||||
self.enable_checkbox.setChecked(True) # Default enabled
|
||||
self.enable_checkbox.toggled.connect(self.on_enable_toggled)
|
||||
layout.addWidget(self.enable_checkbox)
|
||||
|
||||
# Frequency options
|
||||
frequency_group = QGroupBox("Update Frequency")
|
||||
frequency_layout = QVBoxLayout()
|
||||
|
||||
self.frequency_group = QButtonGroup(self)
|
||||
|
||||
self.startup_radio = QRadioButton("Check on every startup (minimum 1 hour between checks)")
|
||||
self.daily_radio = QRadioButton("Check daily")
|
||||
self.weekly_radio = QRadioButton("Check weekly")
|
||||
|
||||
self.daily_radio.setChecked(True) # Default to daily
|
||||
|
||||
self.frequency_group.addButton(self.startup_radio, 0)
|
||||
self.frequency_group.addButton(self.daily_radio, 1)
|
||||
self.frequency_group.addButton(self.weekly_radio, 2)
|
||||
|
||||
frequency_layout.addWidget(self.startup_radio)
|
||||
frequency_layout.addWidget(self.daily_radio)
|
||||
frequency_layout.addWidget(self.weekly_radio)
|
||||
frequency_group.setLayout(frequency_layout)
|
||||
|
||||
layout.addWidget(frequency_group)
|
||||
|
||||
# Current status
|
||||
status_group = QGroupBox("Current Status")
|
||||
status_layout = QVBoxLayout()
|
||||
|
||||
self.current_version_label = QLabel("Current yt-dlp version: Checking...")
|
||||
self.last_check_label = QLabel("Last update check: Never")
|
||||
self.next_check_label = QLabel("Next check: Based on settings")
|
||||
|
||||
status_layout.addWidget(self.current_version_label)
|
||||
status_layout.addWidget(self.last_check_label)
|
||||
status_layout.addWidget(self.next_check_label)
|
||||
status_group.setLayout(status_layout)
|
||||
|
||||
layout.addWidget(status_group)
|
||||
|
||||
# Manual check button
|
||||
self.manual_check_btn = QPushButton("🔍 Check for Updates Now")
|
||||
self.manual_check_btn.clicked.connect(self.manual_check)
|
||||
layout.addWidget(self.manual_check_btn)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
self.save_btn = QPushButton("Save Settings")
|
||||
self.save_btn.clicked.connect(self.save_settings)
|
||||
|
||||
self.cancel_btn = QPushButton("Cancel")
|
||||
self.cancel_btn.clicked.connect(self.reject)
|
||||
|
||||
button_layout.addWidget(self.save_btn)
|
||||
button_layout.addWidget(self.cancel_btn)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
def apply_styling(self) -> None:
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
QDialog {
|
||||
background-color: #15181b;
|
||||
color: #ffffff;
|
||||
}
|
||||
QLabel {
|
||||
color: #ffffff;
|
||||
}
|
||||
QGroupBox {
|
||||
color: #ffffff;
|
||||
border: 2px solid #1b2021;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
font-weight: bold;
|
||||
background-color: #15181b;
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
left: 10px;
|
||||
padding: 0 5px 0 5px;
|
||||
color: #ffffff;
|
||||
}
|
||||
QCheckBox, QRadioButton {
|
||||
color: #ffffff;
|
||||
spacing: 5px;
|
||||
margin: 5px;
|
||||
}
|
||||
QCheckBox::indicator, QRadioButton::indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
}
|
||||
QCheckBox::indicator:unchecked, QRadioButton::indicator:unchecked {
|
||||
border: 2px solid #666666;
|
||||
background: #15181b;
|
||||
}
|
||||
QCheckBox::indicator:checked, QRadioButton::indicator:checked {
|
||||
border: 2px solid #c90000;
|
||||
background: #c90000;
|
||||
}
|
||||
QPushButton {
|
||||
padding: 8px 15px;
|
||||
background-color: #c90000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
margin: 5px;
|
||||
min-width: 100px;
|
||||
min-height: 20px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #a50000;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #800000;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #666666;
|
||||
color: #999999;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
def load_current_settings(self) -> None:
|
||||
"""Load current auto-update settings from config."""
|
||||
try:
|
||||
settings = get_auto_update_settings()
|
||||
|
||||
# Set checkbox
|
||||
self.enable_checkbox.setChecked(settings["enabled"])
|
||||
|
||||
# Set frequency
|
||||
frequency = settings["frequency"]
|
||||
if frequency == "startup":
|
||||
self.startup_radio.setChecked(True)
|
||||
elif frequency == "weekly":
|
||||
self.weekly_radio.setChecked(True)
|
||||
else: # daily
|
||||
self.daily_radio.setChecked(True)
|
||||
|
||||
# Update status labels
|
||||
current_version = get_ytdlp_version()
|
||||
self.current_version_label.setText(f"Current yt-dlp version: {current_version}")
|
||||
|
||||
last_check = settings["last_check"]
|
||||
if last_check > 0:
|
||||
last_check_time = datetime.fromtimestamp(last_check).strftime("%Y-%m-%d %H:%M:%S")
|
||||
self.last_check_label.setText(f"Last update check: {last_check_time}")
|
||||
else:
|
||||
self.last_check_label.setText("Last update check: Never")
|
||||
|
||||
# Calculate next check time
|
||||
self.update_next_check_label()
|
||||
|
||||
# Update UI state
|
||||
self.on_enable_toggled(settings["enabled"])
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading auto-update settings: {e}")
|
||||
|
||||
def update_next_check_label(self) -> None:
|
||||
"""Update the next check label based on current settings."""
|
||||
try:
|
||||
if not self.enable_checkbox.isChecked():
|
||||
self.next_check_label.setText("Next check: Disabled")
|
||||
return
|
||||
|
||||
settings = get_auto_update_settings()
|
||||
last_check = settings["last_check"]
|
||||
frequency = self.get_selected_frequency()
|
||||
|
||||
if last_check == 0:
|
||||
self.next_check_label.setText("Next check: On next startup")
|
||||
return
|
||||
|
||||
next_check_time = last_check
|
||||
if frequency == "startup":
|
||||
next_check_time += 3600 # 1 hour
|
||||
elif frequency == "daily":
|
||||
next_check_time += 86400 # 24 hours
|
||||
elif frequency == "weekly":
|
||||
next_check_time += 604800 # 7 days
|
||||
|
||||
current_time = time.time()
|
||||
if next_check_time <= current_time:
|
||||
self.next_check_label.setText("Next check: Now (overdue)")
|
||||
else:
|
||||
next_check_datetime = datetime.fromtimestamp(next_check_time)
|
||||
self.next_check_label.setText(f"Next check: {next_check_datetime.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
except Exception as e:
|
||||
self.next_check_label.setText("Next check: Error calculating")
|
||||
logger.error(f"Error calculating next check time: {e}")
|
||||
|
||||
def on_enable_toggled(self, enabled) -> None:
|
||||
"""Handle enable/disable checkbox toggle."""
|
||||
# Enable/disable frequency options
|
||||
for i in range(self.frequency_group.buttons().__len__()):
|
||||
self.frequency_group.button(i).setEnabled(enabled)
|
||||
|
||||
self.update_next_check_label()
|
||||
|
||||
def get_selected_frequency(self) -> str:
|
||||
"""Get the selected frequency setting."""
|
||||
if self.startup_radio.isChecked():
|
||||
return "startup"
|
||||
elif self.weekly_radio.isChecked():
|
||||
return "weekly"
|
||||
else:
|
||||
return "daily"
|
||||
|
||||
def manual_check(self) -> None:
|
||||
"""Perform a manual update check."""
|
||||
self.manual_check_btn.setEnabled(False)
|
||||
self.manual_check_btn.setText("🔄 Checking...")
|
||||
|
||||
# Force an immediate update check
|
||||
def check_in_thread() -> None:
|
||||
try:
|
||||
result = check_and_update_ytdlp_auto()
|
||||
|
||||
# Update UI in main thread
|
||||
from PySide6.QtCore import QTimer
|
||||
|
||||
QTimer.singleShot(0, lambda: self.manual_check_finished(result))
|
||||
except Exception as e:
|
||||
logger.error(f"Error during manual check: {e}")
|
||||
QTimer.singleShot(0, lambda: self.manual_check_finished(False))
|
||||
|
||||
# Run in separate thread to avoid blocking UI
|
||||
import threading
|
||||
|
||||
threading.Thread(target=check_in_thread, daemon=True).start()
|
||||
|
||||
def _create_styled_message_box(self, icon, title, text) -> QMessageBox:
|
||||
"""Create a styled QMessageBox that matches the app theme."""
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setIcon(icon)
|
||||
msg_box.setWindowTitle(title)
|
||||
msg_box.setText(text)
|
||||
msg_box.setWindowIcon(self.windowIcon())
|
||||
msg_box.setStyleSheet(
|
||||
"""
|
||||
QMessageBox {
|
||||
background-color: #15181b;
|
||||
color: #ffffff;
|
||||
}
|
||||
QMessageBox QLabel {
|
||||
color: #ffffff;
|
||||
}
|
||||
QMessageBox QPushButton {
|
||||
padding: 8px 15px;
|
||||
background-color: #c90000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
min-width: 80px;
|
||||
}
|
||||
QMessageBox QPushButton:hover {
|
||||
background-color: #a50000;
|
||||
}
|
||||
QMessageBox QPushButton:pressed {
|
||||
background-color: #800000;
|
||||
}
|
||||
"""
|
||||
)
|
||||
return msg_box
|
||||
|
||||
def manual_check_finished(self, success) -> None:
|
||||
"""Handle completion of manual update check."""
|
||||
self.manual_check_btn.setEnabled(True)
|
||||
self.manual_check_btn.setText("🔍 Check for Updates Now")
|
||||
|
||||
if success:
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Information,
|
||||
"Update Check",
|
||||
"✅ Update check completed successfully!\nCheck the console for details.",
|
||||
)
|
||||
msg_box.exec()
|
||||
else:
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Warning,
|
||||
"Update Check",
|
||||
"❌ Update check failed.\nCheck the console for error details.",
|
||||
)
|
||||
msg_box.exec()
|
||||
|
||||
# Refresh the current settings display
|
||||
self.load_current_settings()
|
||||
|
||||
def save_settings(self) -> None:
|
||||
"""Save the auto-update settings."""
|
||||
try:
|
||||
enabled = self.enable_checkbox.isChecked()
|
||||
frequency = self.get_selected_frequency()
|
||||
|
||||
if update_auto_update_settings(enabled, frequency):
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Information,
|
||||
"Settings Saved",
|
||||
"✅ Auto-update settings have been saved successfully!",
|
||||
)
|
||||
msg_box.exec()
|
||||
self.accept()
|
||||
else:
|
||||
msg_box = self._create_styled_message_box(
|
||||
QMessageBox.Icon.Warning,
|
||||
"Error",
|
||||
"❌ Failed to save auto-update settings.\nPlease try again.",
|
||||
)
|
||||
msg_box.exec()
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving auto-update settings: {e}")
|
||||
msg_box = self._create_styled_message_box(QMessageBox.Icon.Critical, "Error", f"❌ Error saving settings: {str(e)}")
|
||||
msg_box.exec()
|
||||
@@ -0,0 +1,649 @@
|
||||
"""
|
||||
Update-related dialogs and threads for YTSage application.
|
||||
Contains dialogs and background threads for checking and performing yt-dlp updates.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from packaging import version
|
||||
from PySide6.QtCore import Qt, QThread, QTimer, Signal
|
||||
from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QProgressBar, QPushButton, QVBoxLayout
|
||||
|
||||
from src.core.ytsage_logging import logger
|
||||
from src.core.ytsage_utils import get_ytdlp_version, load_config, save_config
|
||||
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
||||
from src.utils.ytsage_constants import OS_NAME, SUBPROCESS_CREATIONFLAGS, YTDLP_APP_BIN_PATH, YTDLP_DOWNLOAD_URL
|
||||
|
||||
try:
|
||||
import yt_dlp
|
||||
|
||||
YT_DLP_AVAILABLE = True
|
||||
except ImportError:
|
||||
YT_DLP_AVAILABLE = False
|
||||
|
||||
|
||||
class VersionCheckThread(QThread):
|
||||
finished = Signal(str, str, str) # current_version, latest_version, error_message
|
||||
|
||||
def run(self) -> None:
|
||||
current_version = ""
|
||||
latest_version = ""
|
||||
error_message = ""
|
||||
|
||||
try:
|
||||
# Get the yt-dlp executable path
|
||||
yt_dlp_path = get_yt_dlp_path()
|
||||
|
||||
# Get current version with timeout
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[yt_dlp_path, "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30, # 30 second timeout
|
||||
creationflags=SUBPROCESS_CREATIONFLAGS,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
current_version = result.stdout.strip()
|
||||
else: # Try fallback if command failed
|
||||
if YT_DLP_AVAILABLE:
|
||||
current_version = yt_dlp.version.__version__ # type: ignore[reportAttributeAccessIssue]
|
||||
else:
|
||||
error_message = "yt-dlp not available."
|
||||
self.finished.emit(current_version, latest_version, error_message)
|
||||
return
|
||||
except subprocess.TimeoutExpired:
|
||||
# Try fallback if timeout
|
||||
if YT_DLP_AVAILABLE:
|
||||
current_version = yt_dlp.version.__version__ # type: ignore[reportAttributeAccessIssue]
|
||||
else:
|
||||
error_message = "yt-dlp version check timed out and package not found."
|
||||
self.finished.emit(current_version, latest_version, error_message)
|
||||
return
|
||||
except Exception:
|
||||
# Fallback to importing yt_dlp package directly if subprocess fails
|
||||
if YT_DLP_AVAILABLE:
|
||||
current_version = yt_dlp.version.__version__ # type: ignore[reportAttributeAccessIssue]
|
||||
else:
|
||||
error_message = "yt-dlp not found or accessible."
|
||||
self.finished.emit(current_version, latest_version, error_message)
|
||||
return
|
||||
|
||||
# Get latest version from PyPI
|
||||
response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10)
|
||||
response.raise_for_status()
|
||||
latest_version = response.json()["info"]["version"]
|
||||
|
||||
# Clean up version strings
|
||||
current_version = current_version.replace("_", ".")
|
||||
latest_version = latest_version.replace("_", ".")
|
||||
|
||||
except requests.RequestException as e:
|
||||
error_message = f"Network error checking PyPI: {e}"
|
||||
except Exception as e:
|
||||
error_message = f"Error checking version: {e}"
|
||||
|
||||
self.finished.emit(current_version, latest_version, error_message)
|
||||
|
||||
|
||||
class UpdateThread(QThread):
|
||||
update_status = Signal(str) # For status messages
|
||||
update_progress = Signal(int) # For progress percentage (0-100)
|
||||
update_finished = Signal(bool, str) # success (bool), message/error (str)
|
||||
|
||||
def run(self) -> None:
|
||||
error_message = ""
|
||||
success = False
|
||||
try:
|
||||
self.update_status.emit("🔍 Checking current installation...")
|
||||
self.update_progress.emit(10)
|
||||
|
||||
# Get the yt-dlp path
|
||||
try:
|
||||
yt_dlp_path = get_yt_dlp_path()
|
||||
self.update_status.emit(f"📍 Found yt-dlp at: {yt_dlp_path}")
|
||||
except Exception as e:
|
||||
self.update_status.emit(f"❌ Error getting yt-dlp path: {e}")
|
||||
self.update_finished.emit(False, f"❌ Error getting yt-dlp path: {e}")
|
||||
return
|
||||
|
||||
# Extra logic moved to src\utils\ytsage_constants.py
|
||||
|
||||
self.update_progress.emit(20)
|
||||
|
||||
# Extra logic moved to src\utils\ytsage_constants.py
|
||||
is_app_managed = yt_dlp_path.samefile(YTDLP_APP_BIN_PATH)
|
||||
|
||||
if is_app_managed:
|
||||
self.update_status.emit("📦 Updating app-managed yt-dlp binary...")
|
||||
success = self._update_binary(yt_dlp_path)
|
||||
else:
|
||||
self.update_status.emit("🐍 Updating system yt-dlp via pip...")
|
||||
success = self._update_via_pip()
|
||||
|
||||
if success:
|
||||
self.update_progress.emit(100)
|
||||
error_message = "✅ yt-dlp has been successfully updated!"
|
||||
else:
|
||||
error_message = "❌ Failed to update yt-dlp. Please try again or check your internet connection."
|
||||
|
||||
except requests.RequestException as e:
|
||||
error_message = f"❌ Network error during update: {str(e)}"
|
||||
self.update_status.emit(error_message)
|
||||
success = False
|
||||
except Exception as e:
|
||||
error_message = f"❌ Update failed: {str(e)}"
|
||||
self.update_status.emit(error_message)
|
||||
success = False
|
||||
|
||||
self.update_finished.emit(success, error_message)
|
||||
|
||||
def _update_binary(self, yt_dlp_path: Path) -> bool:
|
||||
"""Update yt-dlp binary using its built-in updater (same logic as AutoUpdateThread)."""
|
||||
try:
|
||||
logger.info("UpdateThread: Checking for yt-dlp updates...")
|
||||
|
||||
result = subprocess.run(
|
||||
[yt_dlp_path, "-U"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
creationflags=SUBPROCESS_CREATIONFLAGS,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
# Make executable on Unix systems
|
||||
if OS_NAME != "Windows":
|
||||
os.chmod(yt_dlp_path, 0o755)
|
||||
|
||||
logger.info("UpdateThread: yt-dlp update completed successfully.")
|
||||
if result.stdout:
|
||||
logger.debug(f"yt-dlp output: {result.stdout.strip()}")
|
||||
self.update_status.emit("✅ Binary successfully updated!")
|
||||
self.update_progress.emit(95)
|
||||
return True
|
||||
else:
|
||||
logger.error(f"UpdateThread: yt-dlp update failed. {result.stderr.strip()}")
|
||||
self.update_status.emit(f"❌ yt-dlp update failed: {result.stderr.strip()}")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("UpdateThread: yt-dlp update timed out.")
|
||||
self.update_status.emit("❌ yt-dlp update timed out.")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"UpdateThread: Unexpected error during update: {e}", exc_info=True)
|
||||
self.update_status.emit(f"❌ Unexpected error during update: {e}")
|
||||
return False
|
||||
|
||||
def _update_via_pip(self) -> bool:
|
||||
"""Update yt-dlp via pip."""
|
||||
try:
|
||||
import pkg_resources
|
||||
|
||||
self.update_status.emit("🔍 Checking current pip installation...")
|
||||
self.update_progress.emit(30)
|
||||
|
||||
# Get current version
|
||||
try:
|
||||
current_version = pkg_resources.get_distribution("yt-dlp").version
|
||||
self.update_status.emit(f"📋 Current version: {current_version}")
|
||||
except pkg_resources.DistributionNotFound:
|
||||
self.update_status.emit("⚠️ yt-dlp not found via pip, attempting installation...")
|
||||
current_version = "0.0.0"
|
||||
|
||||
self.update_progress.emit(40)
|
||||
|
||||
# Get the latest version from PyPI
|
||||
self.update_status.emit("🌐 Checking for latest version...")
|
||||
response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10)
|
||||
|
||||
if response.status_code != 200:
|
||||
self.update_status.emit("❌ Failed to check for updates")
|
||||
return False
|
||||
|
||||
data = response.json()
|
||||
latest_version = data["info"]["version"]
|
||||
self.update_status.emit(f"🆕 Latest version: {latest_version}")
|
||||
self.update_progress.emit(50)
|
||||
|
||||
# Compare versions
|
||||
if version.parse(latest_version) > version.parse(current_version):
|
||||
self.update_status.emit(f"⬆️ Updating from {current_version} to {latest_version}...")
|
||||
self.update_progress.emit(60)
|
||||
|
||||
try:
|
||||
# Run pip update with timeout
|
||||
self.update_status.emit("📦 Running pip install --upgrade...")
|
||||
update_result = subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=300, # 5 minute timeout for pip install
|
||||
creationflags=SUBPROCESS_CREATIONFLAGS,
|
||||
)
|
||||
|
||||
self.update_progress.emit(85)
|
||||
|
||||
if update_result.returncode == 0:
|
||||
self.update_status.emit("✅ Pip update completed successfully!")
|
||||
self.update_progress.emit(95)
|
||||
return True
|
||||
else:
|
||||
self.update_status.emit(f"❌ Pip update failed: {update_result.stderr}")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
self.update_status.emit("❌ Pip update timed out after 5 minutes")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.update_status.emit(f"❌ Error during pip update: {e}")
|
||||
return False
|
||||
else:
|
||||
self.update_status.emit("✅ yt-dlp is already up to date!")
|
||||
self.update_progress.emit(95)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.update_status.emit(f"❌ Pip update failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
class YTDLPUpdateDialog(QDialog):
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Update yt-dlp")
|
||||
self.setMinimumWidth(450)
|
||||
self.setMinimumHeight(200)
|
||||
self._closing = False # Flag to track if dialog is closing
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Status label
|
||||
self.status_label = QLabel("Checking for updates...")
|
||||
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.status_label.setWordWrap(True)
|
||||
self.status_label.setMinimumHeight(60)
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
# Progress bar
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.hide() # Hide initially
|
||||
layout.addWidget(self.progress_bar)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
self.update_btn = QPushButton("Update")
|
||||
self.update_btn.clicked.connect(self.perform_update)
|
||||
self.update_btn.setEnabled(False)
|
||||
|
||||
self.close_btn = QPushButton("Close")
|
||||
self.close_btn.clicked.connect(self.close)
|
||||
|
||||
button_layout.addWidget(self.update_btn)
|
||||
button_layout.addWidget(self.close_btn)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Style
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
QDialog {
|
||||
background-color: #15181b;
|
||||
}
|
||||
QLabel {
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
QPushButton {
|
||||
padding: 8px 15px;
|
||||
background-color: #c90000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
min-width: 100px;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #666666;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #a50000;
|
||||
}
|
||||
QProgressBar {
|
||||
border: 2px solid #1d1e22;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
background-color: #1d1e22;
|
||||
height: 30px;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0,
|
||||
stop: 0 #e60000, stop: 0.5 #ff3333, stop: 1 #c90000);
|
||||
border-radius: 4px;
|
||||
margin: 1px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
# Start version check in background
|
||||
self.check_version()
|
||||
|
||||
def check_version(self) -> None:
|
||||
self.status_label.setText("Checking for updates...")
|
||||
self.update_btn.setEnabled(False)
|
||||
self.version_check_thread = VersionCheckThread()
|
||||
self.version_check_thread.finished.connect(self.on_version_check_finished)
|
||||
self.version_check_thread.start()
|
||||
|
||||
def on_version_check_finished(self, current_version, latest_version, error_message) -> None:
|
||||
# Check if dialog is closing to avoid unnecessary updates
|
||||
if hasattr(self, "_closing") and self._closing:
|
||||
return
|
||||
|
||||
if error_message:
|
||||
self.status_label.setText(error_message)
|
||||
self.update_btn.setEnabled(False)
|
||||
return
|
||||
|
||||
if not current_version or not latest_version:
|
||||
self.status_label.setText("Could not determine versions.")
|
||||
self.update_btn.setEnabled(False)
|
||||
return
|
||||
|
||||
try:
|
||||
# Compare versions
|
||||
current_ver = version.parse(current_version)
|
||||
latest_ver = version.parse(latest_version)
|
||||
|
||||
if current_ver < latest_ver:
|
||||
self.status_label.setText(
|
||||
f"Update available!\nCurrent version: {current_version}\nLatest version: {latest_version}"
|
||||
)
|
||||
self.update_btn.setEnabled(True)
|
||||
else:
|
||||
self.status_label.setText(f"yt-dlp is up to date (version {current_version})")
|
||||
self.update_btn.setEnabled(False)
|
||||
except version.InvalidVersion:
|
||||
# If version parsing fails, do a simple string comparison
|
||||
if current_version != latest_version:
|
||||
self.status_label.setText(
|
||||
f"Update available! (Comparison failed)\nCurrent: {current_version}\nLatest: {latest_version}"
|
||||
)
|
||||
self.update_btn.setEnabled(True)
|
||||
else:
|
||||
self.status_label.setText(f"yt-dlp is up to date (version {current_version})")
|
||||
self.update_btn.setEnabled(False)
|
||||
except Exception as e:
|
||||
self.status_label.setText(f"Error comparing versions: {e}")
|
||||
self.update_btn.setEnabled(False)
|
||||
|
||||
def perform_update(self) -> None:
|
||||
# Immediate visual feedback
|
||||
self.update_btn.setEnabled(False)
|
||||
self.close_btn.setEnabled(False)
|
||||
self.update_btn.setText("Updating...")
|
||||
self.status_label.setText("🚀 Initializing update process...")
|
||||
|
||||
# Show progress bar immediately
|
||||
self.progress_bar.setRange(0, 100)
|
||||
self.progress_bar.setValue(0)
|
||||
self.progress_bar.show()
|
||||
|
||||
# Start the update thread
|
||||
self._start_update_thread()
|
||||
|
||||
def _start_update_thread(self) -> None:
|
||||
"""Start the actual update thread."""
|
||||
# Create and start the update thread
|
||||
self.update_thread = UpdateThread()
|
||||
self.update_thread.update_status.connect(self.on_update_status)
|
||||
self.update_thread.update_progress.connect(self.on_update_progress)
|
||||
self.update_thread.update_finished.connect(self.on_update_finished)
|
||||
self.update_thread.start()
|
||||
|
||||
def on_update_status(self, message) -> None:
|
||||
"""Slot to receive status messages from UpdateThread."""
|
||||
if not (hasattr(self, "_closing") and self._closing):
|
||||
self.status_label.setText(message)
|
||||
|
||||
def on_update_progress(self, progress) -> None:
|
||||
"""Slot to receive progress updates from UpdateThread."""
|
||||
if not (hasattr(self, "_closing") and self._closing):
|
||||
self.progress_bar.setValue(progress)
|
||||
|
||||
def on_update_finished(self, success, message) -> None:
|
||||
"""Slot called when the UpdateThread finishes."""
|
||||
# Check if dialog is closing to avoid unnecessary updates
|
||||
if hasattr(self, "_closing") and self._closing:
|
||||
return
|
||||
|
||||
self.progress_bar.setValue(100)
|
||||
self.status_label.setText(message)
|
||||
self.close_btn.setEnabled(True)
|
||||
self.update_btn.setText("Update") # Reset button text
|
||||
|
||||
if success:
|
||||
# Show success briefly then auto-check version
|
||||
QTimer.singleShot(2000, self.check_version) # Wait 2 seconds then refresh
|
||||
else:
|
||||
# Re-enable update button on failure after a short delay
|
||||
QTimer.singleShot(
|
||||
3000,
|
||||
lambda: (self.update_btn.setEnabled(True) if not (hasattr(self, "_closing") and self._closing) else None),
|
||||
)
|
||||
|
||||
def closeEvent(self, event) -> None:
|
||||
"""Ensure threads are terminated if the dialog is closed prematurely."""
|
||||
# Set a flag to indicate dialog is closing
|
||||
self._closing = True
|
||||
|
||||
if hasattr(self, "version_check_thread") and self.version_check_thread.isRunning():
|
||||
self.version_check_thread.quit()
|
||||
if not self.version_check_thread.wait(3000): # Wait up to 3 seconds
|
||||
self.version_check_thread.terminate()
|
||||
|
||||
if hasattr(self, "update_thread") and self.update_thread.isRunning():
|
||||
self.update_thread.quit()
|
||||
if not self.update_thread.wait(5000): # Wait up to 5 seconds for update to finish
|
||||
self.update_thread.terminate()
|
||||
|
||||
super().closeEvent(event)
|
||||
|
||||
|
||||
class AutoUpdateThread(QThread):
|
||||
"""Thread for performing automatic background updates without UI feedback."""
|
||||
|
||||
update_finished = Signal(bool, str) # success (bool), message (str)
|
||||
|
||||
def run(self) -> None:
|
||||
"""Perform automatic yt-dlp update check and update if needed."""
|
||||
try:
|
||||
logger.info("AutoUpdateThread: Performing automatic yt-dlp update check...")
|
||||
|
||||
# Get current version
|
||||
current_version = get_ytdlp_version()
|
||||
if "Error" in current_version:
|
||||
logger.warning("AutoUpdateThread: Could not determine current yt-dlp version, skipping auto-update")
|
||||
self.update_finished.emit(False, "Could not determine current yt-dlp version")
|
||||
return
|
||||
|
||||
# Get latest version from PyPI
|
||||
try:
|
||||
response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10)
|
||||
response.raise_for_status()
|
||||
latest_version = response.json()["info"]["version"]
|
||||
|
||||
# Clean up version strings
|
||||
current_version = current_version.replace("_", ".")
|
||||
latest_version = latest_version.replace("_", ".")
|
||||
|
||||
logger.info(f"AutoUpdateThread: Current yt-dlp version: {current_version}")
|
||||
logger.info(f"AutoUpdateThread: Latest yt-dlp version: {latest_version}")
|
||||
|
||||
# Compare versions
|
||||
if version.parse(latest_version) > version.parse(current_version):
|
||||
logger.info(f"AutoUpdateThread: Auto-updating yt-dlp from {current_version} to {latest_version}...")
|
||||
|
||||
# Perform the update
|
||||
success = self._perform_update()
|
||||
|
||||
if success:
|
||||
logger.info("AutoUpdateThread: Auto-update completed successfully!")
|
||||
# Update the last check timestamp
|
||||
config = load_config()
|
||||
config["last_update_check"] = time.time()
|
||||
save_config(config)
|
||||
self.update_finished.emit(
|
||||
True,
|
||||
f"Successfully updated yt-dlp from {current_version} to {latest_version}",
|
||||
)
|
||||
else:
|
||||
logger.warning("AutoUpdateThread: Auto-update failed")
|
||||
self.update_finished.emit(False, "Auto-update failed")
|
||||
else:
|
||||
logger.info("AutoUpdateThread: yt-dlp is already up to date")
|
||||
# Still update the timestamp even if no update was needed
|
||||
config = load_config()
|
||||
config["last_update_check"] = time.time()
|
||||
save_config(config)
|
||||
self.update_finished.emit(
|
||||
True,
|
||||
f"yt-dlp is already up to date (version {current_version})",
|
||||
)
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.warning(f"AutoUpdateThread: Network error during auto-update check: {e}")
|
||||
self.update_finished.emit(False, f"Network error: {e}")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"AutoUpdateThread: Error during auto-update check: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
self.update_finished.emit(False, f"Update check error: {e}")
|
||||
|
||||
except Exception as e:
|
||||
logger.critical(f"AutoUpdateThread: Critical error in auto-update: {e}", exc_info=True)
|
||||
self.update_finished.emit(False, f"Critical error: {e}")
|
||||
|
||||
def _perform_update(self) -> bool:
|
||||
"""Perform the actual update using similar logic to UpdateThread but without UI feedback."""
|
||||
try:
|
||||
# Get the yt-dlp path
|
||||
yt_dlp_path = get_yt_dlp_path()
|
||||
|
||||
# Extra logic moved to src\utils\ytsage_constants.py
|
||||
|
||||
is_app_managed = yt_dlp_path.samefile(YTDLP_APP_BIN_PATH)
|
||||
|
||||
if is_app_managed:
|
||||
logger.info("AutoUpdateThread: Updating app-managed yt-dlp binary...")
|
||||
return self._update_binary(yt_dlp_path)
|
||||
else:
|
||||
logger.info("AutoUpdateThread: Updating system yt-dlp via pip...")
|
||||
return self._update_via_pip()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"AutoUpdateThread: Error in _perform_update: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def _update_binary(self, yt_dlp_path: Path) -> bool:
|
||||
"""Update yt-dlp binary using its built-in updater."""
|
||||
try:
|
||||
logger.info("AutoUpdateThread: Checking for yt-dlp updates...")
|
||||
|
||||
result = subprocess.run(
|
||||
[yt_dlp_path, "-U"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
creationflags=SUBPROCESS_CREATIONFLAGS,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
# Make executable on Unix systems
|
||||
if OS_NAME != "Windows":
|
||||
os.chmod(yt_dlp_path, 0o755)
|
||||
|
||||
logger.info("AutoUpdateThread: yt-dlp update completed successfully.")
|
||||
if result.stdout:
|
||||
logger.debug(f"yt-dlp output: {result.stdout.strip()}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"AutoUpdateThread: yt-dlp update failed. {result.stderr.strip()}")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("AutoUpdateThread: yt-dlp update timed out.")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"AutoUpdateThread: Unexpected error during update: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
def _update_via_pip(self) -> bool:
|
||||
"""Update yt-dlp via pip (silent version)."""
|
||||
try:
|
||||
import pkg_resources
|
||||
|
||||
logger.info("AutoUpdateThread: Checking current pip installation...")
|
||||
|
||||
# Get current version
|
||||
try:
|
||||
current_version = pkg_resources.get_distribution("yt-dlp").version
|
||||
logger.info(f"AutoUpdateThread: Current version: {current_version}")
|
||||
except pkg_resources.DistributionNotFound:
|
||||
logger.warning("AutoUpdateThread: yt-dlp not found via pip, attempting installation...")
|
||||
current_version = "0.0.0"
|
||||
|
||||
# Get the latest version from PyPI
|
||||
logger.info("AutoUpdateThread: Checking for latest version...")
|
||||
response = requests.get("https://pypi.org/pypi/yt-dlp/json", timeout=10)
|
||||
|
||||
if response.status_code != 200:
|
||||
logger.error("AutoUpdateThread: Failed to check for updates")
|
||||
return False
|
||||
|
||||
data = response.json()
|
||||
latest_version = data["info"]["version"]
|
||||
logger.info(f"AutoUpdateThread: Latest version: {latest_version}")
|
||||
|
||||
# Compare versions
|
||||
if version.parse(latest_version) > version.parse(current_version):
|
||||
logger.info(f"AutoUpdateThread: Updating from {current_version} to {latest_version}...")
|
||||
|
||||
# Extra logic moved to src\utils\ytsage_constants.py
|
||||
|
||||
# Run pip update
|
||||
logger.info("AutoUpdateThread: Running pip install --upgrade...")
|
||||
update_result = subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
creationflags=SUBPROCESS_CREATIONFLAGS,
|
||||
)
|
||||
|
||||
if update_result.returncode == 0:
|
||||
logger.info("AutoUpdateThread: Pip update completed successfully!")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"AutoUpdateThread: Pip update failed: {update_result.stderr}")
|
||||
return False
|
||||
else:
|
||||
logger.info("AutoUpdateThread: yt-dlp is already up to date!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"AutoUpdateThread: Pip update failed: {e}", exc_info=True)
|
||||
return False
|
||||
Reference in New Issue
Block a user