Refactor Imports, yt-dlp/Pygame Availability, and ffmpeg/yt-dlp Checks
This commit is contained in:
+457
-50
@@ -17,11 +17,24 @@ from pathlib import Path
|
||||
from packaging import version
|
||||
import subprocess
|
||||
import re
|
||||
import yt_dlp
|
||||
try:
|
||||
import yt_dlp
|
||||
YT_DLP_AVAILABLE = True
|
||||
except ImportError:
|
||||
YT_DLP_AVAILABLE = False
|
||||
print("Warning: yt-dlp not available at startup, will be downloaded at runtime")
|
||||
import markdown
|
||||
try:
|
||||
import pygame
|
||||
PYGAME_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYGAME_AVAILABLE = False
|
||||
print("Warning: pygame not available, audio notifications disabled")
|
||||
import threading
|
||||
|
||||
from ytsage_downloader import DownloadThread, SignalManager # Import downloader related classes
|
||||
from ytsage_utils import check_ffmpeg, get_yt_dlp_path, load_saved_path, save_path # Import utility functions
|
||||
from ytsage_utils import check_ffmpeg, load_saved_path, save_path, get_config_file_path, get_ytdlp_version, get_ffmpeg_version, should_check_for_auto_update, check_and_update_ytdlp_auto # Import utility functions
|
||||
from ytsage_yt_dlp import check_ytdlp_binary, setup_ytdlp, get_ytdlp_executable_path, get_yt_dlp_path # Import the new yt-dlp functions
|
||||
from ytsage_gui_dialogs import (LogWindow, CustomCommandDialog, FFmpegCheckDialog,
|
||||
YTDLPUpdateDialog, AboutDialog, SubtitleSelectionDialog,
|
||||
PlaylistSelectionDialog, CookieLoginDialog,
|
||||
@@ -32,13 +45,25 @@ from ytsage_gui_video_info import VideoInfoMixin # Import VideoInfoMixin
|
||||
class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from mixins
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# Check for FFmpeg before proceeding
|
||||
if not check_ffmpeg():
|
||||
self.show_ffmpeg_dialog()
|
||||
|
||||
self.version = "4.5.7"
|
||||
# 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:
|
||||
print(f"Using yt-dlp from: {ytdlp_path}")
|
||||
|
||||
self.version = "4.6.0"
|
||||
self.check_for_updates()
|
||||
self.config_file = Path.home() / '.ytsage_config.json'
|
||||
|
||||
# Check for auto-updates if enabled
|
||||
self.check_auto_update_ytdlp()
|
||||
|
||||
self.config_file = get_config_file_path()
|
||||
load_saved_path(self)
|
||||
# Load custom icon
|
||||
icon_path = os.path.join(os.path.dirname(__file__), 'Icon', 'icon.png')
|
||||
@@ -248,6 +273,57 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
|
||||
# Initialize UI state based on current mode
|
||||
self.handle_mode_change()
|
||||
|
||||
# Initialize pygame for sound notifications
|
||||
self.init_sound()
|
||||
|
||||
def init_sound(self):
|
||||
"""Initialize pygame mixer for sound notifications"""
|
||||
try:
|
||||
if PYGAME_AVAILABLE:
|
||||
pygame.mixer.init()
|
||||
self.sound_enabled = True
|
||||
|
||||
# Get the notification sound path
|
||||
self.notification_sound_path = os.path.join(os.path.dirname(__file__), 'sound', 'notification.mp3')
|
||||
|
||||
# Check if the notification sound file exists
|
||||
if not os.path.exists(self.notification_sound_path):
|
||||
print(f"Warning: Notification sound file not found at: {self.notification_sound_path}")
|
||||
self.sound_enabled = False
|
||||
else:
|
||||
print(f"Notification sound loaded from: {self.notification_sound_path}")
|
||||
else:
|
||||
self.sound_enabled = False
|
||||
print("Sound notifications disabled - pygame not available")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error initializing sound: {e}")
|
||||
self.sound_enabled = False
|
||||
|
||||
def play_notification_sound(self):
|
||||
"""Play notification sound in a separate thread to avoid blocking the UI"""
|
||||
if not self.sound_enabled:
|
||||
return
|
||||
|
||||
def play_sound():
|
||||
try:
|
||||
if PYGAME_AVAILABLE:
|
||||
# Load and play the sound
|
||||
pygame.mixer.music.load(self.notification_sound_path)
|
||||
pygame.mixer.music.play()
|
||||
|
||||
# Wait for the sound to finish playing
|
||||
while pygame.mixer.music.get_busy():
|
||||
pygame.time.wait(100)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error playing notification sound: {e}")
|
||||
|
||||
# Play sound in a separate thread to avoid blocking the UI
|
||||
sound_thread = threading.Thread(target=play_sound)
|
||||
sound_thread.daemon = True
|
||||
sound_thread.start()
|
||||
|
||||
def load_saved_path(self): # Using function from ytsage_utils now - no longer needed in class
|
||||
pass # Handled in class init now via ytsage_utils.load_saved_path(self)
|
||||
|
||||
@@ -255,7 +331,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
|
||||
save_path(self, path) # Call the utility function
|
||||
|
||||
def init_ui(self):
|
||||
self.setWindowTitle('YTSage v4.5.7')
|
||||
self.setWindowTitle('YTSage v4.6.0')
|
||||
self.setMinimumSize(900, 750)
|
||||
|
||||
# Main widget and layout
|
||||
@@ -606,6 +682,12 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
|
||||
playlist_id = url.split('list=')[1].split('&')[0]
|
||||
url = f'https://www.youtube.com/playlist?list={playlist_id}'
|
||||
|
||||
# Check if yt-dlp Python module is available
|
||||
if not YT_DLP_AVAILABLE:
|
||||
# Use subprocess to call yt-dlp executable
|
||||
self._analyze_url_with_subprocess(url)
|
||||
return
|
||||
|
||||
# Initial extraction with basic options - suppress warnings here too
|
||||
ydl_opts = {
|
||||
'quiet': False,
|
||||
@@ -781,20 +863,11 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
|
||||
self.url_input.setText(clipboard.text())
|
||||
|
||||
def update_ytdlp(self):
|
||||
# Check if we're running in a PyInstaller package
|
||||
if getattr(sys, 'frozen', False):
|
||||
# Running from PyInstaller package, show info message
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setIcon(QMessageBox.Icon.Information)
|
||||
msg_box.setWindowTitle("Feature Unavailable")
|
||||
msg_box.setText("This feature is available only on the Python (PyPI) version.")
|
||||
msg_box.setInformativeText("To update yt-dlp in a packaged version, please download a new version of the application.")
|
||||
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||
msg_box.exec()
|
||||
else:
|
||||
# Running from source code, show the normal update dialog
|
||||
"""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.exec()
|
||||
dialog.setModal(False) # Make it non-modal
|
||||
dialog.show() # Use show() instead of exec() to avoid blocking
|
||||
|
||||
def show_download_settings_dialog(self): # Renamed method
|
||||
dialog = DownloadSettingsDialog(
|
||||
@@ -963,6 +1036,9 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
|
||||
else:
|
||||
self.status_label.setText("✅ Download completed!")
|
||||
|
||||
# Play notification sound when download completes
|
||||
self.play_notification_sound()
|
||||
|
||||
def download_error(self, error_message):
|
||||
self.toggle_download_controls(True)
|
||||
self.pause_btn.setVisible(False)
|
||||
@@ -1010,8 +1086,8 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
|
||||
def show_update_dialog(self, latest_version, release_url, changelog): # Added changelog parameter
|
||||
msg = QDialog(self)
|
||||
msg.setWindowTitle("Update Available")
|
||||
msg.setMinimumWidth(500) # Increased width for changelog
|
||||
msg.setMinimumHeight(400) # Added min height
|
||||
msg.setMinimumWidth(600) # Increased width for better layout
|
||||
msg.setMinimumHeight(450) # Increased height for better spacing
|
||||
|
||||
# Set custom icon directly
|
||||
icon_path = os.path.join(os.path.dirname(__file__), 'Icon', 'icon.png')
|
||||
@@ -1022,19 +1098,47 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
|
||||
msg.setWindowIcon(self.windowIcon())
|
||||
|
||||
layout = QVBoxLayout(msg)
|
||||
layout.setSpacing(10) # Added spacing
|
||||
layout.setSpacing(15) # Increased spacing for better layout
|
||||
layout.setContentsMargins(20, 20, 20, 20) # Added margins
|
||||
|
||||
# Update message
|
||||
# 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("<h2 style='color: #c90000; margin: 0;'>Update Available</h2>")
|
||||
header_layout.addWidget(title_label)
|
||||
header_layout.addStretch()
|
||||
|
||||
layout.addLayout(header_layout)
|
||||
|
||||
# Update message with better formatting
|
||||
message_label = QLabel(
|
||||
f"<b>A new version of YTSage is available!</b>\n\n"
|
||||
f"Current version: {self.version}\n"
|
||||
f"Latest version: {latest_version}"
|
||||
f"<div style='font-size: 13px; line-height: 1.4;'>"
|
||||
f"<b style='color: #ffffff;'>A new version of YTSage is available!</b><br><br>"
|
||||
f"<span style='color: #cccccc;'>Current version: <b style='color: #ffffff;'>{self.version}</b></span><br>"
|
||||
f"<span style='color: #cccccc;'>Latest version: <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("<b>Changelog:</b>")
|
||||
changelog_label = QLabel("<b style='color: #ffffff; font-size: 14px;'>Changelog:</b>")
|
||||
changelog_label.setStyleSheet("padding: 5px 0; margin-top: 10px;")
|
||||
layout.addWidget(changelog_label)
|
||||
|
||||
changelog_text = QTextEdit()
|
||||
@@ -1049,52 +1153,95 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
|
||||
|
||||
changelog_text.setStyleSheet("""
|
||||
QTextEdit {
|
||||
background-color: #1e1e1e;
|
||||
border: 1px solid #3d3d3d;
|
||||
border-radius: 4px;
|
||||
color: #cccccc;
|
||||
padding: 5px;
|
||||
font-family: Consolas, monospace;
|
||||
font-size: 11px; /* Slightly smaller font */
|
||||
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(200) # Limit height
|
||||
changelog_text.setMaximumHeight(180) # Limit height
|
||||
layout.addWidget(changelog_text)
|
||||
|
||||
# Buttons
|
||||
# Buttons with better styling
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(10)
|
||||
|
||||
download_btn = QPushButton("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("Remind Me 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
|
||||
# Style the dialog with improved theme matching
|
||||
msg.setStyleSheet("""
|
||||
QDialog {
|
||||
background-color: #2b2b2b;
|
||||
background-color: #15181b;
|
||||
border: 1px solid #3d3d3d;
|
||||
border-radius: 8px;
|
||||
}
|
||||
QLabel {
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
padding: 5px 0; /* Adjusted padding */
|
||||
}
|
||||
QPushButton {
|
||||
padding: 8px 15px;
|
||||
background-color: #ff0000;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
min-width: 120px; /* Adjusted min-width */
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #cc0000;
|
||||
}
|
||||
""")
|
||||
|
||||
@@ -1103,6 +1250,75 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
|
||||
def open_release_page(self, url):
|
||||
webbrowser.open(url)
|
||||
|
||||
def check_auto_update_ytdlp(self):
|
||||
"""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():
|
||||
print("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
|
||||
from PySide6.QtCore import QTimer
|
||||
QTimer.singleShot(2000, self._perform_auto_update) # Delay 2 seconds after startup
|
||||
except Exception as e:
|
||||
print(f"Error in auto-update check: {e}")
|
||||
|
||||
def _perform_auto_update(self):
|
||||
"""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
|
||||
from ytsage_gui_dialogs import AutoUpdateThread
|
||||
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:
|
||||
print(f"Error starting auto-update thread: {e}")
|
||||
|
||||
def _on_auto_update_finished(self, success, message):
|
||||
"""Handle auto-update completion."""
|
||||
if success:
|
||||
print(f"Auto-update completed successfully: {message}")
|
||||
else:
|
||||
print(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):
|
||||
"""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():
|
||||
print("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
|
||||
print("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():
|
||||
print("Canceling running download...")
|
||||
self.current_download.cancel()
|
||||
if not self.current_download.wait(3000): # Wait up to 3 seconds for graceful shutdown
|
||||
print("Force terminating download thread...")
|
||||
self.current_download.terminate()
|
||||
self.current_download.wait(1000) # Wait for termination
|
||||
|
||||
print("Application closing...")
|
||||
event.accept()
|
||||
except Exception as e:
|
||||
print(f"Error during application close: {e}")
|
||||
event.accept() # Accept the close event anyway
|
||||
|
||||
def show_custom_options(self):
|
||||
dialog = CustomOptionsDialog(self)
|
||||
if dialog.exec():
|
||||
@@ -1328,3 +1544,194 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from m
|
||||
self.force_keyframes = False
|
||||
self.time_range_btn.setStyleSheet("")
|
||||
self.time_range_btn.setToolTip("")
|
||||
|
||||
def show_ytdlp_setup_dialog(self):
|
||||
"""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.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):
|
||||
"""Analyze URL using yt-dlp executable when Python module is not available"""
|
||||
import subprocess
|
||||
import json
|
||||
import tempfile
|
||||
|
||||
try:
|
||||
yt_dlp_path = get_yt_dlp_path()
|
||||
if not yt_dlp_path:
|
||||
raise Exception("yt-dlp executable not found. Please install yt-dlp first.")
|
||||
|
||||
self.signals.update_status.emit("Analyzing (30%)... Extracting info with yt-dlp executable")
|
||||
|
||||
# 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', self.cookie_file_path])
|
||||
|
||||
# Execute command with hidden console window on Windows
|
||||
import sys
|
||||
if sys.platform == 'win32':
|
||||
# Hide the console window on Windows
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
startupinfo.wShowWindow = subprocess.SW_HIDE
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, startupinfo=startupinfo)
|
||||
else:
|
||||
# For other platforms, use normal subprocess call
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
||||
|
||||
if result.returncode != 0:
|
||||
raise Exception(f"yt-dlp failed: {result.stderr}")
|
||||
|
||||
# Parse JSON output - yt-dlp outputs one JSON object per line for playlists
|
||||
json_lines = [line.strip() for line in result.stdout.strip().split('\n') if line.strip()]
|
||||
|
||||
if not json_lines:
|
||||
raise Exception("No data returned from yt-dlp")
|
||||
|
||||
# Parse first JSON object to determine if it's a playlist
|
||||
first_info = json.loads(json_lines[0])
|
||||
|
||||
self.signals.update_status.emit("Analyzing (60%)... 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:
|
||||
raise Exception("Playlist contains no valid videos.")
|
||||
|
||||
# Use first video for format information
|
||||
self.video_info = self.playlist_entries[0]
|
||||
|
||||
# Update playlist info label
|
||||
playlist_text = (f"Playlist: {first_info.get('title', 'Unknown Playlist')} | "
|
||||
f"{len(self.playlist_entries)} videos")
|
||||
QMetaObject.invokeMethod(
|
||||
self.playlist_info_label, "setText", Qt.ConnectionType.QueuedConnection,
|
||||
Q_ARG(str, playlist_text)
|
||||
)
|
||||
QMetaObject.invokeMethod(
|
||||
self.playlist_info_label, "setVisible", Qt.ConnectionType.QueuedConnection,
|
||||
Q_ARG(bool, True)
|
||||
)
|
||||
|
||||
# Show playlist selection button
|
||||
QMetaObject.invokeMethod(
|
||||
self, 'update_playlist_button_text', Qt.ConnectionType.QueuedConnection,
|
||||
Q_ARG(str, "Select Videos... (All selected)")
|
||||
)
|
||||
QMetaObject.invokeMethod(
|
||||
self.playlist_select_btn, "setVisible", Qt.ConnectionType.QueuedConnection,
|
||||
Q_ARG(bool, 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
|
||||
QMetaObject.invokeMethod(
|
||||
self.playlist_info_label, "setVisible", Qt.ConnectionType.QueuedConnection,
|
||||
Q_ARG(bool, False)
|
||||
)
|
||||
QMetaObject.invokeMethod(
|
||||
self.playlist_select_btn, "setVisible", Qt.ConnectionType.QueuedConnection,
|
||||
Q_ARG(bool, False)
|
||||
)
|
||||
|
||||
# Verify we have format information
|
||||
if not self.video_info or 'formats' not in self.video_info:
|
||||
raise Exception("No format information available")
|
||||
|
||||
self.signals.update_status.emit("Analyzing (75%)... Processing formats")
|
||||
self.all_formats = self.video_info['formats']
|
||||
|
||||
# Update UI
|
||||
self.update_video_info(self.video_info)
|
||||
|
||||
# Update thumbnail
|
||||
self.signals.update_status.emit("Analyzing (85%)... Loading thumbnail")
|
||||
thumbnail_url = None
|
||||
if self.is_playlist:
|
||||
thumbnail_url = self.playlist_info.get('thumbnail')
|
||||
|
||||
if not thumbnail_url:
|
||||
thumbnail_url = self.video_info.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())
|
||||
|
||||
# Handle subtitles
|
||||
self.signals.update_status.emit("Analyzing (90%)... Processing subtitles")
|
||||
self.selected_subtitles = []
|
||||
self.available_subtitles = self.video_info.get('subtitles', {})
|
||||
self.available_automatic_subtitles = self.video_info.get('automatic_captions', {})
|
||||
|
||||
# Update subtitle UI
|
||||
QMetaObject.invokeMethod(self.selected_subs_label, "setText", Qt.ConnectionType.QueuedConnection, Q_ARG(str, "0 selected"))
|
||||
|
||||
# Update format table
|
||||
self.signals.update_status.emit("Analyzing (95%)... Updating format table")
|
||||
self.video_button.setChecked(True)
|
||||
self.audio_button.setChecked(False)
|
||||
self.filter_formats()
|
||||
|
||||
self.signals.update_status.emit("Analysis complete!")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
raise Exception("Analysis timed out. Please try again.")
|
||||
except json.JSONDecodeError as e:
|
||||
raise Exception(f"Failed to parse yt-dlp output: {str(e)}")
|
||||
except Exception as e:
|
||||
raise Exception(f"Analysis failed: {str(e)}")
|
||||
|
||||
Reference in New Issue
Block a user