e862c811c2
Replaces hardcoded version strings with the APP_VERSION constant in both the main window and About dialog. This ensures version consistency across the application and simplifies future version updates.
1783 lines
73 KiB
Python
1783 lines
73 KiB
Python
import json
|
|
import subprocess
|
|
import threading
|
|
import webbrowser
|
|
from pathlib import Path
|
|
|
|
import markdown
|
|
import pyglet
|
|
import requests
|
|
from packaging import version
|
|
from PySide6.QtCore import Q_ARG, QMetaObject, Qt, QTimer
|
|
from PySide6.QtGui import QIcon
|
|
from PySide6.QtWidgets import (
|
|
QApplication,
|
|
QButtonGroup,
|
|
QCheckBox,
|
|
QDialog,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QLineEdit,
|
|
QMainWindow,
|
|
QMessageBox,
|
|
QProgressBar,
|
|
QPushButton,
|
|
QStyle,
|
|
QTextEdit,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from src import __version__ as APP_VERSION
|
|
from src.core.ytsage_downloader import DownloadThread, SignalManager # Import downloader related classes
|
|
from src.core.ytsage_utils import check_ffmpeg, load_saved_path, parse_yt_dlp_error, save_path, should_check_for_auto_update, validate_video_url
|
|
from src.core.ytsage_yt_dlp import get_yt_dlp_path, setup_ytdlp # Import the new yt-dlp functions
|
|
from src.gui.ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__init__.py
|
|
AboutDialog,
|
|
AutoUpdateThread,
|
|
CustomOptionsDialog,
|
|
DownloadSettingsDialog,
|
|
FFmpegCheckDialog,
|
|
PlaylistSelectionDialog,
|
|
TimeRangeDialog,
|
|
YTDLPUpdateDialog,
|
|
)
|
|
from src.gui.ytsage_gui_format_table import FormatTableMixin
|
|
from src.gui.ytsage_gui_video_info import VideoInfoMixin
|
|
from src.utils.ytsage_constants import ICON_PATH, SOUND_PATH, SUBPROCESS_CREATIONFLAGS
|
|
from src.utils.ytsage_logger import logger
|
|
from src.utils.ytsage_config_manager import ConfigManager
|
|
from src.utils.ytsage_localization import LocalizationManager, _
|
|
|
|
# Note: yt-dlp Python package removed - using binary-only approach
|
|
# DownloadError and ExtractorError definitions kept for compatibility
|
|
class DownloadError(Exception):
|
|
"""Compatibility class for yt-dlp DownloadError"""
|
|
pass
|
|
|
|
class ExtractorError(Exception):
|
|
"""Compatibility class for yt-dlp ExtractorError"""
|
|
pass
|
|
|
|
|
|
class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from mixins
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
|
|
# Initialize localization system
|
|
saved_language = ConfigManager.get("language") or "en"
|
|
LocalizationManager.initialize(saved_language)
|
|
|
|
# Check for FFmpeg before proceeding
|
|
if not check_ffmpeg():
|
|
self.show_ffmpeg_dialog()
|
|
|
|
# Check for yt-dlp in our app's bin directory or system PATH
|
|
ytdlp_path = get_yt_dlp_path()
|
|
if ytdlp_path == "yt-dlp": # Not found in app dir or PATH
|
|
self.show_ytdlp_setup_dialog()
|
|
else:
|
|
logger.info(f"Using yt-dlp from: {ytdlp_path}")
|
|
|
|
self.version = APP_VERSION
|
|
self.check_for_updates()
|
|
|
|
# Check for auto-updates if enabled
|
|
self.check_auto_update_ytdlp()
|
|
|
|
load_saved_path(self)
|
|
# Load custom icon
|
|
if ICON_PATH.exists():
|
|
self.setWindowIcon(QIcon(str(ICON_PATH)))
|
|
else:
|
|
logger.warning(f"Icon file not found at {ICON_PATH}. Using default icon.")
|
|
self.setWindowIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowDown)) # Fallback
|
|
self.signals = SignalManager()
|
|
self.download_paused = False
|
|
self.current_download = None
|
|
self.download_cancelled = False
|
|
self.save_thumbnail = False # Initialize thumbnail state
|
|
self.thumbnail_url = None # Add this to store thumbnail URL
|
|
self.all_formats = [] # Initialize all_formats
|
|
self.available_subtitles = {}
|
|
self.available_automatic_subtitles = {}
|
|
self.is_playlist = False
|
|
self.playlist_info = None
|
|
self.video_info = None
|
|
self.playlist_entries = [] # Initialize playlist entries
|
|
self.selected_playlist_items = None # Initialize selection string
|
|
self.save_description = False # Initialize description state
|
|
self.embed_chapters = False # Initialize chapters state
|
|
self.subtitle_filter = ""
|
|
self.thumbnail_image = None
|
|
self.video_url = ""
|
|
self.selected_subtitles = [] # Initialize selected subtitles list
|
|
# Initialize cookie settings - ensure they start clean
|
|
self.cookie_file_path = None
|
|
self.browser_cookies_option = None
|
|
# Initialize proxy settings from config
|
|
self.proxy_url = ConfigManager.get("proxy_url")
|
|
self.geo_proxy_url = ConfigManager.get("geo_proxy_url")
|
|
self.speed_limit_value = None # Store speed limit value
|
|
self.speed_limit_unit_index = 0 # Store speed limit unit index (0: KB/s, 1: MB/s)
|
|
self.download_section = None
|
|
self.force_keyframes = False
|
|
|
|
self.init_ui()
|
|
self.setStyleSheet(
|
|
"""
|
|
QMainWindow {
|
|
background-color: #15181b;
|
|
}
|
|
QWidget {
|
|
background-color: #15181b;
|
|
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;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #800000;
|
|
}
|
|
QTableWidget {
|
|
border: 2px solid #1b2021;
|
|
border-radius: 4px;
|
|
background-color: #1b2021;
|
|
gridline-color: #1b2021;
|
|
}
|
|
QHeaderView::section {
|
|
background-color: #15181b;
|
|
padding: 5px;
|
|
border: 1px solid #1b2021;
|
|
color: #ffffff;
|
|
}
|
|
QProgressBar {
|
|
border: 2px solid #1b2021;
|
|
border-radius: 4px;
|
|
text-align: center;
|
|
color: white;
|
|
}
|
|
QProgressBar::chunk {
|
|
background-color: #c90000;
|
|
border-radius: 2px;
|
|
}
|
|
QLabel {
|
|
color: #ffffff;
|
|
}
|
|
/* Style for filter buttons */
|
|
QPushButton.filter-btn {
|
|
background-color: #1b2021;
|
|
padding: 5px 10px;
|
|
margin: 0 5px;
|
|
}
|
|
QPushButton.filter-btn:checked {
|
|
background-color: #c90000;
|
|
}
|
|
QPushButton.filter-btn:hover {
|
|
background-color: #444444;
|
|
}
|
|
QPushButton.filter-btn:checked:hover {
|
|
background-color: #a50000;
|
|
}
|
|
/* Modern Scrollbar Styling */
|
|
QScrollBar:vertical {
|
|
border: none;
|
|
background: #15181b;
|
|
width: 14px;
|
|
margin: 15px 0 15px 0;
|
|
border-radius: 7px;
|
|
}
|
|
QScrollBar::handle:vertical {
|
|
background: #404040;
|
|
min-height: 30px;
|
|
border-radius: 7px;
|
|
}
|
|
QScrollBar::handle:vertical:hover {
|
|
background: #505050;
|
|
}
|
|
QScrollBar::sub-line:vertical {
|
|
border: none;
|
|
background: #15181b;
|
|
height: 15px;
|
|
border-top-left-radius: 7px;
|
|
border-top-right-radius: 7px;
|
|
subcontrol-position: top;
|
|
subcontrol-origin: margin;
|
|
}
|
|
QScrollBar::add-line:vertical {
|
|
border: none;
|
|
background: #15181b;
|
|
height: 15px;
|
|
border-bottom-left-radius: 7px;
|
|
border-bottom-right-radius: 7px;
|
|
subcontrol-position: bottom;
|
|
subcontrol-origin: margin;
|
|
}
|
|
QScrollBar::sub-line:vertical:hover,
|
|
QScrollBar::add-line:vertical:hover {
|
|
background: #404040;
|
|
}
|
|
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {
|
|
background: none;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
|
|
background: none;
|
|
}
|
|
/* Horizontal Scrollbar */
|
|
QScrollBar:horizontal {
|
|
border: none;
|
|
background: #15181b;
|
|
height: 14px;
|
|
margin: 0 15px 0 15px;
|
|
border-radius: 7px;
|
|
}
|
|
QScrollBar::handle:horizontal {
|
|
background: #404040;
|
|
min-width: 30px;
|
|
border-radius: 7px;
|
|
}
|
|
QScrollBar::handle:horizontal:hover {
|
|
background: #505050;
|
|
}
|
|
QScrollBar::sub-line:horizontal {
|
|
border: none;
|
|
background: #15181b;
|
|
width: 15px;
|
|
border-top-left-radius: 7px;
|
|
border-bottom-left-radius: 7px;
|
|
subcontrol-position: left;
|
|
subcontrol-origin: margin;
|
|
}
|
|
QScrollBar::add-line:horizontal {
|
|
border: none;
|
|
background: #15181b;
|
|
width: 15px;
|
|
border-top-right-radius: 7px;
|
|
border-bottom-right-radius: 7px;
|
|
subcontrol-position: right;
|
|
subcontrol-origin: margin;
|
|
}
|
|
QScrollBar::sub-line:horizontal:hover,
|
|
QScrollBar::add-line:horizontal:hover {
|
|
background: #404040;
|
|
}
|
|
QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal {
|
|
background: none;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
|
|
background: none;
|
|
}
|
|
"""
|
|
)
|
|
self.signals.update_progress.connect(self.update_progress_bar)
|
|
|
|
# After adding format buttons
|
|
self.video_button.clicked.connect(self.filter_formats) # Connect video button
|
|
self.audio_button.clicked.connect(self.filter_formats) # Connect audio button
|
|
|
|
# Add connections to handle video/audio mode-specific controls
|
|
self.video_button.clicked.connect(self.handle_mode_change)
|
|
self.audio_button.clicked.connect(self.handle_mode_change)
|
|
|
|
# Initialize UI state based on current mode
|
|
self.handle_mode_change()
|
|
|
|
# Init_sound method is removed, serve no purpose.
|
|
|
|
def play_notification_sound(self) -> None:
|
|
"""Play notification sound asynchronously (non-blocking)."""
|
|
try:
|
|
# Check if the notification sound file exists
|
|
if not SOUND_PATH.exists():
|
|
logger.warning(f"Notification sound file not found at: {SOUND_PATH}")
|
|
return
|
|
|
|
# Play the sound using pyglet
|
|
# no need for the thread, as .play() is async
|
|
sound = pyglet.media.load(str(SOUND_PATH), streaming=False)
|
|
sound.play()
|
|
logger.debug("Notification sound played")
|
|
except Exception as e:
|
|
logger.exception(f"Error playing notification sound: {e}")
|
|
|
|
def init_ui(self) -> None:
|
|
self.setWindowTitle(f"{_('app.title')} {_('app.version', version=self.version)}")
|
|
self.setMinimumSize(900, 750)
|
|
|
|
# Main widget and layout
|
|
main_widget = QWidget()
|
|
self.setCentralWidget(main_widget)
|
|
layout = QVBoxLayout(main_widget)
|
|
layout.setSpacing(8)
|
|
layout.setContentsMargins(20, 20, 20, 20)
|
|
|
|
# URL input section
|
|
url_layout = QHBoxLayout()
|
|
self.url_input = QLineEdit()
|
|
self.url_input.setPlaceholderText(_("main_ui.url_placeholder"))
|
|
self.url_input.returnPressed.connect(self.analyze_url) # Analyze on Enter key
|
|
|
|
self.analyze_button = QPushButton(_("buttons.analyze"))
|
|
self.analyze_button.clicked.connect(self.analyze_url)
|
|
|
|
self.paste_button = QPushButton(_("buttons.paste_url"))
|
|
self.paste_button.clicked.connect(self.paste_url)
|
|
|
|
url_layout.addWidget(self.url_input, 1)
|
|
url_layout.addWidget(self.paste_button)
|
|
url_layout.addWidget(self.analyze_button)
|
|
|
|
layout.addLayout(url_layout)
|
|
|
|
# Video info container
|
|
video_info_container = QWidget()
|
|
video_info_layout = QVBoxLayout(video_info_container)
|
|
video_info_layout.setSpacing(5)
|
|
video_info_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# Add media info layout (Thumbnail | Video Details)
|
|
media_info_layout = self.setup_video_info_section()
|
|
video_info_layout.addLayout(media_info_layout)
|
|
|
|
# Add video info container to main layout
|
|
layout.addWidget(video_info_container)
|
|
|
|
# --- Add Playlist Info Section Directly to Main Layout ---
|
|
# Add playlist info label (initially hidden)
|
|
self.playlist_info_label = self.setup_playlist_info_section()
|
|
layout.addWidget(self.playlist_info_label)
|
|
|
|
# Add playlist selection BUTTON (initially hidden) - REPLACED QLineEdit
|
|
self.playlist_select_btn = QPushButton(_("buttons.select_videos"))
|
|
self.playlist_select_btn.clicked.connect(self.open_playlist_selection_dialog)
|
|
self.playlist_select_btn.setVisible(False)
|
|
self.playlist_select_btn.setStyleSheet(
|
|
"""
|
|
QPushButton {
|
|
padding: 6px 12px;
|
|
background-color: #1d1e22;
|
|
border: 1px solid #c90000;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-weight: normal;
|
|
text-align: left;
|
|
padding-left: 10px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #2a2d36;
|
|
border-color: #a50000;
|
|
}
|
|
"""
|
|
)
|
|
layout.addWidget(self.playlist_select_btn)
|
|
# --- End Playlist Info Section ---
|
|
|
|
# Format controls section with minimal spacing
|
|
layout.addSpacing(5)
|
|
|
|
# Format selection layout (horizontal)
|
|
self.format_layout = QHBoxLayout()
|
|
|
|
# Show formats label
|
|
self.show_formats_label = QLabel(_("formats.show_formats"))
|
|
self.show_formats_label.setStyleSheet("color: white;")
|
|
self.format_layout.addWidget(self.show_formats_label)
|
|
|
|
# Format buttons group
|
|
self.format_buttons = QButtonGroup(self)
|
|
self.format_buttons.setExclusive(True)
|
|
|
|
# Video button
|
|
self.video_button = QPushButton(_("buttons.video"))
|
|
self.video_button.setCheckable(True)
|
|
self.video_button.setChecked(True) # Set video as default
|
|
self.video_button.setStyleSheet(
|
|
"""
|
|
QPushButton {
|
|
padding: 8px 15px;
|
|
background-color: #1d1e22;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:checked {
|
|
background-color: #c90000;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #2a2d36;
|
|
}
|
|
QPushButton:checked:hover {
|
|
background-color: #a50000;
|
|
}
|
|
"""
|
|
)
|
|
self.format_buttons.addButton(self.video_button)
|
|
self.format_layout.addWidget(self.video_button)
|
|
|
|
# Audio button
|
|
self.audio_button = QPushButton(_("buttons.audio_only"))
|
|
self.audio_button.setCheckable(True)
|
|
self.audio_button.setStyleSheet(
|
|
"""
|
|
QPushButton {
|
|
padding: 8px 15px;
|
|
background-color: #1d1e22;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:checked {
|
|
background-color: #c90000;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #2a2d36;
|
|
}
|
|
QPushButton:checked:hover {
|
|
background-color: #a50000;
|
|
}
|
|
"""
|
|
)
|
|
self.format_buttons.addButton(self.audio_button)
|
|
self.format_layout.addWidget(self.audio_button)
|
|
|
|
# Add Merge Subtitles checkbox (Moved here)
|
|
self.merge_subs_checkbox = QCheckBox(_("main_ui.merge_subtitles"))
|
|
self.merge_subs_checkbox.setStyleSheet(
|
|
"""
|
|
QCheckBox {
|
|
color: #ffffff;
|
|
padding: 5px;
|
|
margin-left: 20px; /* Consistent margin */
|
|
}
|
|
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;
|
|
}
|
|
/* Add disabled state styling if needed */
|
|
QCheckBox:disabled { color: #888888; }
|
|
QCheckBox::indicator:disabled { border-color: #555555; background: #444444; }
|
|
"""
|
|
)
|
|
# Initially disable it, will be enabled if subtitles are selected later
|
|
self.merge_subs_checkbox.setEnabled(False)
|
|
self.format_layout.addWidget(self.merge_subs_checkbox)
|
|
|
|
# Add Save Thumbnail Checkbox (Moved here)
|
|
self.save_thumbnail_checkbox = QCheckBox(_("main_ui.save_thumbnail"))
|
|
self.save_thumbnail_checkbox.setChecked(False)
|
|
self.save_thumbnail_checkbox.stateChanged.connect(self.toggle_save_thumbnail)
|
|
self.save_thumbnail_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; }
|
|
QCheckBox:disabled { color: #888888; }
|
|
QCheckBox::indicator:disabled { border-color: #555555; background: #444444; }
|
|
"""
|
|
)
|
|
self.format_layout.addWidget(self.save_thumbnail_checkbox)
|
|
|
|
# Add Save Description Checkbox (Moved here)
|
|
self.save_description_checkbox = QCheckBox(_("main_ui.save_description"))
|
|
self.save_description_checkbox.setChecked(False)
|
|
self.save_description_checkbox.stateChanged.connect(self.toggle_save_description)
|
|
self.save_description_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; }
|
|
QCheckBox:disabled { color: #888888; }
|
|
QCheckBox::indicator:disabled { border-color: #555555; background: #444444; }
|
|
"""
|
|
)
|
|
self.format_layout.addWidget(self.save_description_checkbox)
|
|
|
|
# Add Embed Chapters Checkbox
|
|
self.embed_chapters_checkbox = QCheckBox(_("main_ui.embed_chapters"))
|
|
self.embed_chapters_checkbox.setChecked(False)
|
|
self.embed_chapters_checkbox.stateChanged.connect(self.toggle_embed_chapters)
|
|
self.embed_chapters_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; }
|
|
QCheckBox:disabled { color: #888888; }
|
|
QCheckBox::indicator:disabled { border-color: #555555; background: #444444; }
|
|
"""
|
|
)
|
|
self.format_layout.addWidget(self.embed_chapters_checkbox)
|
|
|
|
self.format_layout.addStretch()
|
|
|
|
layout.addLayout(self.format_layout)
|
|
|
|
# Format table with stretch
|
|
format_table = self.setup_format_table()
|
|
layout.addWidget(format_table, stretch=1)
|
|
|
|
# Download section
|
|
download_layout = QHBoxLayout()
|
|
|
|
# Replace the two separate buttons with a single Custom Options button
|
|
self.custom_options_btn = QPushButton(_("buttons.custom_options"))
|
|
self.custom_options_btn.clicked.connect(self.show_custom_options)
|
|
|
|
self.about_btn = QPushButton(_("buttons.about"))
|
|
self.about_btn.clicked.connect(self.show_about_dialog)
|
|
|
|
# Add new Time Range button
|
|
self.time_range_btn = QPushButton(_("buttons.trim_video"))
|
|
self.time_range_btn.clicked.connect(self.show_time_range_dialog)
|
|
|
|
self.update_ytdlp_btn = QPushButton(_("buttons.update"))
|
|
self.update_ytdlp_btn.clicked.connect(self.update_ytdlp)
|
|
|
|
# --- Rename Path Button to Settings Button ---
|
|
self.settings_button = QPushButton(_("buttons.download_settings")) # Renamed button
|
|
self.settings_button.clicked.connect(self.show_download_settings_dialog) # Renamed method
|
|
self.settings_button.setToolTip(f"Current Path: {self.last_path}\nSpeed Limit: None") # Update initial tooltip
|
|
# --- End Settings Button ---
|
|
|
|
self.download_btn = QPushButton(_("buttons.download"))
|
|
self.download_btn.clicked.connect(self.start_download)
|
|
|
|
# Add pause and cancel buttons
|
|
self.pause_btn = QPushButton(_("buttons.pause"))
|
|
self.pause_btn.clicked.connect(self.toggle_pause)
|
|
self.pause_btn.setVisible(False) # Hidden initially
|
|
|
|
self.cancel_btn = QPushButton(_("buttons.cancel"))
|
|
self.cancel_btn.clicked.connect(self.cancel_download)
|
|
self.cancel_btn.setVisible(False) # Hidden initially
|
|
|
|
# Add all buttons to layout in the correct order
|
|
download_layout.addWidget(self.custom_options_btn)
|
|
download_layout.addWidget(self.about_btn)
|
|
download_layout.addWidget(self.time_range_btn) # New button position
|
|
download_layout.addWidget(self.update_ytdlp_btn)
|
|
download_layout.addWidget(self.settings_button)
|
|
download_layout.addWidget(self.download_btn)
|
|
download_layout.addWidget(self.pause_btn)
|
|
download_layout.addWidget(self.cancel_btn)
|
|
|
|
layout.addLayout(download_layout)
|
|
|
|
# Progress section with improved styling
|
|
progress_layout = QVBoxLayout()
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setStyleSheet(
|
|
"""
|
|
QProgressBar {
|
|
border: 2px solid #3d3d3d;
|
|
border-radius: 4px;
|
|
text-align: center;
|
|
color: white;
|
|
background-color: #363636;
|
|
height: 25px;
|
|
}
|
|
QProgressBar::chunk {
|
|
background-color: #ff0000;
|
|
border-radius: 2px;
|
|
}
|
|
"""
|
|
)
|
|
progress_layout.addWidget(self.progress_bar)
|
|
|
|
# Add download details label with improved styling
|
|
self.download_details_label = QLabel()
|
|
self.download_details_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.download_details_label.setStyleSheet(
|
|
"""
|
|
QLabel {
|
|
color: #cccccc;
|
|
font-size: 12px;
|
|
padding: 5px;
|
|
}
|
|
"""
|
|
)
|
|
progress_layout.addWidget(self.download_details_label)
|
|
|
|
# Create a horizontal layout for status label and open folder button
|
|
status_layout = QHBoxLayout()
|
|
|
|
self.status_label = QLabel(_("app.ready"))
|
|
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.status_label.setStyleSheet(
|
|
"""
|
|
QLabel {
|
|
color: #cccccc;
|
|
font-size: 12px;
|
|
padding: 5px;
|
|
}
|
|
"""
|
|
)
|
|
status_layout.addWidget(self.status_label)
|
|
|
|
# Add "Open Folder" button (initially hidden)
|
|
self.open_folder_btn = QPushButton("📁")
|
|
self.open_folder_btn.setToolTip(_("buttons.open_folder"))
|
|
self.open_folder_btn.setFixedSize(30, 30)
|
|
self.open_folder_btn.setStyleSheet(
|
|
"""
|
|
QPushButton {
|
|
background-color: #2a2d2e;
|
|
color: #cccccc;
|
|
border: 1px solid #404040;
|
|
border-radius: 5px;
|
|
font-size: 16px;
|
|
padding: 2px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #3a3d3e;
|
|
border: 1px solid #505050;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #1a1d1e;
|
|
}
|
|
"""
|
|
)
|
|
self.open_folder_btn.clicked.connect(self.open_download_folder)
|
|
self.open_folder_btn.setVisible(False) # Hidden by default
|
|
status_layout.addWidget(self.open_folder_btn)
|
|
|
|
progress_layout.addLayout(status_layout)
|
|
|
|
layout.addLayout(progress_layout)
|
|
|
|
# Connect signals
|
|
self.signals.update_formats.connect(self.update_format_table)
|
|
self.signals.update_status.connect(self.status_label.setText)
|
|
self.signals.update_progress.connect(self.update_progress_bar)
|
|
|
|
# Connect new signals
|
|
self.signals.playlist_info_label_visible.connect(self.playlist_info_label.setVisible)
|
|
self.signals.playlist_info_label_text.connect(self.playlist_info_label.setText)
|
|
self.signals.selected_subs_label_text.connect(self.selected_subs_label.setText)
|
|
self.signals.playlist_select_btn_visible.connect(self.playlist_select_btn.setVisible)
|
|
self.signals.playlist_select_btn_text.connect(self.playlist_select_btn.setText)
|
|
|
|
def analyze_url(self) -> None:
|
|
url = self.url_input.text().strip()
|
|
if not url:
|
|
self.signals.update_status.emit(_("main_ui.invalid_url_or_enter"))
|
|
return
|
|
|
|
# Validate URL before processing
|
|
is_valid, error_message = validate_video_url(url)
|
|
if not is_valid:
|
|
QMessageBox.warning(self, _("main_ui.error_title"), error_message)
|
|
return
|
|
|
|
self.signals.update_status.emit(_("main_ui.analyzing_preparing"))
|
|
threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start()
|
|
|
|
def _analyze_url_thread(self, url) -> None:
|
|
try:
|
|
self.signals.update_status.emit(_("main_ui.analyzing_extracting_basic"))
|
|
|
|
# Clean up the URL to handle both playlist and video URLs
|
|
if "list=" in url and "watch?v=" in url:
|
|
playlist_id = url.split("list=")[1].split("&")[0]
|
|
url = f"https://www.youtube.com/playlist?list={playlist_id}"
|
|
|
|
# Always use subprocess to call yt-dlp binary (Python package removed)
|
|
self._analyze_url_with_subprocess(url)
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Error in analysis: {e}")
|
|
self.signals.update_status.emit(_("errors.generic_error", error=str(e)))
|
|
# Ensure playlist UI is hidden on error too
|
|
# update signal method from QMetaObject.invokeMethod to signals
|
|
self.signals.playlist_info_label_visible.emit(False)
|
|
self.signals.playlist_select_btn_visible.emit(False)
|
|
|
|
def paste_url(self) -> None:
|
|
clipboard = QApplication.clipboard()
|
|
self.url_input.setText(clipboard.text())
|
|
|
|
def update_ytdlp(self) -> None:
|
|
"""Show the yt-dlp update dialog with proper progress tracking"""
|
|
# Make the dialog non-modal to prevent blocking the main UI
|
|
dialog = YTDLPUpdateDialog(self)
|
|
dialog.setModal(False) # Make it non-modal
|
|
dialog.show() # Use show() instead of exec() to avoid blocking
|
|
|
|
def show_download_settings_dialog(self) -> None: # Renamed method
|
|
dialog = DownloadSettingsDialog(self.last_path, self.speed_limit_value, self.speed_limit_unit_index, self)
|
|
if dialog.exec():
|
|
# Update Path
|
|
new_path = dialog.get_selected_path()
|
|
path_changed = False
|
|
if new_path != self.last_path:
|
|
self.last_path = new_path
|
|
save_path(self, self.last_path) # Save the updated path
|
|
path_changed = True
|
|
logger.info(f"Download path updated to: {self.last_path}")
|
|
|
|
# Update Speed Limit
|
|
new_limit_value = dialog.get_selected_speed_limit()
|
|
new_unit_index = dialog.get_selected_unit_index()
|
|
limit_changed = False
|
|
if new_limit_value != self.speed_limit_value or new_unit_index != self.speed_limit_unit_index:
|
|
self.speed_limit_value = new_limit_value
|
|
self.speed_limit_unit_index = new_unit_index
|
|
limit_changed = True
|
|
logger.info(
|
|
f"Speed limit updated to: {self.speed_limit_value} {['KB/s', 'MB/s'][self.speed_limit_unit_index] if self.speed_limit_value else 'None'}"
|
|
)
|
|
|
|
# Update Tooltip if anything changed
|
|
if path_changed or limit_changed:
|
|
limit_text = "None"
|
|
if self.speed_limit_value:
|
|
limit_text = f"{self.speed_limit_value} {['KB/s', 'MB/s'][self.speed_limit_unit_index]}"
|
|
self.settings_button.setToolTip(f"Current Path: {self.last_path}\nSpeed Limit: {limit_text}")
|
|
|
|
def start_download(self) -> None:
|
|
url = self.url_input.text().strip()
|
|
# --- Use self.last_path instead of reading from QLineEdit ---
|
|
path = self.last_path
|
|
|
|
if not url or not path:
|
|
# More specific error message if path is missing
|
|
if not path:
|
|
self.status_label.setText(_('download.please_set_path'))
|
|
elif not url:
|
|
self.status_label.setText(_('download.please_enter_url'))
|
|
else:
|
|
self.status_label.setText(_('download.please_enter_url_and_path'))
|
|
return
|
|
# --- End Path Change ---
|
|
|
|
# Validate URL before starting download
|
|
is_valid, error_message = validate_video_url(url)
|
|
if not is_valid:
|
|
QMessageBox.warning(self, _("main_ui.error_title"), error_message)
|
|
return
|
|
|
|
# Get selected format
|
|
selected_format = self.get_selected_format()
|
|
if not selected_format:
|
|
self.status_label.setText(_('download.please_select_format'))
|
|
return
|
|
format_id = selected_format["format_id"]
|
|
is_audio_only = bool(selected_format.get("is_audio_only"))
|
|
format_has_audio = bool(selected_format.get("has_audio"))
|
|
|
|
# Show preparation message
|
|
self.status_label.setText(_('download.preparing'))
|
|
self.progress_bar.setValue(0)
|
|
self.open_folder_btn.setVisible(False) # Hide the open folder button on new download
|
|
|
|
# Get resolution for filename
|
|
resolution = "default"
|
|
for checkbox in self.format_checkboxes:
|
|
if checkbox.isChecked():
|
|
parts = checkbox.text().split("•")
|
|
if len(parts) >= 1:
|
|
resolution = parts[0].strip().lower()
|
|
break
|
|
|
|
# Get subtitle selection if available - Now get the list
|
|
selected_subs = self.selected_subtitles if hasattr(self, "selected_subtitles") else []
|
|
|
|
# Get playlist selection IF in playlist mode - USE STORED VALUE
|
|
playlist_items_to_download = None
|
|
if self.is_playlist:
|
|
playlist_items_to_download = self.selected_playlist_items # Use the stored selection string
|
|
|
|
# --- Use stored speed limit values ---
|
|
rate_limit = None
|
|
if self.speed_limit_value:
|
|
try:
|
|
limit_value = float(self.speed_limit_value)
|
|
if self.speed_limit_unit_index == 0: # KB/s
|
|
rate_limit = f"{int(limit_value * 1024)}"
|
|
elif self.speed_limit_unit_index == 1: # MB/s
|
|
rate_limit = f"{int(limit_value * 1024 * 1024)}"
|
|
except ValueError:
|
|
# Use a signal to show error in status bar, similar to URL/Path errors
|
|
self.signals.update_status.emit(_("errors.invalid_speed_limit"))
|
|
return
|
|
# --- End speed limit update ---
|
|
|
|
# Save thumbnail if enabled
|
|
if self.save_thumbnail:
|
|
# Consider moving thumbnail download *after* successful video download
|
|
# Or handle errors more gracefully if thumbnail download fails
|
|
try:
|
|
self.download_thumbnail_file(url, path)
|
|
except Exception as e:
|
|
logger.warning(f"Thumbnail download failed: {e}", exc_info=True)
|
|
# Optionally inform the user, but don't stop the main download
|
|
|
|
# Create download thread with resolution in output template
|
|
self.download_thread = DownloadThread(
|
|
url=url,
|
|
path=path,
|
|
format_id=format_id,
|
|
is_audio_only=is_audio_only,
|
|
format_has_audio=format_has_audio,
|
|
subtitle_langs=selected_subs, # Pass the list of selected subs
|
|
is_playlist=self.is_playlist, # Use the flag directly
|
|
merge_subs=self.merge_subs_checkbox.isChecked(),
|
|
enable_sponsorblock=len(self.selected_sponsorblock_categories) > 0,
|
|
sponsorblock_categories=self.selected_sponsorblock_categories,
|
|
resolution=resolution,
|
|
playlist_items=playlist_items_to_download, # Pass the selection string
|
|
save_description=self.save_description, # Pass the new flag here
|
|
embed_chapters=self.embed_chapters, # Pass the embed chapters flag
|
|
cookie_file=self.cookie_file_path, # Pass the cookie file path
|
|
browser_cookies=self.browser_cookies_option, # Pass the browser cookies option
|
|
rate_limit=rate_limit, # Pass the calculated rate limit
|
|
download_section=self.download_section, # Pass the download section
|
|
force_keyframes=self.force_keyframes, # Pass the force keyframes setting
|
|
proxy_url=self.proxy_url, # Pass the proxy URL
|
|
geo_proxy_url=self.geo_proxy_url, # Pass the geo-verification proxy URL
|
|
)
|
|
|
|
# Connect signals
|
|
self.download_thread.progress_signal.connect(self.update_progress_bar)
|
|
self.download_thread.status_signal.connect(self.status_label.setText)
|
|
self.download_thread.update_details.connect(self.download_details_label.setText)
|
|
self.download_thread.finished_signal.connect(self.download_finished)
|
|
self.download_thread.error_signal.connect(self.download_error)
|
|
self.download_thread.file_exists_signal.connect(self.file_already_exists)
|
|
|
|
# Reset download state
|
|
self.download_paused = False
|
|
self.download_cancelled = False
|
|
|
|
# Show pause/cancel buttons
|
|
self.pause_btn.setText(_("buttons.pause"))
|
|
self.pause_btn.setVisible(True)
|
|
self.cancel_btn.setVisible(True)
|
|
|
|
# Start download thread
|
|
self.current_download = self.download_thread
|
|
self.download_thread.start()
|
|
self.toggle_download_controls(False)
|
|
|
|
def download_finished(self) -> None:
|
|
self.toggle_download_controls(True)
|
|
self.pause_btn.setVisible(False)
|
|
self.cancel_btn.setVisible(False)
|
|
self.progress_bar.setValue(100)
|
|
|
|
# Set completion message based on the file type of last downloaded file
|
|
if self.download_thread and self.download_thread.current_filename:
|
|
filename = Path(self.download_thread.current_filename)
|
|
ext = filename.suffix.lower()
|
|
|
|
# Video file extensions
|
|
if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]:
|
|
self.status_label.setText(_('download.video_completed'))
|
|
# Audio file extensions
|
|
elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]:
|
|
self.status_label.setText(_('download.audio_completed'))
|
|
# Subtitle file extensions
|
|
elif ext in [".vtt", ".srt", ".ass", ".ssa"]:
|
|
self.status_label.setText(_('download.subtitle_completed'))
|
|
# Default case
|
|
else:
|
|
self.status_label.setText(_('download.completed'))
|
|
|
|
# Show the open folder button
|
|
self.open_folder_btn.setVisible(True)
|
|
|
|
# Play notification sound when download completes
|
|
self.play_notification_sound()
|
|
|
|
def open_download_folder(self) -> None:
|
|
"""Open the folder containing the downloaded file and select it if possible"""
|
|
try:
|
|
if self.download_thread and self.download_thread.last_file_path:
|
|
file_path = Path(self.download_thread.last_file_path)
|
|
|
|
if file_path.exists():
|
|
# On Windows, use explorer with /select to highlight the file
|
|
if subprocess.sys.platform == "win32":
|
|
subprocess.run(['explorer', '/select,', str(file_path)], creationflags=SUBPROCESS_CREATIONFLAGS)
|
|
# On macOS, use open with -R to reveal in Finder
|
|
elif subprocess.sys.platform == "darwin":
|
|
subprocess.run(['open', '-R', str(file_path)])
|
|
# On Linux, try to open the folder (file selection not widely supported)
|
|
else:
|
|
folder_path = file_path.parent
|
|
subprocess.run(['xdg-open', str(folder_path)])
|
|
|
|
logger.info(f"Opened folder for: {file_path}")
|
|
else:
|
|
# If file doesn't exist, just open the download folder
|
|
folder_path = Path(self.last_path)
|
|
if folder_path.exists():
|
|
if subprocess.sys.platform == "win32":
|
|
subprocess.run(['explorer', str(folder_path)], creationflags=SUBPROCESS_CREATIONFLAGS)
|
|
elif subprocess.sys.platform == "darwin":
|
|
subprocess.run(['open', str(folder_path)])
|
|
else:
|
|
subprocess.run(['xdg-open', str(folder_path)])
|
|
else:
|
|
logger.warning(f"Download folder does not exist: {folder_path}")
|
|
else:
|
|
# Fallback to opening the general download folder
|
|
folder_path = Path(self.last_path)
|
|
if folder_path.exists():
|
|
if subprocess.sys.platform == "win32":
|
|
subprocess.run(['explorer', str(folder_path)], creationflags=SUBPROCESS_CREATIONFLAGS)
|
|
elif subprocess.sys.platform == "darwin":
|
|
subprocess.run(['open', str(folder_path)])
|
|
else:
|
|
subprocess.run(['xdg-open', str(folder_path)])
|
|
else:
|
|
logger.warning(f"Download folder does not exist: {folder_path}")
|
|
|
|
except Exception as e:
|
|
logger.exception(f"Error opening download folder: {e}")
|
|
QMessageBox.warning(self, "Error", f"Could not open folder: {str(e)}")
|
|
|
|
def download_error(self, error_message) -> None:
|
|
self.toggle_download_controls(True)
|
|
self.pause_btn.setVisible(False)
|
|
self.cancel_btn.setVisible(False)
|
|
self.status_label.setText(_("errors.generic_error", error=error_message))
|
|
self.download_details_label.setText("") # Clear details label on error
|
|
|
|
def update_progress_bar(self, value) -> None:
|
|
try:
|
|
# Ensure the value is an integer
|
|
int_value = int(value)
|
|
self.progress_bar.setValue(int_value)
|
|
except Exception as e:
|
|
logger.exception(f"Progress bar update error: {e}")
|
|
|
|
def toggle_pause(self) -> None:
|
|
if self.current_download:
|
|
self.current_download.paused = not self.current_download.paused
|
|
if self.current_download.paused:
|
|
self.pause_btn.setText(_("buttons.resume"))
|
|
self.signals.update_status.emit(_("download.paused"))
|
|
else:
|
|
self.pause_btn.setText(_("buttons.pause"))
|
|
self.signals.update_status.emit(_("download.resumed"))
|
|
|
|
def check_for_updates(self) -> None:
|
|
try:
|
|
# Get the latest release info from GitHub
|
|
response = requests.get(
|
|
"https://api.github.com/repos/oop7/YTSage/releases/latest",
|
|
headers={"Accept": "application/vnd.github.v3+json"},
|
|
)
|
|
response.raise_for_status()
|
|
|
|
latest_release = response.json()
|
|
latest_version = latest_release["tag_name"].lstrip("v")
|
|
|
|
# Compare versions
|
|
if version.parse(latest_version) > version.parse(self.version):
|
|
changelog = latest_release.get("body", "No changelog available.") # Get changelog body
|
|
self.show_update_dialog(latest_version, latest_release["html_url"], changelog) # Pass changelog
|
|
except Exception as e:
|
|
logger.exception(f"Failed to check for updates: {e}")
|
|
|
|
def show_update_dialog(self, latest_version, release_url, changelog) -> None: # Added changelog parameter
|
|
msg = QDialog(self)
|
|
msg.setWindowTitle(_("update_dialog.title"))
|
|
msg.setMinimumWidth(600) # Increased width for better layout
|
|
msg.setMinimumHeight(450) # Increased height for better spacing
|
|
|
|
# Set custom icon directly
|
|
try:
|
|
if self.windowIcon() and not self.windowIcon().isNull():
|
|
msg.setWindowIcon(self.windowIcon())
|
|
else:
|
|
# Fallback to icon file
|
|
# icon_path logic moved to src\utils\ytsage_constants.py
|
|
if ICON_PATH.exists():
|
|
msg.setWindowIcon(QIcon(str(ICON_PATH)))
|
|
except Exception:
|
|
pass
|
|
|
|
layout = QVBoxLayout(msg)
|
|
layout.setSpacing(15) # Increased spacing for better layout
|
|
layout.setContentsMargins(20, 20, 20, 20) # Added margins
|
|
|
|
# Header with icon and title
|
|
header_layout = QHBoxLayout()
|
|
|
|
# Add update icon
|
|
icon_label = QLabel()
|
|
icon_label.setPixmap(self.style().standardIcon(QStyle.StandardPixmap.SP_BrowserReload).pixmap(32, 32))
|
|
header_layout.addWidget(icon_label)
|
|
|
|
# Title
|
|
title_label = QLabel(f"<h2 style='color: #c90000; margin: 0;'>{_('update_dialog.title')}</h2>")
|
|
header_layout.addWidget(title_label)
|
|
header_layout.addStretch()
|
|
|
|
layout.addLayout(header_layout)
|
|
|
|
# Update message with better formatting
|
|
message_label = QLabel(
|
|
f"<div style='font-size: 13px; line-height: 1.4;'>"
|
|
f"<b style='color: #ffffff;'>{_('update_dialog.new_version_available')}</b><br><br>"
|
|
f"<span style='color: #cccccc;'>{_('update_dialog.current_version_label')} <b style='color: #ffffff;'>{self.version}</b></span><br>"
|
|
f"<span style='color: #cccccc;'>{_('update_dialog.latest_version_label')} <b style='color: #00ff88;'>{latest_version}</b></span>"
|
|
f"</div>"
|
|
)
|
|
message_label.setWordWrap(True)
|
|
message_label.setStyleSheet(
|
|
"""
|
|
QLabel {
|
|
background-color: #1d1e22;
|
|
border: 1px solid #3d3d3d;
|
|
border-radius: 6px;
|
|
padding: 15px;
|
|
margin: 5px 0;
|
|
}
|
|
"""
|
|
)
|
|
layout.addWidget(message_label)
|
|
|
|
# Changelog Section
|
|
changelog_label = QLabel(f"<b style='color: #ffffff; font-size: 14px;'>{_('update_dialog.changelog')}:</b>")
|
|
changelog_label.setStyleSheet("padding: 5px 0; margin-top: 10px;")
|
|
layout.addWidget(changelog_label)
|
|
|
|
changelog_text = QTextEdit()
|
|
changelog_text.setReadOnly(True)
|
|
# Convert Markdown to HTML and set it
|
|
try:
|
|
html_changelog = markdown.markdown(
|
|
changelog,
|
|
extensions=[
|
|
"markdown.extensions.tables",
|
|
"markdown.extensions.fenced_code",
|
|
],
|
|
)
|
|
changelog_text.setHtml(html_changelog)
|
|
except Exception as e:
|
|
logger.warning(f"Error converting changelog markdown to HTML: {e}", exc_info=True)
|
|
changelog_text.setPlainText(changelog) # Fallback to plain text
|
|
|
|
changelog_text.setStyleSheet(
|
|
"""
|
|
QTextEdit {
|
|
background-color: #1d1e22;
|
|
border: 2px solid #3d3d3d;
|
|
border-radius: 6px;
|
|
color: #ffffff;
|
|
padding: 10px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
}
|
|
QScrollBar:vertical {
|
|
border: none;
|
|
background: #1d1e22;
|
|
width: 12px;
|
|
border-radius: 6px;
|
|
}
|
|
QScrollBar::handle:vertical {
|
|
background: #404040;
|
|
min-height: 20px;
|
|
border-radius: 6px;
|
|
}
|
|
QScrollBar::handle:vertical:hover {
|
|
background: #505050;
|
|
}
|
|
"""
|
|
)
|
|
changelog_text.setMaximumHeight(180) # Limit height
|
|
layout.addWidget(changelog_text)
|
|
|
|
# Buttons with better styling
|
|
button_layout = QHBoxLayout()
|
|
button_layout.setSpacing(10)
|
|
|
|
download_btn = QPushButton(_('update_dialog.download_update'))
|
|
download_btn.clicked.connect(lambda: self.open_release_page(release_url))
|
|
download_btn.setStyleSheet(
|
|
"""
|
|
QPushButton {
|
|
padding: 10px 20px;
|
|
background-color: #c90000;
|
|
border: none;
|
|
border-radius: 6px;
|
|
color: white;
|
|
font-weight: bold;
|
|
font-size: 13px;
|
|
min-width: 140px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #a50000;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #800000;
|
|
}
|
|
"""
|
|
)
|
|
|
|
remind_btn = QPushButton(_('update_dialog.remind_later'))
|
|
remind_btn.clicked.connect(msg.close)
|
|
remind_btn.setStyleSheet(
|
|
"""
|
|
QPushButton {
|
|
padding: 10px 20px;
|
|
background-color: #3d3d3d;
|
|
border: 1px solid #555555;
|
|
border-radius: 6px;
|
|
color: white;
|
|
font-weight: bold;
|
|
font-size: 13px;
|
|
min-width: 140px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #4d4d4d;
|
|
border-color: #666666;
|
|
}
|
|
QPushButton:pressed {
|
|
background-color: #2d2d2d;
|
|
}
|
|
"""
|
|
)
|
|
|
|
button_layout.addStretch()
|
|
button_layout.addWidget(download_btn)
|
|
button_layout.addWidget(remind_btn)
|
|
layout.addLayout(button_layout)
|
|
|
|
# Style the dialog with improved theme matching
|
|
msg.setStyleSheet(
|
|
"""
|
|
QDialog {
|
|
background-color: #15181b;
|
|
border: 1px solid #3d3d3d;
|
|
border-radius: 8px;
|
|
}
|
|
QLabel {
|
|
color: #ffffff;
|
|
font-size: 12px;
|
|
}
|
|
"""
|
|
)
|
|
|
|
msg.show()
|
|
|
|
def open_release_page(self, url):
|
|
webbrowser.open(url)
|
|
|
|
def check_auto_update_ytdlp(self) -> None:
|
|
"""Check and perform auto-update for yt-dlp if enabled and due."""
|
|
try:
|
|
# Check if auto-update should be performed
|
|
if should_check_for_auto_update():
|
|
logger.info("Performing auto-update check for yt-dlp...")
|
|
# Perform the auto-update in a non-blocking way
|
|
# We don't want to block the UI startup for this
|
|
QTimer.singleShot(2000, self._perform_auto_update) # Delay 2 seconds after startup
|
|
except Exception as e:
|
|
logger.exception(f"Error in auto-update check: {e}")
|
|
|
|
def _perform_auto_update(self) -> None:
|
|
"""Actually perform the auto-update check and update if needed in a background thread."""
|
|
try:
|
|
# Create and start the auto-update thread to avoid blocking the UI
|
|
|
|
self.auto_update_thread = AutoUpdateThread()
|
|
self.auto_update_thread.update_finished.connect(self._on_auto_update_finished)
|
|
self.auto_update_thread.start()
|
|
except Exception as e:
|
|
logger.exception(f"Error starting auto-update thread: {e}")
|
|
|
|
def _on_auto_update_finished(self, success, message) -> None:
|
|
"""Handle auto-update completion."""
|
|
if success:
|
|
logger.info(f"Auto-update completed successfully: {message}")
|
|
else:
|
|
logger.warning(f"Auto-update completed with issues: {message}")
|
|
|
|
# Clean up the thread reference and ensure it's properly finished
|
|
if hasattr(self, "auto_update_thread"):
|
|
# Disconnect all signals to prevent further callbacks
|
|
self.auto_update_thread.update_finished.disconnect()
|
|
# Make sure thread is finished
|
|
if self.auto_update_thread.isRunning():
|
|
self.auto_update_thread.quit()
|
|
self.auto_update_thread.wait(1000) # Wait up to 1 second
|
|
# Remove the reference
|
|
delattr(self, "auto_update_thread")
|
|
|
|
def closeEvent(self, event) -> None:
|
|
"""Handle application close event to ensure proper cleanup of background threads."""
|
|
try:
|
|
# Stop the auto-update thread if it's running
|
|
if hasattr(self, "auto_update_thread") and self.auto_update_thread.isRunning():
|
|
logger.info("Stopping auto-update thread...")
|
|
self.auto_update_thread.quit()
|
|
if not self.auto_update_thread.wait(3000): # Wait up to 3 seconds for graceful shutdown
|
|
logger.warning("Force terminating auto-update thread...")
|
|
self.auto_update_thread.terminate()
|
|
self.auto_update_thread.wait(1000) # Wait for termination
|
|
|
|
# Cancel any running downloads
|
|
if self.current_download and self.current_download.isRunning():
|
|
logger.info("Canceling running download...")
|
|
self.current_download.cancel()
|
|
if not self.current_download.wait(3000): # Wait up to 3 seconds for graceful shutdown
|
|
logger.warning("Force terminating download thread...")
|
|
self.current_download.terminate()
|
|
self.current_download.wait(1000) # Wait for termination
|
|
|
|
logger.info("Application closing...")
|
|
event.accept()
|
|
except Exception as e:
|
|
logger.exception(f"Error during application close: {e}")
|
|
event.accept() # Accept the close event anyway
|
|
|
|
def show_custom_options(self) -> None:
|
|
dialog = CustomOptionsDialog(self)
|
|
if dialog.exec():
|
|
# Handle proxy options
|
|
proxy_url = dialog.get_proxy_url()
|
|
geo_proxy_url = dialog.get_geo_proxy_url()
|
|
|
|
# Update instance variables
|
|
self.proxy_url = proxy_url
|
|
self.geo_proxy_url = geo_proxy_url
|
|
|
|
# Save proxy settings to config
|
|
ConfigManager.set("proxy_url", proxy_url)
|
|
ConfigManager.set("geo_proxy_url", geo_proxy_url)
|
|
|
|
# Show confirmation messages
|
|
if proxy_url:
|
|
logger.info(f"Main proxy set: {self.proxy_url}")
|
|
QMessageBox.information(
|
|
self,
|
|
"Proxy Set",
|
|
f"Main proxy set and saved: {proxy_url}",
|
|
)
|
|
|
|
if geo_proxy_url:
|
|
logger.info(f"Geo-verification proxy set: {self.geo_proxy_url}")
|
|
QMessageBox.information(
|
|
self,
|
|
"Geo Proxy Set",
|
|
f"Geo-verification proxy set and saved: {geo_proxy_url}",
|
|
)
|
|
|
|
# Show a combined message if both are cleared
|
|
if not proxy_url and not geo_proxy_url and (ConfigManager.get("proxy_url") or ConfigManager.get("geo_proxy_url")):
|
|
QMessageBox.information(
|
|
self,
|
|
"Proxy Settings Cleared",
|
|
"All proxy settings have been cleared and saved.",
|
|
)
|
|
|
|
def show_about_dialog(self) -> None: # ADDED METHOD HERE
|
|
dialog = AboutDialog(self)
|
|
dialog.exec()
|
|
|
|
def file_already_exists(self, filename) -> None:
|
|
"""Handle case when file already exists - simplified version"""
|
|
self.toggle_download_controls(True)
|
|
self.pause_btn.setVisible(False)
|
|
self.cancel_btn.setVisible(False)
|
|
self.progress_bar.setValue(100)
|
|
|
|
# Determine file type based on extension
|
|
ext = Path(filename).suffix.lower()
|
|
|
|
# Video file extensions
|
|
if ext in [".mp4", ".webm", ".mkv", ".avi", ".mov", ".flv"]:
|
|
self.status_label.setText(_("status.video_file_exists"))
|
|
# Audio file extensions
|
|
elif ext in [".mp3", ".m4a", ".aac", ".wav", ".ogg", ".opus", ".flac"]:
|
|
self.status_label.setText(_("status.audio_file_exists"))
|
|
# Subtitle file extensions
|
|
elif ext in [".vtt", ".srt", ".ass", ".ssa"]:
|
|
self.status_label.setText(_("status.subtitle_file_exists"))
|
|
# Default case
|
|
else:
|
|
self.status_label.setText(_("status.file_exists"))
|
|
|
|
# Show a simple message dialog
|
|
msg_box = QMessageBox()
|
|
msg_box.setIcon(QMessageBox.Icon.Information)
|
|
msg_box.setWindowTitle(_("file_exists_dialog.title"))
|
|
msg_box.setText(_("file_exists_dialog.message", filename=filename))
|
|
msg_box.setInformativeText(_("file_exists_dialog.info"))
|
|
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
|
|
|
# Set the window icon to match the main application
|
|
msg_box.setWindowIcon(self.windowIcon())
|
|
|
|
# Style the dialog
|
|
msg_box.setStyleSheet(
|
|
"""
|
|
QMessageBox {
|
|
background-color: #2b2b2b;
|
|
}
|
|
QLabel {
|
|
color: #ffffff;
|
|
}
|
|
QPushButton {
|
|
padding: 8px 15px;
|
|
background-color: #ff0000;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-weight: bold;
|
|
min-width: 80px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #cc0000;
|
|
}
|
|
"""
|
|
)
|
|
|
|
msg_box.exec()
|
|
|
|
# --- Add Toggle Methods Here ---
|
|
def toggle_save_thumbnail(self, state) -> None:
|
|
logger.debug(f"Raw thumbnail state received: {state}") # Debug: Print raw state
|
|
self.save_thumbnail = bool(state == 2) # Compare state directly with 2 (Checked state)
|
|
logger.debug(f"Save thumbnail toggled: {self.save_thumbnail}")
|
|
|
|
def toggle_save_description(self, state) -> None:
|
|
logger.debug(f"Raw description state received: {state}") # Debug: Print raw state
|
|
self.save_description = bool(state == 2) # Compare state directly with 2 (Checked state)
|
|
logger.debug(f"Save description toggled: {self.save_description}")
|
|
|
|
def toggle_embed_chapters(self, state) -> None:
|
|
logger.debug(f"Raw chapters state received: {state}") # Debug: Print raw state
|
|
self.embed_chapters = bool(state == 2) # Compare state directly with 2 (Checked state)
|
|
logger.debug(f"Embed chapters toggled: {self.embed_chapters}")
|
|
|
|
# --- End Toggle Methods ---
|
|
|
|
def open_playlist_selection_dialog(self) -> None:
|
|
if not self.is_playlist or not self.playlist_entries:
|
|
logger.info("No playlist data available to select from.")
|
|
return
|
|
|
|
dialog = PlaylistSelectionDialog(self.playlist_entries, self.selected_playlist_items, self)
|
|
|
|
if dialog.exec():
|
|
self.selected_playlist_items = dialog.get_selected_items_string()
|
|
logger.info(f"Playlist items selected: {self.selected_playlist_items}")
|
|
|
|
# Update button text (this call is safe as it happens in the main thread after dialog closes)
|
|
if self.selected_playlist_items is None:
|
|
button_text = "Select Videos... (All selected)"
|
|
else:
|
|
selected_indices = dialog._parse_selection_string(self.selected_playlist_items)
|
|
count = len(selected_indices)
|
|
display_text = (
|
|
self.selected_playlist_items if len(self.selected_playlist_items) < 30 else f"{count} videos selected"
|
|
)
|
|
button_text = f"Select Videos... ({display_text})"
|
|
self.playlist_select_btn.setText(button_text) # Direct call is fine here
|
|
|
|
# --- New Slot for Updating Playlist Button Text ---
|
|
# moved to SignalManager as Signal and added to init_ui() method.
|
|
|
|
def toggle_download_controls(self, enabled=True) -> None:
|
|
"""Enable or disable download-related controls"""
|
|
self.url_input.setEnabled(enabled)
|
|
self.analyze_button.setEnabled(enabled)
|
|
self.format_table.setEnabled(enabled) # Changed from format_scroll_area to format_table
|
|
self.download_btn.setEnabled(enabled)
|
|
if hasattr(self, "subtitle_combo"):
|
|
self.subtitle_combo.setEnabled(enabled) # type: ignore[reportAttributeAccessIssue]
|
|
self.video_button.setEnabled(enabled)
|
|
self.audio_button.setEnabled(enabled)
|
|
if hasattr(self, "sponsorblock_select_btn"):
|
|
self.sponsorblock_select_btn.setEnabled(enabled)
|
|
self.merge_subs_checkbox.setEnabled(enabled) # Enable/disable merge subs checkbox
|
|
self.custom_options_btn.setEnabled(enabled) # Enable/disable custom options button
|
|
self.update_ytdlp_btn.setEnabled(enabled) # Enable/disable update button
|
|
self.time_range_btn.setEnabled(enabled) # Enable/disable time range button
|
|
self.settings_button.setEnabled(enabled) # Enable/disable settings button
|
|
|
|
# Clear progress/status when controls are re-enabled
|
|
if enabled:
|
|
self.progress_bar.setValue(0)
|
|
self.status_label.setText(_("status.ready"))
|
|
self.download_details_label.setText("") # Clear details label
|
|
|
|
def handle_format_selection(self, button) -> None:
|
|
# Update formats
|
|
self.filter_formats()
|
|
|
|
def handle_mode_change(self) -> None:
|
|
"""Enable or disable features based on video/audio mode"""
|
|
if self.audio_button.isChecked():
|
|
# In Audio Only mode, disable video-specific features
|
|
if hasattr(self, "sponsorblock_select_btn"):
|
|
self.sponsorblock_select_btn.setEnabled(False)
|
|
if hasattr(self, "selected_sponsorblock_categories"):
|
|
self.selected_sponsorblock_categories = [] # Clear selection when disabled
|
|
if hasattr(self, "_update_sponsorblock_display"):
|
|
self._update_sponsorblock_display()
|
|
self.merge_subs_checkbox.setEnabled(False)
|
|
self.merge_subs_checkbox.setChecked(False) # Uncheck when disabled
|
|
|
|
# Allow subtitle selection in Audio Only mode too
|
|
if hasattr(self, "subtitle_select_btn"):
|
|
self.subtitle_select_btn.setEnabled(True)
|
|
else:
|
|
# In Video mode, enable video-specific features
|
|
if hasattr(self, "sponsorblock_select_btn"):
|
|
self.sponsorblock_select_btn.setEnabled(True)
|
|
# Don't automatically restore categories - let user choose when they open the dialog
|
|
|
|
# Enable merge_subs only if subtitles are selected
|
|
has_subs_selected = len(getattr(self, "selected_subtitles", [])) > 0
|
|
self.merge_subs_checkbox.setEnabled(has_subs_selected)
|
|
|
|
# Re-enable subtitle selection button in Video mode
|
|
if hasattr(self, "subtitle_select_btn"):
|
|
self.subtitle_select_btn.setEnabled(True)
|
|
|
|
# Keep these methods for backwards compatibility - they just call the new dialog now
|
|
def show_custom_command(self) -> None:
|
|
dialog = CustomOptionsDialog(self)
|
|
dialog.tab_widget.setCurrentIndex(1) # Select the Custom Command tab
|
|
dialog.exec()
|
|
|
|
def show_cookie_login_dialog(self) -> None:
|
|
dialog = CustomOptionsDialog(self)
|
|
dialog.tab_widget.setCurrentIndex(0) # Select the Cookie Login tab
|
|
if dialog.exec():
|
|
# Handle cookies
|
|
cookie_path = dialog.get_cookie_file_path()
|
|
browser_cookies = dialog.get_browser_cookies_option()
|
|
|
|
if cookie_path:
|
|
self.cookie_file_path = cookie_path
|
|
self.browser_cookies_option = None # Clear browser cookies if file is used
|
|
logger.info(f"Selected cookie file: {self.cookie_file_path}")
|
|
QMessageBox.information(
|
|
self,
|
|
"Cookie File Selected",
|
|
f"Cookie file selected: {self.cookie_file_path}",
|
|
)
|
|
elif browser_cookies:
|
|
self.browser_cookies_option = browser_cookies
|
|
self.cookie_file_path = None # Clear file cookies if browser is used
|
|
logger.info(f"Selected browser cookies: {self.browser_cookies_option}")
|
|
QMessageBox.information(
|
|
self,
|
|
"Browser Cookies Selected",
|
|
f"Browser cookies will be extracted from: {browser_cookies}",
|
|
)
|
|
else:
|
|
self.cookie_file_path = None # Clear path if dialog accepted but no file selected
|
|
self.browser_cookies_option = None # Clear browser cookies too
|
|
|
|
def cancel_download(self) -> None:
|
|
if self.current_download:
|
|
self.current_download.cancelled = True
|
|
self.status_label.setText(_("status.cancelling")) # Set status directly
|
|
self.download_details_label.setText("") # Clear details label on cancellation
|
|
|
|
def show_ffmpeg_dialog(self) -> None:
|
|
dialog = FFmpegCheckDialog(self)
|
|
dialog.exec()
|
|
|
|
# Add method for showing time range dialog
|
|
def show_time_range_dialog(self) -> None:
|
|
dialog = TimeRangeDialog(self)
|
|
if dialog.exec():
|
|
# Store the time range settings
|
|
self.download_section = dialog.get_download_sections()
|
|
self.force_keyframes = dialog.get_force_keyframes()
|
|
|
|
if self.download_section:
|
|
self.time_range_btn.setStyleSheet(
|
|
"""
|
|
QPushButton {
|
|
padding: 8px 15px;
|
|
background-color: #c90000;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-weight: bold;
|
|
border: 2px solid white;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #a50000;
|
|
}
|
|
"""
|
|
)
|
|
self.time_range_btn.setToolTip(f"Section set: {self.download_section}")
|
|
else:
|
|
# Reset to default style if no section is selected
|
|
self.download_section = None
|
|
self.force_keyframes = False
|
|
self.time_range_btn.setStyleSheet("")
|
|
self.time_range_btn.setToolTip("")
|
|
|
|
def show_ytdlp_setup_dialog(self) -> None:
|
|
"""Show the yt-dlp setup dialog to configure yt-dlp"""
|
|
yt_dlp_path = setup_ytdlp(self)
|
|
if yt_dlp_path != "yt-dlp":
|
|
success_dialog = QMessageBox(self)
|
|
success_dialog.setIcon(QMessageBox.Icon.Information)
|
|
success_dialog.setWindowTitle("yt-dlp Setup")
|
|
success_dialog.setText(f"yt-dlp has been successfully configured at:\n{yt_dlp_path}")
|
|
success_dialog.setWindowIcon(self.windowIcon())
|
|
success_dialog.setStyleSheet(
|
|
"""
|
|
QMessageBox {
|
|
background-color: #15181b;
|
|
color: #ffffff;
|
|
}
|
|
QLabel {
|
|
color: #ffffff;
|
|
}
|
|
QPushButton {
|
|
padding: 8px 15px;
|
|
background-color: #c90000;
|
|
border: none;
|
|
border-radius: 4px;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #a50000;
|
|
}
|
|
"""
|
|
)
|
|
success_dialog.exec()
|
|
|
|
def _analyze_url_with_subprocess(self, url) -> None:
|
|
"""Analyze URL using yt-dlp executable when Python module is not available"""
|
|
|
|
try:
|
|
yt_dlp_path = get_yt_dlp_path()
|
|
if not yt_dlp_path:
|
|
logger.error("yt-dlp executable not found. Please install yt-dlp first.")
|
|
self.signals.update_status.emit(_("errors.ytdlp_not_found"))
|
|
self.signals.playlist_info_label_visible.emit(False)
|
|
self.signals.playlist_select_btn_visible.emit(False)
|
|
return
|
|
|
|
self.signals.update_status.emit(_("main_ui.analyzing_extracting_ytdlp"))
|
|
|
|
# Clean up the URL to handle both playlist and video URLs
|
|
if "list=" in url and "watch?v=" in url:
|
|
playlist_id = url.split("list=")[1].split("&")[0]
|
|
url = f"https://www.youtube.com/playlist?list={playlist_id}"
|
|
|
|
# Build command for basic info extraction
|
|
cmd = [yt_dlp_path, "--dump-json", "--no-warnings", url]
|
|
|
|
# Add cookies if available
|
|
if self.cookie_file_path:
|
|
cmd.extend(["--cookies", str(self.cookie_file_path)])
|
|
elif self.browser_cookies_option:
|
|
cmd.extend(["--cookies-from-browser", self.browser_cookies_option])
|
|
|
|
# Add proxy settings if available
|
|
if self.proxy_url:
|
|
cmd.extend(["--proxy", self.proxy_url])
|
|
|
|
if self.geo_proxy_url:
|
|
cmd.extend(["--geo-verification-proxy", self.geo_proxy_url])
|
|
|
|
# Execute command with hidden console window on Windows
|
|
# Extra logic moved to src\utils\ytsage_constants.py
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, creationflags=SUBPROCESS_CREATIONFLAGS)
|
|
|
|
if result.returncode != 0:
|
|
logger.error(f"yt-dlp failed: {result.stderr}")
|
|
self.signals.update_status.emit(_("errors.ytdlp_failed", error=result.stderr))
|
|
self.signals.playlist_info_label_visible.emit(False)
|
|
self.signals.playlist_select_btn_visible.emit(False)
|
|
return
|
|
|
|
json_lines = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
|
|
|
|
if not json_lines:
|
|
logger.error("No data returned from yt-dlp")
|
|
self.signals.update_status.emit(_("errors.no_data_returned"))
|
|
self.signals.playlist_info_label_visible.emit(False)
|
|
self.signals.playlist_select_btn_visible.emit(False)
|
|
return
|
|
|
|
try:
|
|
first_info = json.loads(json_lines[0])
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"Failed to parse yt-dlp output: {e}")
|
|
self.signals.update_status.emit(_("errors.parse_failed", error=str(e)))
|
|
self.signals.playlist_info_label_visible.emit(False)
|
|
self.signals.playlist_select_btn_visible.emit(False)
|
|
return
|
|
|
|
self.signals.update_status.emit(_("main_ui.analyzing_processing_data"))
|
|
|
|
if first_info.get("_type") == "playlist" or len(json_lines) > 1:
|
|
# Handle playlist
|
|
self.is_playlist = True
|
|
self.playlist_info = first_info
|
|
self.selected_playlist_items = None
|
|
self.playlist_entries = []
|
|
|
|
# Parse all entries
|
|
for line in json_lines:
|
|
try:
|
|
entry = json.loads(line)
|
|
if entry.get("_type") != "playlist": # Skip playlist metadata
|
|
self.playlist_entries.append(entry)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
|
|
if not self.playlist_entries:
|
|
logger.error("Playlist contains no valid videos.")
|
|
self.signals.update_status.emit(_("errors.playlist_no_videos"))
|
|
self.signals.playlist_info_label_visible.emit(False)
|
|
self.signals.playlist_select_btn_visible.emit(False)
|
|
return
|
|
|
|
# Use first video for format information
|
|
self.video_info = self.playlist_entries[0]
|
|
|
|
# Update playlist info label
|
|
playlist_text = _("playlist.display_format",
|
|
title=first_info.get('title', _('playlist.unknown')),
|
|
count=len(self.playlist_entries))
|
|
# update signal method from QMetaObject.invokeMethod to signals
|
|
self.signals.playlist_info_label_text.emit(playlist_text)
|
|
self.signals.playlist_info_label_visible.emit(True)
|
|
|
|
# Show playlist selection button
|
|
# update signal method from QMetaObject.invokeMethod to signals
|
|
self.signals.playlist_select_btn_text.emit(_("main_ui.select_videos_all"))
|
|
|
|
# update signal method from QMetaObject.invokeMethod to signals
|
|
self.signals.playlist_select_btn_visible.emit(True)
|
|
|
|
else:
|
|
# Handle single video
|
|
self.is_playlist = False
|
|
self.video_info = first_info
|
|
self.playlist_entries = []
|
|
self.selected_playlist_items = None
|
|
|
|
# Hide playlist UI
|
|
# update signal method from QMetaObject.invokeMethod to signals
|
|
self.signals.playlist_info_label_visible.emit(False)
|
|
|
|
# update signal method from QMetaObject.invokeMethod to signals
|
|
self.signals.playlist_select_btn_visible.emit(False)
|
|
|
|
# Verify we have format information
|
|
if not self.video_info or "formats" not in self.video_info:
|
|
logger.error("No format information available")
|
|
self.signals.update_status.emit(_("errors.no_format_info"))
|
|
self.signals.playlist_info_label_visible.emit(False)
|
|
self.signals.playlist_select_btn_visible.emit(False)
|
|
return
|
|
|
|
self.signals.update_status.emit(_("main_ui.analyzing_processing_formats_ytdlp"))
|
|
self.all_formats = self.video_info["formats"]
|
|
|
|
# Update UI
|
|
self.update_video_info(self.video_info)
|
|
|
|
# Update thumbnail
|
|
self.signals.update_status.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp"))
|
|
# Try to get thumbnail from playlist info first
|
|
# Fallback to video thumbnail if playlist thumbnail not found or not a playlist
|
|
thumbnail_url = (self.playlist_info or {}).get("thumbnail") or (self.video_info or {}).get("thumbnail")
|
|
|
|
self.download_thumbnail(thumbnail_url)
|
|
|
|
# Save thumbnail if enabled
|
|
if self.save_thumbnail:
|
|
self.download_thumbnail_file(self.video_url, self.path_input.text()) # type: ignore[reportAttributeAccessIssue]
|
|
|
|
# Handle subtitles
|
|
self.signals.update_status.emit(_("main_ui.analyzing_processing_subtitles_ytdlp"))
|
|
self.selected_subtitles = []
|
|
self.available_subtitles = self.video_info.get("subtitles", {})
|
|
self.available_automatic_subtitles = self.video_info.get("automatic_captions", {})
|
|
|
|
# Update subtitle UI
|
|
# update signal method from QMetaObject.invokeMethod to signals
|
|
self.signals.selected_subs_label_text.emit(_("main_ui.zero_selected"))
|
|
|
|
# Update format table
|
|
self.signals.update_status.emit(_("main_ui.analyzing_updating_table"))
|
|
self.video_button.setChecked(True)
|
|
self.audio_button.setChecked(False)
|
|
self.filter_formats()
|
|
|
|
self.signals.update_status.emit(_("main_ui.analysis_complete"))
|
|
|
|
except subprocess.TimeoutExpired:
|
|
logger.error("Analysis timed out. Please try again.")
|
|
self.signals.update_status.emit(_("errors.analysis_timeout"))
|
|
self.signals.playlist_info_label_visible.emit(False)
|
|
self.signals.playlist_select_btn_visible.emit(False)
|
|
except json.JSONDecodeError as e:
|
|
logger.error(f"Failed to parse yt-dlp output: {e}")
|
|
self.signals.update_status.emit(f"Error: Failed to parse yt-dlp output: {e}")
|
|
self.signals.playlist_info_label_visible.emit(False)
|
|
self.signals.playlist_select_btn_visible.emit(False)
|
|
except Exception as e:
|
|
logger.error(f"Analysis failed: {e}")
|
|
self.signals.update_status.emit(_("errors.analysis_failed", error=str(e)))
|
|
self.signals.playlist_info_label_visible.emit(False)
|
|
self.signals.playlist_select_btn_visible.emit(False)
|