diff --git a/YTSage.py b/YTSage.py index eb4fba2..fe10d9f 100644 --- a/YTSage.py +++ b/YTSage.py @@ -1,10 +1,11 @@ import sys import os import threading +import time from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, QProgressBar, QLabel, QFileDialog, - QHeaderView, QStyle, QStyleFactory, QComboBox) + QHeaderView, QStyle, QStyleFactory, QComboBox, QTextEdit, QDialog, QPlainTextEdit) from PyQt6.QtCore import Qt, pyqtSignal, QObject from PyQt6.QtGui import QIcon, QPalette, QColor, QPixmap import yt_dlp @@ -13,17 +14,258 @@ from io import BytesIO from PIL import Image from urllib.request import urlopen from datetime import datetime +import json +from pathlib import Path +from packaging import version +import subprocess +import webbrowser class SignalManager(QObject): update_formats = pyqtSignal(list) update_status = pyqtSignal(str) update_progress = pyqtSignal(float) -class YouTubeDownloaderGUI(QMainWindow): +class LogWindow(QDialog): + def __init__(self, parent=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): + self.log_text.append(message) + # Auto-scroll to bottom + scrollbar = self.log_text.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + +class CustomCommandDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.parent = 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: #363636; + color: #ffffff; + border: 2px solid #3d3d3d; + border-radius: 4px; + padding: 8px; + font-family: Consolas, monospace; + } + """) + layout.addWidget(self.command_input) + + # 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: #2b2b2b; + color: #ffffff; + border: 2px solid #3d3d3d; + border-radius: 4px; + padding: 8px; + font-family: Consolas, monospace; + font-size: 12px; + } + """) + layout.addWidget(self.log_output) + + self.setStyleSheet(""" + QDialog { + background-color: #2b2b2b; + } + QPushButton { + padding: 8px 15px; + background-color: #ff0000; + border: none; + border-radius: 4px; + color: white; + font-weight: bold; + } + QPushButton:hover { + background-color: #cc0000; + } + """) + + def run_custom_command(self): + url = self.parent.url_input.text().strip() + if not url: + self.log_output.append("Error: No URL provided") + return + + command = self.command_input.toPlainText().strip() + path = self.parent.path_input.text().strip() + + 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): + 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, + } + + # 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 FFmpegCheckDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle('FFmpeg Required') + self.setMinimumWidth(400) + + layout = QVBoxLayout(self) + + # Message + message_label = QLabel( + "FFmpeg is not installed. Please install it.\n" + "To install FFmpeg, click the button below." + ) + message_label.setWordWrap(True) + layout.addWidget(message_label) + + # Install button + install_btn = QPushButton("Install FFmpeg") + install_btn.clicked.connect(lambda: webbrowser.open('https://github.com/oop7/ffmpeg-install-guide')) + layout.addWidget(install_btn) + + # Close button + close_btn = QPushButton("Close") + close_btn.clicked.connect(self.close) + layout.addWidget(close_btn) + + # Style the dialog + self.setStyleSheet(""" + QDialog { + background-color: #2b2b2b; + } + QLabel { + color: #ffffff; + font-size: 12px; + padding: 10px; + } + QPushButton { + padding: 8px 15px; + background-color: #ff0000; + border: none; + border-radius: 4px; + color: white; + font-weight: bold; + } + QPushButton:hover { + background-color: #cc0000; + } + """) + +class YTSage(QMainWindow): def __init__(self): super().__init__() + # Check for FFmpeg before proceeding + if not self.check_ffmpeg(): + self.show_ffmpeg_dialog() + + self.version = "2.0.0" + self.check_for_updates() # Check for updates on startup + self.config_file = Path.home() / '.ytsage_config.json' + self.load_saved_path() self.setWindowIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowDown)) self.signals = SignalManager() + self.download_paused = False + self.current_download = None + self.log_window = None + self.show_log = False + self.download_cancelled = False self.init_ui() self.setStyleSheet(""" QMainWindow { @@ -94,11 +336,127 @@ class YouTubeDownloaderGUI(QMainWindow): QPushButton.filter-btn:checked:hover { background-color: #cc0000; } + /* Modern Scrollbar Styling */ + QScrollBar:vertical { + border: none; + background: #2b2b2b; + 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: #2b2b2b; + 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: #2b2b2b; + 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: #2b2b2b; + 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: #2b2b2b; + 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: #2b2b2b; + 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) + def load_saved_path(self): + try: + if self.config_file.exists(): + with open(self.config_file, 'r') as f: + config = json.load(f) + saved_path = config.get('download_path', '') + if os.path.exists(saved_path): + self.last_path = saved_path + else: + self.last_path = str(Path.home() / 'Downloads') + else: + self.last_path = str(Path.home() / 'Downloads') + except Exception as e: + print(f"Error loading saved path: {e}") + self.last_path = str(Path.home() / 'Downloads') + + def save_path(self, path): + try: + config = {'download_path': path} + with open(self.config_file, 'w') as f: + json.dump(config, f) + except Exception as e: + print(f"Error saving path: {e}") + def init_ui(self): - self.setWindowTitle('YT-GUI Downloader') + self.setWindowTitle('YTSage v2.0.0') self.setMinimumSize(900, 600) # Main widget and layout @@ -163,6 +521,12 @@ class YouTubeDownloaderGUI(QMainWindow): media_info_layout.addLayout(video_info_layout) layout.addLayout(media_info_layout) + # Add playlist information section + self.playlist_info_label = QLabel() + self.playlist_info_label.setVisible(False) + self.playlist_info_label.setStyleSheet("font-size: 12px; color: #ff9900;") + layout.addWidget(self.playlist_info_label) + # Simplified format filter buttons filter_layout = QHBoxLayout() self.filter_label = QLabel("Show formats:") @@ -198,22 +562,56 @@ class YouTubeDownloaderGUI(QMainWindow): download_layout = QHBoxLayout() self.path_input = QLineEdit() self.path_input.setPlaceholderText('Download path...') + self.path_input.setText(self.last_path) self.browse_btn = QPushButton('Browse') self.browse_btn.clicked.connect(self.browse_path) + + # Create download control buttons self.download_btn = QPushButton('Download') self.download_btn.clicked.connect(self.start_download) + + self.pause_btn = QPushButton('Pause') + self.pause_btn.clicked.connect(self.toggle_pause) + self.pause_btn.setVisible(False) + + # Add cancel button + self.cancel_btn = QPushButton('Cancel') + self.cancel_btn.clicked.connect(self.cancel_download) + self.cancel_btn.setVisible(False) + + # Add log button + self.log_btn = QPushButton('yt-dlp Log') + self.log_btn.setCheckable(True) + self.log_btn.clicked.connect(self.toggle_log_window) + download_layout.addWidget(self.log_btn) + + # Add custom command button to download_layout + self.custom_cmd_btn = QPushButton('Custom Command') + self.custom_cmd_btn.clicked.connect(self.show_custom_command) + download_layout.addWidget(self.custom_cmd_btn) + download_layout.addWidget(self.path_input) download_layout.addWidget(self.browse_btn) download_layout.addWidget(self.download_btn) + download_layout.addWidget(self.pause_btn) + download_layout.addWidget(self.cancel_btn) layout.addLayout(download_layout) # Progress section + progress_layout = QVBoxLayout() self.progress_bar = QProgressBar() - layout.addWidget(self.progress_bar) + progress_layout.addWidget(self.progress_bar) + + # Add download details label + self.download_details_label = QLabel() + self.download_details_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + progress_layout.addWidget(self.download_details_label) self.status_label = QLabel('Ready') self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - layout.addWidget(self.status_label) + progress_layout.addWidget(self.status_label) + + layout.addLayout(progress_layout) # Connect signals self.signals.update_formats.connect(self.update_format_table) @@ -221,51 +619,115 @@ class YouTubeDownloaderGUI(QMainWindow): self.signals.update_progress.connect(self.update_progress_bar) def analyze_url(self): - url = self.url_input.text() - if url: - self.signals.update_status.emit("Analyzing...") - threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start() + url = self.url_input.text().strip() + if not url: + self.signals.update_status.emit("Invalid URL or please enter a URL.") + return + + self.signals.update_status.emit("Analyzing...") + threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start() def _analyze_url_thread(self, url): try: + # Clean up the URL to handle both playlist and video URLs + if 'list=' in url and 'watch?v=' in url: + # Extract playlist ID + playlist_id = url.split('list=')[1].split('&')[0] + url = f'https://www.youtube.com/playlist?list={playlist_id}' + + # Initial extraction with basic options ydl_opts = { - 'format': 'best', 'quiet': True, 'no_warnings': True, - 'extract_flat': False, - 'writesubtitles': True, - 'allsubtitles': True, - 'writeautomaticsub': True, - 'format_sort': ['res', 'ext', 'size', 'br', 'proto'], + 'extract_flat': True, + 'force_generic_extractor': False, + 'ignoreerrors': True, + 'no_color': True } with yt_dlp.YoutubeDL(ydl_opts) as ydl: - info = ydl.extract_info(url, download=False) - - # Store all formats and video info - self.all_formats = info['formats'] - self.video_info = info - - # Update UI with video information - self.signals.update_status.emit("Updating video information...") - self.update_video_info(info) - - # Download and display thumbnail - thumbnail_url = info.get('thumbnail') - if thumbnail_url: - self.download_thumbnail(thumbnail_url) + try: + basic_info = ydl.extract_info(url, download=False) + except Exception as e: + print(f"First extraction failed: {str(e)}") + raise Exception("Could not extract video information") - # Update available subtitles (including auto-generated) - self.available_subtitles = info.get('subtitles', {}) - self.available_automatic_subtitles = info.get('automatic_captions', {}) - self.update_subtitle_list() + # Configure options for detailed extraction + ydl_opts.update({ + 'extract_flat': False, + 'format': 'best', # Start with best format to ensure we get some formats + 'writesubtitles': True, + 'allsubtitles': True, + 'writeautomaticsub': True, + 'playliststart': 1, + 'playlistend': 1 # Only get first video initially + }) - # Update table with filtered formats (default to video) - self.video_btn.setChecked(True) - self.audio_btn.setChecked(False) - self.filter_formats() - - self.signals.update_status.emit("Analysis complete") + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + try: + if basic_info.get('_type') == 'playlist': + self.is_playlist = True + self.playlist_info = basic_info + + # Get the first video URL from the playlist + first_video = None + for entry in basic_info['entries']: + if entry: + first_video = entry + break + + if not first_video: + raise Exception("No valid videos found in playlist") + + # Get the video URL + video_url = first_video.get('url') or first_video.get('webpage_url') + if not video_url: + raise Exception("Could not extract video URL from playlist") + + # Extract detailed information for the first video + self.video_info = ydl.extract_info(video_url, download=False) + + # Update playlist info + playlist_text = f"Playlist: {basic_info.get('title', 'Unknown')} | {len(basic_info['entries'])} videos" + self.playlist_info_label.setText(playlist_text) + self.playlist_info_label.setVisible(True) + else: + self.is_playlist = False + self.video_info = ydl.extract_info(url, download=False) + self.playlist_info_label.setVisible(False) + + # Verify we have format information + if not self.video_info or 'formats' not in self.video_info: + print(f"Debug - video_info keys: {self.video_info.keys() if self.video_info else 'None'}") + raise Exception("No format information available") + + self.all_formats = self.video_info['formats'] + + # Update UI + self.signals.update_status.emit("Updating video information...") + self.update_video_info(self.video_info) + + # Update thumbnail + thumbnail_url = self.video_info.get('thumbnail') + if thumbnail_url: + self.download_thumbnail(thumbnail_url) + + # Update subtitles + self.available_subtitles = self.video_info.get('subtitles', {}) + self.available_automatic_subtitles = self.video_info.get('automatic_captions', {}) + self.update_subtitle_list() + + # Update format table + self.video_btn.setChecked(True) + self.audio_btn.setChecked(False) + self.filter_formats() + + self.signals.update_status.emit("Analysis complete") + + except Exception as e: + print(f"Detailed extraction failed: {str(e)}") + raise Exception(f"Failed to extract video details: {str(e)}") + except Exception as e: error_message = str(e) print(f"Error in analysis: {error_message}") @@ -410,13 +872,21 @@ class YouTubeDownloaderGUI(QMainWindow): self.format_table.setItem(row, 5, audio_item) def browse_path(self): - path = QFileDialog.getExistingDirectory(self, "Select Download Directory") + path = QFileDialog.getExistingDirectory(self, "Select Download Directory", self.last_path) if path: self.path_input.setText(path) + self.save_path(path) + self.last_path = path def start_download(self): url = self.url_input.text() - path = self.path_input.text() + path = self.path_input.text().strip() + + # Path validation + if not path: + self.signals.update_status.emit("Invalid path or please enter a path.") + return + selected_items = self.format_table.selectedItems() if not selected_items: self.signals.update_status.emit("Please select a format") @@ -433,56 +903,132 @@ class YouTubeDownloaderGUI(QMainWindow): download_subs = self.subtitle_check.isChecked() selected_sub = self.subtitle_combo.currentText().split(' - ')[0] if download_subs else None + self.download_paused = False + self.download_cancelled = False + self.pause_btn.setText('Pause') + self.download_btn.setEnabled(False) + self.pause_btn.setVisible(True) + self.cancel_btn.setVisible(True) + threading.Thread(target=self._download_thread, - args=(url, path, format_id, selected_sub), + args=(url, path, format_id, selected_sub, self.is_playlist), daemon=True).start() - def _download_thread(self, url, path, format_id, subtitle_lang=None): - ydl_opts = { - 'format': format_id, - 'outtmpl': os.path.join(path, '%(title)s.%(ext)s'), - 'progress_hooks': [self._progress_hook], - 'merge_output_format': 'mp4', - } - - # Add subtitle options if requested - if subtitle_lang: - # Extract language code from the combo box text - lang_code = subtitle_lang.split(' - ')[0] - is_auto = 'Auto-generated' in subtitle_lang - - ydl_opts.update({ - 'writesubtitles': True, - 'subtitleslangs': [lang_code], - 'writeautomaticsub': True, - 'skip_manual_subs': is_auto, # Skip manual subs if auto is selected - 'skip_auto_subs': not is_auto, # Skip auto subs if manual is selected - }) - + def _download_thread(self, url, path, format_id, subtitle_lang=None, is_playlist=False): try: + class DebugLogger: + def debug(self, msg): + # Update both status and log window + if any(x in msg.lower() for x in ['downloading webpage', 'downloading api', 'extracting', 'downloading m3u8']): + self.signals.update_status.emit("Preparing for download...") + self.signals.update_progress.emit(0) + if self.parent.show_log and self.parent.log_window: + self.parent.log_window.append_log(msg) + + def warning(self, msg): + if self.parent.show_log and self.parent.log_window: + self.parent.log_window.append_log(f"Warning: {msg}") + + def error(self, msg): + if self.parent.show_log and self.parent.log_window: + self.parent.log_window.append_log(f"Error: {msg}") + + def __init__(self, signals, parent): + self.signals = signals + self.parent = parent + + class CancelHandler: + def __init__(self, parent): + self.parent = parent + + def __call__(self, signum, frame): + return self.parent.download_cancelled + + ydl_opts = { + 'format': format_id, + 'outtmpl': os.path.join(path, '%(playlist_title)s/%(title)s.%(ext)s' if is_playlist else '%(title)s.%(ext)s'), + 'progress_hooks': [self._progress_hook], + 'merge_output_format': 'mp4', + 'debug_printout': True, + 'logger': DebugLogger(self.signals, self), + 'signal_handler': CancelHandler(self), + } + + # Add subtitle options if requested + if subtitle_lang: + lang_code = subtitle_lang.split(' - ')[0] + is_auto = 'Auto-generated' in subtitle_lang + ydl_opts.update({ + 'writesubtitles': True, + 'subtitleslangs': [lang_code], + 'writeautomaticsub': True, + 'skip_manual_subs': is_auto, + 'skip_auto_subs': not is_auto, + }) + + self.signals.update_status.emit("Preparing for download...") + self.signals.update_progress.emit(0) + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + self.current_download = ydl ydl.download([url]) + except Exception as e: - error_message = str(e) - self.signals.update_status.emit(f"Error: {error_message}") + if self.download_cancelled: + self.signals.update_status.emit("Download cancelled") + else: + error_message = str(e) + self.signals.update_status.emit(f"Error: {error_message}") + finally: + self.current_download = None + self.download_btn.setEnabled(True) + self.pause_btn.setVisible(False) + self.cancel_btn.setVisible(False) def _progress_hook(self, d): + if self.download_cancelled: + raise Exception("Download cancelled by user") + if d['status'] == 'downloading': + while self.download_paused and not self.download_cancelled: + time.sleep(0.1) + continue + try: downloaded_bytes = int(d.get('downloaded_bytes', 0)) - total_bytes = int(d.get('total_bytes', 1)) # Use 1 as default to avoid division by zero - # Ensure we're working with integers throughout + total_bytes = int(d.get('total_bytes', 1)) progress = int((downloaded_bytes * 100) // total_bytes) - # Clamp the value between 0 and 100 progress = max(0, min(100, progress)) + + # Extract additional download information + speed = d.get('speed', 0) + eta = d.get('eta', 0) + filename = os.path.basename(d.get('filename', '')) + + # Format the speed dynamically + if speed: + if speed > 1024 * 1024: # If speed is greater than 1 MiB/s + speed_str = f"{speed/(1024*1024):.1f} MiB/s" + else: + speed_str = f"{speed/1024:.1f} KiB/s" + else: + speed_str = "N/A" + + eta_str = f"{eta//60}:{eta%60:02d}" if eta else "N/A" + + # Update the UI + details_text = f"Speed: {speed_str} | ETA: {eta_str} | File: {filename}" + self.signals.update_status.emit(details_text) self.signals.update_progress.emit(progress) - self.signals.update_status.emit(f"Downloading: {progress}%") + except Exception as e: print(f"Progress calculation error: {str(e)}") self.signals.update_status.emit("Downloading...") + elif d['status'] == 'finished': self.signals.update_progress.emit(100) self.signals.update_status.emit("Download completed!") + self.download_details_label.setText("") # Clear the details when finished def update_progress_bar(self, value): try: @@ -492,10 +1038,130 @@ class YouTubeDownloaderGUI(QMainWindow): except Exception as e: print(f"Progress bar update error: {str(e)}") + def toggle_pause(self): + if self.current_download: + self.download_paused = not self.download_paused + if self.download_paused: + self.pause_btn.setText('Resume') + self.signals.update_status.emit("Download paused") + else: + self.pause_btn.setText('Pause') + self.signals.update_status.emit("Download resumed") + + def toggle_log_window(self): + if self.log_btn.isChecked(): + if not self.log_window: + self.log_window = LogWindow(self) + self.log_window.show() + self.show_log = True + else: + if self.log_window: + self.log_window.hide() + self.show_log = False + + def check_for_updates(self): + 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): + self.show_update_dialog(latest_version, latest_release["html_url"]) + except Exception as e: + print(f"Failed to check for updates: {str(e)}") + + def show_update_dialog(self, latest_version, release_url): + msg = QDialog(self) + msg.setWindowTitle("Update Available") + msg.setMinimumWidth(400) + + layout = QVBoxLayout(msg) + + # Update message + message_label = QLabel( + f"A new version of YTSage is available!\n\n" + f"Current version: {self.version}\n" + f"Latest version: {latest_version}" + ) + message_label.setWordWrap(True) + layout.addWidget(message_label) + + # Buttons + button_layout = QHBoxLayout() + + download_btn = QPushButton("Download Update") + download_btn.clicked.connect(lambda: self.open_release_page(release_url)) + + remind_btn = QPushButton("Remind Me Later") + remind_btn.clicked.connect(msg.close) + + button_layout.addWidget(download_btn) + button_layout.addWidget(remind_btn) + layout.addLayout(button_layout) + + # Style the dialog + msg.setStyleSheet(""" + QDialog { + background-color: #2b2b2b; + } + QLabel { + color: #ffffff; + font-size: 12px; + padding: 10px; + } + QPushButton { + padding: 8px 15px; + background-color: #ff0000; + border: none; + border-radius: 4px; + color: white; + font-weight: bold; + } + QPushButton:hover { + background-color: #cc0000; + } + """) + + msg.show() + + def open_release_page(self, url): + import webbrowser + webbrowser.open(url) + + def show_custom_command(self): + dialog = CustomCommandDialog(self) + dialog.exec() + + def cancel_download(self): + self.download_cancelled = True + if self.current_download: + self.signals.update_status.emit("Cancelling download...") + + def check_ffmpeg(self): + try: + subprocess.run(['ffmpeg', '-version'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=True) + return True + except (subprocess.SubprocessError, FileNotFoundError): + return False + + def show_ffmpeg_dialog(self): + dialog = FFmpegCheckDialog(self) + dialog.exec() + def main(): app = QApplication(sys.argv) app.setStyle(QStyleFactory.create('Fusion')) - window = YouTubeDownloaderGUI() + window = YTSage() window.show() sys.exit(app.exec())