Files
SageTube/ytsage_gui_dialogs.py
T
Muhamed 985ba9d864 Revert "4v.5.0"
This reverts commit 82de19c95a.
2025-04-29 15:03:52 +03:00

673 lines
24 KiB
Python

import sys
import os
import webbrowser
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLineEdit, QPushButton, QTableWidget,
QTableWidgetItem, QProgressBar, QLabel, QFileDialog,
QHeaderView, QStyle, QStyleFactory, QComboBox, QTextEdit, QDialog, QPlainTextEdit, QCheckBox, QButtonGroup)
from PySide6.QtCore import Qt, Signal, QObject, QThread, QProcess
from PySide6.QtGui import QIcon, QPalette, QColor, QPixmap
import requests
from io import BytesIO
from PIL import Image
from datetime import datetime
import json
from pathlib import Path
from packaging import version
import subprocess
import re
import yt_dlp
from ytsage_ffmpeg import auto_install_ffmpeg, check_ffmpeg_installed
from ytsage_utils import check_ffmpeg, get_yt_dlp_path, load_saved_path, save_path # Import utility functions
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)
# Add SponsorBlock checkbox
self.sponsorblock_checkbox = QCheckBox("Remove Sponsor Segments")
self.sponsorblock_checkbox.setStyleSheet("""
QCheckBox {
color: #ffffff;
padding: 5px;
margin-left: 20px;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border-radius: 9px; /* Make indicator round */
}
QCheckBox::indicator:unchecked {
border: 2px solid #666666;
background: #2b2b2b;
border-radius: 9px; /* Make indicator round */
}
QCheckBox::indicator:checked {
border: 2px solid #ff0000;
background: #ff0000;
border-radius: 9px; /* Make indicator round */
}
""")
layout.insertWidget(layout.indexOf(self.command_input), self.sponsorblock_checkbox)
# Buttons
button_layout = QHBoxLayout()
self.run_btn = QPushButton("Run Command")
self.run_btn.clicked.connect(self.run_custom_command)
self.close_btn = QPushButton("Close")
self.close_btn.clicked.connect(self.close)
button_layout.addWidget(self.run_btn)
button_layout.addWidget(self.close_btn)
layout.addLayout(button_layout)
# Log output
self.log_output = QTextEdit()
self.log_output.setReadOnly(True)
self.log_output.setStyleSheet("""
QTextEdit {
background-color: #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
import threading
threading.Thread(target=self._run_command_thread,
args=(command, url, path),
daemon=True).start()
def _run_command_thread(self, command, url, path):
try:
class CommandLogger:
def debug(self, msg):
self.dialog.log_output.append(msg)
def warning(self, msg):
self.dialog.log_output.append(f"Warning: {msg}")
def error(self, msg):
self.dialog.log_output.append(f"Error: {msg}")
def __init__(self, dialog):
self.dialog = dialog
# Split command into arguments
args = command.split()
# Base options
ydl_opts = {
'logger': CommandLogger(self),
'paths': {'home': path},
'debug_printout': True,
'postprocessors': []
}
# Add SponsorBlock options if enabled
if self.sponsorblock_checkbox.isChecked():
ydl_opts['postprocessors'].extend([{
'key': 'SponsorBlock',
'categories': ['sponsor', 'selfpromo', 'interaction'],
'api': 'https://sponsor.ajay.app'
}, {
'key': 'ModifyChapters',
'remove_sponsor_segments': ['sponsor', 'selfpromo', 'interaction'],
'sponsorblock_chapter_title': '[SponsorBlock]: %(category_names)l',
'force_keyframes': True
}])
# Add custom arguments
for i in range(0, len(args), 2):
if i + 1 < len(args):
key = args[i].lstrip('-').replace('-', '_')
value = args[i + 1]
try:
# Try to convert to appropriate type
if value.lower() in ('true', 'false'):
value = value.lower() == 'true'
elif value.isdigit():
value = int(value)
ydl_opts[key] = value
except:
ydl_opts[key] = value
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([url])
self.log_output.append("Command completed successfully")
except Exception as e:
self.log_output.append(f"Error: {str(e)}")
finally:
self.run_btn.setEnabled(True)
class FFmpegInstallThread(QThread):
finished = Signal(bool)
progress = Signal(str)
def run(self):
# Redirect stdout to capture progress messages
import sys
from io import StringIO
import contextlib
output = StringIO()
with contextlib.redirect_stdout(output):
success = auto_install_ffmpeg()
# Process captured output and emit progress signals
for line in output.getvalue().splitlines():
self.progress.emit(line)
self.finished.emit(success)
class FFmpegCheckDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle('Installing FFmpeg')
self.setMinimumWidth(450)
self.setMinimumHeight(250)
# Set the window icon to match the main app
self.setWindowIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowDown))
layout = QVBoxLayout(self)
layout.setSpacing(15)
# Header with icon
header_layout = QHBoxLayout()
icon_label = QLabel()
icon_label.setPixmap(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowDown).pixmap(32, 32))
header_layout.addWidget(icon_label)
header_text = QLabel("FFmpeg Installation")
header_text.setStyleSheet("font-size: 16px; font-weight: bold;")
header_layout.addWidget(header_text)
header_layout.addStretch()
layout.addLayout(header_layout)
# Message
self.message_label = QLabel(
"🎥 YTSage needs FFmpeg to process videos.\n"
"Let's set it up for you automatically!"
)
self.message_label.setWordWrap(True)
self.message_label.setStyleSheet("font-size: 13px;")
layout.addWidget(self.message_label)
# Progress label with cool emojis
self.progress_label = QLabel("")
self.progress_label.setWordWrap(True)
self.progress_label.setStyleSheet("""
QLabel {
background-color: #1e1e1e;
border-radius: 5px;
padding: 10px;
font-family: 'Consolas', monospace;
font-size: 12px;
}
""")
self.progress_label.hide()
layout.addWidget(self.progress_label)
# Buttons container
button_layout = QHBoxLayout()
# Install button
self.install_btn = QPushButton("Install FFmpeg")
self.install_btn.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowDown))
self.install_btn.clicked.connect(self.start_installation)
button_layout.addWidget(self.install_btn)
# Manual install button
self.manual_btn = QPushButton("Manual Guide")
self.manual_btn.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_DialogHelpButton))
self.manual_btn.clicked.connect(lambda: webbrowser.open('https://github.com/oop7/ffmpeg-install-guide'))
button_layout.addWidget(self.manual_btn)
# Close button
self.close_btn = QPushButton("Close")
self.close_btn.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_DialogCloseButton))
self.close_btn.clicked.connect(self.close)
button_layout.addWidget(self.close_btn)
layout.addLayout(button_layout)
# Style the dialog
self.setStyleSheet("""
QDialog {
background-color: #2b2b2b;
}
QLabel {
color: #ffffff;
}
QPushButton {
padding: 8px 15px;
background-color: #3d3d3d;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
margin: 5px;
min-width: 100px;
}
QPushButton:hover {
background-color: #4d4d4d;
}
QPushButton:disabled {
background-color: #2d2d2d;
color: #666666;
}
""")
# Initialize installation thread
self.install_thread = None
def start_installation(self):
self.install_btn.setEnabled(False)
self.manual_btn.setEnabled(False)
self.close_btn.setEnabled(False)
# Check if FFmpeg is already installed
if check_ffmpeg_installed():
self.message_label.setText("🎉 FFmpeg is already installed!")
self.progress_label.setText("✅ You can close this dialog and continue using YTSage.")
self.install_btn.hide()
self.manual_btn.hide()
self.close_btn.setEnabled(True)
return
self.message_label.setText("🚀 Installing FFmpeg... Hold tight!")
self.progress_label.show()
self.install_thread = FFmpegInstallThread()
self.install_thread.finished.connect(self.installation_finished)
self.install_thread.progress.connect(self.update_progress)
self.install_thread.start()
def update_progress(self, message):
self.progress_label.setText(message)
def installation_finished(self, success):
if success:
self.message_label.setText("🎉 FFmpeg has been installed successfully!")
self.progress_label.setText("✅ You're all set! You can now close this dialog and continue using YTSage.")
self.install_btn.hide()
self.manual_btn.hide()
else:
self.message_label.setText("❌ Oops! FFmpeg installation encountered an issue.")
self.progress_label.setText("💡 Try using the manual installation guide instead.")
self.install_btn.setEnabled(True)
self.manual_btn.setEnabled(True)
self.close_btn.setEnabled(True)
class YTDLPUpdateDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Update yt-dlp")
self.setMinimumWidth(400)
layout = QVBoxLayout(self)
# Status label
self.status_label = QLabel("Checking for updates...")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.status_label)
# Progress bar
self.progress_bar = QProgressBar()
self.progress_bar.hide() # Hide initially
layout.addWidget(self.progress_bar)
# Buttons
button_layout = QHBoxLayout()
self.update_btn = QPushButton("Update")
self.update_btn.clicked.connect(self.perform_update)
self.update_btn.setEnabled(False)
self.close_btn = QPushButton("Close")
self.close_btn.clicked.connect(self.close)
button_layout.addWidget(self.update_btn)
button_layout.addWidget(self.close_btn)
layout.addLayout(button_layout)
# Style
self.setStyleSheet("""
QDialog {
background-color: #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;
min-width: 100px;
}
QPushButton:disabled {
background-color: #666666;
}
QPushButton:hover {
background-color: #cc0000;
}
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;
}
""")
# Start version check
self.check_version()
def check_version(self):
try:
# Get current version using yt-dlp command
import subprocess
import sys
import os
# Get the yt-dlp executable path
if getattr(sys, 'frozen', False):
if sys.platform == 'win32':
yt_dlp_path = os.path.join(os.path.dirname(sys.executable), 'yt-dlp.exe')
else:
yt_dlp_path = os.path.join(os.path.dirname(sys.executable), 'yt-dlp')
else:
yt_dlp_path = 'yt-dlp'
# Get current version
try:
result = subprocess.run([yt_dlp_path, '--version'],
capture_output=True,
text=True)
current_version = result.stdout.strip()
except Exception:
import yt_dlp
current_version = yt_dlp.version.__version__
# Get latest version from PyPI
import requests
response = requests.get("https://pypi.org/pypi/yt-dlp/json")
latest_version = response.json()["info"]["version"]
# Clean up version strings
current_version = current_version.replace('_', '.')
latest_version = latest_version.replace('_', '.')
# Compare versions
from packaging import version
try:
current_ver = version.parse(current_version)
latest_ver = version.parse(latest_version)
if current_ver < latest_ver:
self.status_label.setText(f"Update available!\nCurrent version: {current_version}\nLatest version: {latest_version}")
self.update_btn.setEnabled(True)
else:
self.status_label.setText(f"yt-dlp is up to date (version {current_version})")
self.update_btn.setEnabled(False)
except version.InvalidVersion:
# If version parsing fails, do a simple string comparison
if current_version != latest_version:
self.status_label.setText(f"Update available!\nCurrent version: {current_version}\nLatest version: {latest_version}")
self.update_btn.setEnabled(True)
else:
self.status_label.setText(f"yt-dlp is up to date (version {current_version})")
self.update_btn.setEnabled(False)
except Exception as e:
self.status_label.setText(f"Error checking version: {str(e)}")
self.update_btn.setEnabled(False)
def perform_update(self):
try:
self.update_btn.setEnabled(False)
self.close_btn.setEnabled(False)
self.status_label.setText("Updating yt-dlp...")
self.progress_bar.setRange(0, 0)
self.progress_bar.show()
# Get the yt-dlp path
if getattr(sys, 'frozen', False):
if sys.platform == 'win32':
yt_dlp_path = os.path.join(os.path.dirname(sys.executable), 'yt-dlp.exe')
else:
yt_dlp_path = os.path.join(os.path.dirname(sys.executable), 'yt-dlp')
else:
yt_dlp_path = 'yt-dlp'
# Create a QProcess for updating
process = QProcess()
process.setWorkingDirectory(os.path.dirname(yt_dlp_path))
if sys.platform == 'win32':
# For Windows, use pip to update
python_path = os.path.join(os.path.dirname(sys.executable), 'python.exe')
if not os.path.exists(python_path):
python_path = sys.executable
process.start(python_path, ['-m', 'pip', 'install', '--upgrade', '--no-cache-dir', 'yt-dlp'])
else:
# For Unix systems
process.start('pip', ['install', '--upgrade', '--no-cache-dir', 'yt-dlp'])
process.waitForFinished()
if process.exitCode() == 0:
self.status_label.setText("Update completed successfully!\nPlease restart the application.")
self.check_version() # Recheck version after update
else:
error = process.readAllStandardError().data().decode()
self.status_label.setText(f"Update failed: {error}")
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(100)
self.close_btn.setEnabled(True)
except Exception as e:
self.status_label.setText(f"Update failed: {str(e)}")
self.close_btn.setEnabled(True)
self.progress_bar.hide()
class AboutDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle('About YTSage')
self.setMinimumWidth(500)
layout = QVBoxLayout(self)
# App title and version
title_label = QLabel("YTSage")
title_label.setStyleSheet("""
font-size: 24px;
font-weight: bold;
color: #ff0000;
padding: 10px;
""")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title_label)
version_label = QLabel(f"Version {parent.version}")
version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(version_label)
# Description
description = QLabel(
"A modern YouTube downloader with a clean interface.\n"
"Download videos in any quality, extract audio, fetch subtitles,\n"
"and view video metadata. Built with yt-dlp for reliable performance."
)
description.setWordWrap(True)
description.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(description)
# Features list
features_label = QLabel(
"Key Features:\n"
"• Smart video quality selection\n"
"• Audio extraction\n"
"• Subtitle support\n"
"• Playlist downloads\n"
"• Real-time progress tracking\n"
"• Custom command support\n"
"• SponsorBlock Integration\n"
"• Save Thumbnail\n"
"• One-click updates"
)
features_label.setStyleSheet("padding: 10px;")
layout.addWidget(features_label)
# Credits
credits_label = QLabel(
"Powered by:\n"
"• yt-dlp\n"
"• PySide6\n"
"• FFmpeg"
)
credits_label.setStyleSheet("padding: 10px;")
layout.addWidget(credits_label)
# GitHub link
github_btn = QPushButton("Visit GitHub Repository")
github_btn.clicked.connect(lambda: webbrowser.open('https://github.com/oop7/YTSage'))
layout.addWidget(github_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;
}
QPushButton {
padding: 8px 15px;
background-color: #ff0000;
border: none;
border-radius: 4px;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #cc0000;
}
""")