Update YTSage.py

This commit is contained in:
Muhamed
2025-01-28 17:28:50 +02:00
parent 6d11720dca
commit 8ad6c3491d
+412 -162
View File
@@ -2,12 +2,12 @@ import sys
import os import os
import threading import threading
import time import time
from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
QHBoxLayout, QLineEdit, QPushButton, QTableWidget, QHBoxLayout, QLineEdit, QPushButton, QTableWidget,
QTableWidgetItem, QProgressBar, QLabel, QFileDialog, QTableWidgetItem, QProgressBar, QLabel, QFileDialog,
QHeaderView, QStyle, QStyleFactory, QComboBox, QTextEdit, QDialog, QPlainTextEdit) QHeaderView, QStyle, QStyleFactory, QComboBox, QTextEdit, QDialog, QPlainTextEdit)
from PyQt6.QtCore import Qt, pyqtSignal, QObject from PySide6.QtCore import Qt, Signal, QObject, QThread
from PyQt6.QtGui import QIcon, QPalette, QColor, QPixmap from PySide6.QtGui import QIcon, QPalette, QColor, QPixmap
import yt_dlp import yt_dlp
import requests import requests
from io import BytesIO from io import BytesIO
@@ -21,9 +21,9 @@ import subprocess
import webbrowser import webbrowser
class SignalManager(QObject): class SignalManager(QObject):
update_formats = pyqtSignal(list) update_formats = Signal(list)
update_status = pyqtSignal(str) update_status = Signal(str)
update_progress = pyqtSignal(float) update_progress = Signal(float)
class LogWindow(QDialog): class LogWindow(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
@@ -248,6 +248,106 @@ class FFmpegCheckDialog(QDialog):
} }
""") """)
class DownloadThread(QThread):
progress_signal = Signal(float)
status_signal = Signal(str)
finished_signal = Signal()
error_signal = Signal(str)
def __init__(self, url, path, format_id, subtitle_lang=None, is_playlist=False):
super().__init__()
self.url = url
self.path = path
self.format_id = format_id
self.subtitle_lang = subtitle_lang
self.is_playlist = is_playlist
self.paused = False
self.cancelled = False
def run(self):
try:
class DebugLogger:
def debug(self, msg):
if any(x in msg.lower() for x in ['downloading webpage', 'downloading api', 'extracting', 'downloading m3u8']):
self.thread.status_signal.emit("Preparing for download...")
self.thread.progress_signal.emit(0)
def warning(self, msg):
self.thread.status_signal.emit(f"Warning: {msg}")
def error(self, msg):
self.thread.status_signal.emit(f"Error: {msg}")
def __init__(self, thread):
self.thread = thread
def progress_hook(d):
if self.cancelled:
raise Exception("Download cancelled by user")
if d['status'] == 'downloading':
while self.paused and not self.cancelled:
time.sleep(0.1)
continue
try:
downloaded_bytes = int(d.get('downloaded_bytes', 0))
total_bytes = int(d.get('total_bytes', 1))
progress = int((downloaded_bytes * 100) // total_bytes)
progress = max(0, min(100, progress))
speed = d.get('speed', 0)
eta = d.get('eta', 0)
filename = os.path.basename(d.get('filename', ''))
if speed:
if speed > 1024 * 1024:
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"
details_text = f"Speed: {speed_str} | ETA: {eta_str} | File: {filename}"
self.status_signal.emit(details_text)
self.progress_signal.emit(progress)
except Exception as e:
self.status_signal.emit("Downloading...")
elif d['status'] == 'finished':
self.progress_signal.emit(100)
self.status_signal.emit("Download completed!")
ydl_opts = {
'format': self.format_id,
'outtmpl': os.path.join(self.path, '%(playlist_title)s/%(title)s.%(ext)s' if self.is_playlist else '%(title)s.%(ext)s'),
'progress_hooks': [progress_hook],
'merge_output_format': 'mp4',
'logger': DebugLogger(self),
}
if self.subtitle_lang:
lang_code = self.subtitle_lang.split(' - ')[0]
is_auto = 'Auto-generated' in self.subtitle_lang
ydl_opts.update({
'writesubtitles': True,
'subtitleslangs': [lang_code],
'writeautomaticsub': True,
'skip_manual_subs': is_auto,
'skip_auto_subs': not is_auto,
})
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([self.url])
self.finished_signal.emit()
except Exception as e:
self.error_signal.emit(str(e))
class YTSage(QMainWindow): class YTSage(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -255,7 +355,7 @@ class YTSage(QMainWindow):
if not self.check_ffmpeg(): if not self.check_ffmpeg():
self.show_ffmpeg_dialog() self.show_ffmpeg_dialog()
self.version = "2.0.0" self.version = "3.0.0"
self.check_for_updates() # Check for updates on startup self.check_for_updates() # Check for updates on startup
self.config_file = Path.home() / '.ytsage_config.json' self.config_file = Path.home() / '.ytsage_config.json'
self.load_saved_path() self.load_saved_path()
@@ -444,19 +544,21 @@ class YTSage(QMainWindow):
else: else:
self.last_path = str(Path.home() / 'Downloads') self.last_path = str(Path.home() / 'Downloads')
except Exception as e: except Exception as e:
print(f"Error loading saved path: {e}") print(f"Error loading saved settings: {e}")
self.last_path = str(Path.home() / 'Downloads') self.last_path = str(Path.home() / 'Downloads')
def save_path(self, path): def save_path(self, path):
try: try:
config = {'download_path': path} config = {
'download_path': path
}
with open(self.config_file, 'w') as f: with open(self.config_file, 'w') as f:
json.dump(config, f) json.dump(config, f)
except Exception as e: except Exception as e:
print(f"Error saving path: {e}") print(f"Error saving settings: {e}")
def init_ui(self): def init_ui(self):
self.setWindowTitle('YTSage v2.0.0') self.setWindowTitle('YTSage v3.0.0')
self.setMinimumSize(900, 600) self.setMinimumSize(900, 600)
# Main widget and layout # Main widget and layout
@@ -470,9 +572,16 @@ class YTSage(QMainWindow):
url_layout = QHBoxLayout() url_layout = QHBoxLayout()
self.url_input = QLineEdit() self.url_input = QLineEdit()
self.url_input.setPlaceholderText('Enter YouTube URL...') self.url_input.setPlaceholderText('Enter YouTube URL...')
# Add Paste URL button
self.paste_btn = QPushButton('Paste URL')
self.paste_btn.clicked.connect(self.paste_url)
self.analyze_btn = QPushButton('Analyze') self.analyze_btn = QPushButton('Analyze')
self.analyze_btn.clicked.connect(self.analyze_url) self.analyze_btn.clicked.connect(self.analyze_url)
url_layout.addWidget(self.url_input) url_layout.addWidget(self.url_input)
url_layout.addWidget(self.paste_btn)
url_layout.addWidget(self.analyze_btn) url_layout.addWidget(self.analyze_btn)
layout.addLayout(url_layout) layout.addLayout(url_layout)
@@ -497,15 +606,59 @@ class YTSage(QMainWindow):
self.date_label = QLabel() self.date_label = QLabel()
self.duration_label = QLabel() self.duration_label = QLabel()
# Subtitle section # Style the info labels
for label in [self.channel_label, self.views_label, self.date_label, self.duration_label]:
label.setStyleSheet("""
QLabel {
color: #cccccc;
font-size: 12px;
padding: 2px 0;
}
""")
# Subtitle section with improved styling
subtitle_layout = QHBoxLayout() subtitle_layout = QHBoxLayout()
self.subtitle_check = QPushButton("Download Subtitles") self.subtitle_check = QPushButton("Download Subtitles")
self.subtitle_check.setCheckable(True) self.subtitle_check.setCheckable(True)
self.subtitle_check.clicked.connect(self.toggle_subtitle_list) self.subtitle_check.clicked.connect(self.toggle_subtitle_list)
self.subtitle_check.setStyleSheet("""
QPushButton {
background-color: #363636;
border: 2px solid #3d3d3d;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:checked {
background-color: #ff0000;
border-color: #cc0000;
}
""")
subtitle_layout.addWidget(self.subtitle_check) subtitle_layout.addWidget(self.subtitle_check)
self.subtitle_combo = QComboBox() self.subtitle_combo = QComboBox()
self.subtitle_combo.setVisible(False) self.subtitle_combo.setVisible(False)
self.subtitle_combo.setStyleSheet("""
QComboBox {
background-color: #363636;
border: 2px solid #3d3d3d;
border-radius: 4px;
padding: 5px;
min-width: 200px;
}
QComboBox::drop-down {
border: none;
}
QComboBox::down-arrow {
image: url(down_arrow.png);
width: 12px;
height: 12px;
}
QComboBox QAbstractItemView {
background-color: #363636;
selection-background-color: #ff0000;
selection-color: white;
}
""")
subtitle_layout.addWidget(self.subtitle_combo) subtitle_layout.addWidget(self.subtitle_combo)
subtitle_layout.addStretch() subtitle_layout.addStretch()
@@ -521,75 +674,150 @@ class YTSage(QMainWindow):
media_info_layout.addLayout(video_info_layout) media_info_layout.addLayout(video_info_layout)
layout.addLayout(media_info_layout) layout.addLayout(media_info_layout)
# Add playlist information section # Add playlist information section with improved styling
self.playlist_info_label = QLabel() self.playlist_info_label = QLabel()
self.playlist_info_label.setVisible(False) self.playlist_info_label.setVisible(False)
self.playlist_info_label.setStyleSheet("font-size: 12px; color: #ff9900;") self.playlist_info_label.setStyleSheet("""
QLabel {
font-size: 12px;
color: #ff9900;
padding: 5px;
background-color: #363636;
border-radius: 4px;
}
""")
layout.addWidget(self.playlist_info_label) layout.addWidget(self.playlist_info_label)
# Simplified format filter buttons # Format filter buttons with improved styling
filter_layout = QHBoxLayout() filter_layout = QHBoxLayout()
self.filter_label = QLabel("Show formats:") self.filter_label = QLabel("Show formats:")
self.filter_label.setStyleSheet("color: #cccccc; font-size: 12px;")
filter_layout.addWidget(self.filter_label) filter_layout.addWidget(self.filter_label)
# Style for filter buttons
filter_btn_style = """
QPushButton {
background-color: #363636;
border: 2px solid #3d3d3d;
border-radius: 4px;
padding: 5px 15px;
color: #cccccc;
}
QPushButton:checked {
background-color: #ff0000;
border-color: #cc0000;
color: white;
}
QPushButton:hover {
background-color: #444444;
}
"""
self.video_btn = QPushButton("Video") self.video_btn = QPushButton("Video")
self.video_btn.setCheckable(True) self.video_btn.setCheckable(True)
self.video_btn.setChecked(True) self.video_btn.setChecked(True)
self.video_btn.clicked.connect(self.filter_formats) self.video_btn.clicked.connect(self.filter_formats)
self.video_btn.setProperty("class", "filter-btn") self.video_btn.setStyleSheet(filter_btn_style)
self.audio_btn = QPushButton("Audio Only") self.audio_btn = QPushButton("Audio Only")
self.audio_btn.setCheckable(True) self.audio_btn.setCheckable(True)
self.audio_btn.clicked.connect(self.filter_formats) self.audio_btn.clicked.connect(self.filter_formats)
self.audio_btn.setProperty("class", "filter-btn") self.audio_btn.setStyleSheet(filter_btn_style)
filter_layout.addWidget(self.video_btn) filter_layout.addWidget(self.video_btn)
filter_layout.addWidget(self.audio_btn) filter_layout.addWidget(self.audio_btn)
filter_layout.addStretch() filter_layout.addStretch()
layout.addLayout(filter_layout) layout.addLayout(filter_layout)
# Format table # Format table with improved styling
self.format_table = QTableWidget() self.format_table = QTableWidget()
self.format_table.setColumnCount(6) self.format_table.setColumnCount(6)
self.format_table.setHorizontalHeaderLabels(['Format ID', 'Extension', 'Resolution', 'File Size', 'Codec', 'Audio']) self.format_table.setHorizontalHeaderLabels(['Format ID', 'Extension', 'Resolution', 'File Size', 'Codec', 'Audio'])
self.format_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self.format_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
self.format_table.setStyleSheet("""
QTableWidget {
background-color: #363636;
border: 2px solid #3d3d3d;
border-radius: 4px;
gridline-color: #3d3d3d;
}
QTableWidget::item {
padding: 5px;
}
QTableWidget::item:selected {
background-color: #ff0000;
}
QHeaderView::section {
background-color: #2b2b2b;
padding: 5px;
border: 1px solid #3d3d3d;
font-weight: bold;
}
""")
layout.addWidget(self.format_table) layout.addWidget(self.format_table)
# Store formats for filtering # Download section with improved styling
self.all_formats = []
# Download section
download_layout = QHBoxLayout() download_layout = QHBoxLayout()
self.path_input = QLineEdit() self.path_input = QLineEdit()
self.path_input.setPlaceholderText('Download path...') self.path_input.setPlaceholderText('Download path...')
self.path_input.setText(self.last_path) self.path_input.setText(self.last_path)
# Style all buttons consistently
button_style = """
QPushButton {
background-color: #ff0000;
border: none;
border-radius: 4px;
padding: 8px 15px;
color: white;
font-weight: bold;
}
QPushButton:hover {
background-color: #cc0000;
}
QPushButton:pressed {
background-color: #990000;
}
QPushButton:disabled {
background-color: #666666;
}
"""
self.browse_btn = QPushButton('Browse') self.browse_btn = QPushButton('Browse')
self.browse_btn.clicked.connect(self.browse_path) self.browse_btn.clicked.connect(self.browse_path)
self.browse_btn.setStyleSheet(button_style)
# Create download control buttons
self.download_btn = QPushButton('Download') self.download_btn = QPushButton('Download')
self.download_btn.clicked.connect(self.start_download) self.download_btn.clicked.connect(self.start_download)
self.download_btn.setStyleSheet(button_style)
self.pause_btn = QPushButton('Pause') self.pause_btn = QPushButton('Pause')
self.pause_btn.clicked.connect(self.toggle_pause) self.pause_btn.clicked.connect(self.toggle_pause)
self.pause_btn.setVisible(False) self.pause_btn.setVisible(False)
self.pause_btn.setStyleSheet(button_style)
# Add cancel button
self.cancel_btn = QPushButton('Cancel') self.cancel_btn = QPushButton('Cancel')
self.cancel_btn.clicked.connect(self.cancel_download) self.cancel_btn.clicked.connect(self.cancel_download)
self.cancel_btn.setVisible(False) self.cancel_btn.setVisible(False)
self.cancel_btn.setStyleSheet(button_style)
# Add log button
self.log_btn = QPushButton('yt-dlp Log') self.log_btn = QPushButton('yt-dlp Log')
self.log_btn.setCheckable(True) self.log_btn.setCheckable(True)
self.log_btn.clicked.connect(self.toggle_log_window) self.log_btn.clicked.connect(self.toggle_log_window)
download_layout.addWidget(self.log_btn) self.log_btn.setStyleSheet(button_style)
# Add custom command button to download_layout
self.custom_cmd_btn = QPushButton('Custom Command') self.custom_cmd_btn = QPushButton('Custom Command')
self.custom_cmd_btn.clicked.connect(self.show_custom_command) self.custom_cmd_btn.clicked.connect(self.show_custom_command)
download_layout.addWidget(self.custom_cmd_btn) self.custom_cmd_btn.setStyleSheet(button_style)
self.update_ytdlp_btn = QPushButton('Update yt-dlp')
self.update_ytdlp_btn.clicked.connect(self.update_ytdlp)
self.update_ytdlp_btn.setStyleSheet(button_style)
# Add all buttons to layout
download_layout.addWidget(self.log_btn)
download_layout.addWidget(self.custom_cmd_btn)
download_layout.addWidget(self.update_ytdlp_btn)
download_layout.addWidget(self.path_input) download_layout.addWidget(self.path_input)
download_layout.addWidget(self.browse_btn) download_layout.addWidget(self.browse_btn)
download_layout.addWidget(self.download_btn) download_layout.addWidget(self.download_btn)
@@ -597,18 +825,46 @@ class YTSage(QMainWindow):
download_layout.addWidget(self.cancel_btn) download_layout.addWidget(self.cancel_btn)
layout.addLayout(download_layout) layout.addLayout(download_layout)
# Progress section # Progress section with improved styling
progress_layout = QVBoxLayout() progress_layout = QVBoxLayout()
self.progress_bar = QProgressBar() 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) progress_layout.addWidget(self.progress_bar)
# Add download details label # Add download details label with improved styling
self.download_details_label = QLabel() self.download_details_label = QLabel()
self.download_details_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 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) progress_layout.addWidget(self.download_details_label)
self.status_label = QLabel('Ready') self.status_label = QLabel('Ready')
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setStyleSheet("""
QLabel {
color: #cccccc;
font-size: 12px;
padding: 5px;
}
""")
progress_layout.addWidget(self.status_label) progress_layout.addWidget(self.status_label)
layout.addLayout(progress_layout) layout.addLayout(progress_layout)
@@ -624,47 +880,55 @@ class YTSage(QMainWindow):
self.signals.update_status.emit("Invalid URL or please enter a URL.") self.signals.update_status.emit("Invalid URL or please enter a URL.")
return return
self.signals.update_status.emit("Analyzing...") self.signals.update_status.emit("Analyzing (0%)... Preparing request")
threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start() threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start()
def _analyze_url_thread(self, url): def _analyze_url_thread(self, url):
try: try:
self.signals.update_status.emit("Analyzing (20%)... Extracting basic info")
# Clean up the URL to handle both playlist and video URLs # Clean up the URL to handle both playlist and video URLs
if 'list=' in url and 'watch?v=' in url: if 'list=' in url and 'watch?v=' in url:
# Extract playlist ID
playlist_id = url.split('list=')[1].split('&')[0] playlist_id = url.split('list=')[1].split('&')[0]
url = f'https://www.youtube.com/playlist?list={playlist_id}' url = f'https://www.youtube.com/playlist?list={playlist_id}'
# Initial extraction with basic options # Initial extraction with basic options
ydl_opts = { ydl_opts = {
'quiet': True, 'quiet': False,
'no_warnings': True, 'no_warnings': False,
'extract_flat': True, 'extract_flat': True,
'force_generic_extractor': False, 'force_generic_extractor': False,
'ignoreerrors': True, 'ignoreerrors': True,
'no_color': True 'no_color': True,
'verbose': True
} }
with yt_dlp.YoutubeDL(ydl_opts) as ydl: with yt_dlp.YoutubeDL(ydl_opts) as ydl:
try: try:
basic_info = ydl.extract_info(url, download=False) basic_info = ydl.extract_info(url, download=False)
if not basic_info:
raise Exception("Could not extract basic video information")
except Exception as e: except Exception as e:
print(f"First extraction failed: {str(e)}") print(f"First extraction failed: {str(e)}")
raise Exception("Could not extract video information") raise Exception("Could not extract video information")
self.signals.update_status.emit("Analyzing (40%)... Extracting detailed info")
# Configure options for detailed extraction # Configure options for detailed extraction
ydl_opts.update({ ydl_opts.update({
'extract_flat': False, 'extract_flat': False,
'format': 'best', # Start with best format to ensure we get some formats 'format': None,
'writesubtitles': True, 'writesubtitles': True,
'allsubtitles': True, 'allsubtitles': True,
'writeautomaticsub': True, 'writeautomaticsub': True,
'playliststart': 1, 'playliststart': 1,
'playlistend': 1 # Only get first video initially 'playlistend': 1,
'youtube_include_dash_manifest': True,
'youtube_include_hls_manifest': True
}) })
with yt_dlp.YoutubeDL(ydl_opts) as ydl: with yt_dlp.YoutubeDL(ydl_opts) as ydl:
try: try:
self.signals.update_status.emit("Analyzing (60%)... Processing video data")
if basic_info.get('_type') == 'playlist': if basic_info.get('_type') == 'playlist':
self.is_playlist = True self.is_playlist = True
self.playlist_info = basic_info self.playlist_info = basic_info
@@ -701,28 +965,31 @@ class YTSage(QMainWindow):
print(f"Debug - video_info keys: {self.video_info.keys() if self.video_info else 'None'}") print(f"Debug - video_info keys: {self.video_info.keys() if self.video_info else 'None'}")
raise Exception("No format information available") raise Exception("No format information available")
self.signals.update_status.emit("Analyzing (80%)... Processing formats")
self.all_formats = self.video_info['formats'] self.all_formats = self.video_info['formats']
# Update UI # Update UI
self.signals.update_status.emit("Updating video information...")
self.update_video_info(self.video_info) self.update_video_info(self.video_info)
# Update thumbnail # Update thumbnail
self.signals.update_status.emit("Analyzing (90%)... Loading thumbnail")
thumbnail_url = self.video_info.get('thumbnail') thumbnail_url = self.video_info.get('thumbnail')
if thumbnail_url: if thumbnail_url:
self.download_thumbnail(thumbnail_url) self.download_thumbnail(thumbnail_url)
# Update subtitles # Update subtitles
self.signals.update_status.emit("Analyzing (95%)... Processing subtitles")
self.available_subtitles = self.video_info.get('subtitles', {}) self.available_subtitles = self.video_info.get('subtitles', {})
self.available_automatic_subtitles = self.video_info.get('automatic_captions', {}) self.available_automatic_subtitles = self.video_info.get('automatic_captions', {})
self.update_subtitle_list() self.update_subtitle_list()
# Update format table # Update format table
self.signals.update_status.emit("Analyzing (98%)... Updating format table")
self.video_btn.setChecked(True) self.video_btn.setChecked(True)
self.audio_btn.setChecked(False) self.audio_btn.setChecked(False)
self.filter_formats() self.filter_formats()
self.signals.update_status.emit("Analysis complete") self.signals.update_status.emit("Analysis complete!")
except Exception as e: except Exception as e:
print(f"Detailed extraction failed: {str(e)}") print(f"Detailed extraction failed: {str(e)}")
@@ -769,15 +1036,44 @@ class YTSage(QMainWindow):
self.subtitle_combo.addItem("No subtitles available") self.subtitle_combo.addItem("No subtitles available")
return return
# Create a new horizontal layout for subtitle controls if it doesn't exist
if not hasattr(self, 'subtitle_filter_input'):
# Find the parent layout containing the subtitle controls
for i in range(self.centralWidget().layout().count()):
item = self.centralWidget().layout().itemAt(i)
if isinstance(item, QHBoxLayout) and self.subtitle_check in item.parent().findChildren(QPushButton):
# Create and add filter components
self.subtitle_filter_input = QLineEdit()
self.subtitle_filter_input.setPlaceholderText("Filter languages (e.g., en, es)")
self.subtitle_filter_input.setText(self.subtitle_filter)
self.subtitle_filter_input.setMaximumWidth(200) # Limit width
self.subtitle_filter_input.textChanged.connect(self.filter_subtitles)
# Add the filter input before the combo box
layout_index = item.indexOf(self.subtitle_combo)
item.insertWidget(layout_index, QLabel("Filter:"))
item.insertWidget(layout_index + 1, self.subtitle_filter_input)
break
# Add subtitle options
self.subtitle_combo.addItem("Select subtitle language") self.subtitle_combo.addItem("Select subtitle language")
# Filter and add subtitles
filter_text = self.subtitle_filter_input.text().lower() if hasattr(self, 'subtitle_filter_input') else ""
# Add manual subtitles # Add manual subtitles
for lang_code, subtitle_info in self.available_subtitles.items(): for lang_code, subtitle_info in self.available_subtitles.items():
self.subtitle_combo.addItem(f"{lang_code} - Manual") if not filter_text or filter_text in lang_code.lower():
self.subtitle_combo.addItem(f"{lang_code} - Manual")
# Add auto-generated subtitles # Add auto-generated subtitles
for lang_code, subtitle_info in self.available_automatic_subtitles.items(): for lang_code, subtitle_info in self.available_automatic_subtitles.items():
self.subtitle_combo.addItem(f"{lang_code} - Auto-generated") if not filter_text or filter_text in lang_code.lower():
self.subtitle_combo.addItem(f"{lang_code} - Auto-generated")
def filter_subtitles(self):
# Just filter without saving
self.update_subtitle_list()
def download_thumbnail(self, url): def download_thumbnail(self, url):
try: try:
@@ -910,125 +1206,24 @@ class YTSage(QMainWindow):
self.pause_btn.setVisible(True) self.pause_btn.setVisible(True)
self.cancel_btn.setVisible(True) self.cancel_btn.setVisible(True)
threading.Thread(target=self._download_thread, self.download_thread = DownloadThread(url, path, format_id, selected_sub, self.is_playlist)
args=(url, path, format_id, selected_sub, self.is_playlist), self.download_thread.progress_signal.connect(self.update_progress_bar)
daemon=True).start() self.download_thread.status_signal.connect(self.signals.update_status.emit)
self.download_thread.finished_signal.connect(self.download_finished)
self.download_thread.error_signal.connect(self.download_error)
def _download_thread(self, url, path, format_id, subtitle_lang=None, is_playlist=False): self.download_thread.start()
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): def download_finished(self):
if self.parent.show_log and self.parent.log_window: self.download_btn.setEnabled(True)
self.parent.log_window.append_log(f"Warning: {msg}") self.pause_btn.setVisible(False)
self.cancel_btn.setVisible(False)
def error(self, msg): def download_error(self, error_message):
if self.parent.show_log and self.parent.log_window: self.signals.update_status.emit(f"Error: {error_message}")
self.parent.log_window.append_log(f"Error: {msg}") self.download_btn.setEnabled(True)
self.pause_btn.setVisible(False)
def __init__(self, signals, parent): self.cancel_btn.setVisible(False)
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:
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))
progress = int((downloaded_bytes * 100) // total_bytes)
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)
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): def update_progress_bar(self, value):
try: try:
@@ -1039,9 +1234,9 @@ class YTSage(QMainWindow):
print(f"Progress bar update error: {str(e)}") print(f"Progress bar update error: {str(e)}")
def toggle_pause(self): def toggle_pause(self):
if self.current_download: if self.download_thread:
self.download_paused = not self.download_paused self.download_thread.paused = not self.download_thread.paused
if self.download_paused: if self.download_thread.paused:
self.pause_btn.setText('Resume') self.pause_btn.setText('Resume')
self.signals.update_status.emit("Download paused") self.signals.update_status.emit("Download paused")
else: else:
@@ -1140,8 +1335,8 @@ class YTSage(QMainWindow):
dialog.exec() dialog.exec()
def cancel_download(self): def cancel_download(self):
self.download_cancelled = True if self.download_thread:
if self.current_download: self.download_thread.cancelled = True
self.signals.update_status.emit("Cancelling download...") self.signals.update_status.emit("Cancelling download...")
def check_ffmpeg(self): def check_ffmpeg(self):
@@ -1158,6 +1353,61 @@ class YTSage(QMainWindow):
dialog = FFmpegCheckDialog(self) dialog = FFmpegCheckDialog(self)
dialog.exec() dialog.exec()
def paste_url(self):
clipboard = QApplication.clipboard()
self.url_input.setText(clipboard.text())
def update_ytdlp(self):
self.signals.update_status.emit("Updating yt-dlp...")
threading.Thread(target=self._update_ytdlp_thread, daemon=True).start()
def _update_ytdlp_thread(self):
try:
# Show initial status
self.signals.update_status.emit("Updating yt-dlp (0%)...")
# Run pip install with output capture
process = subprocess.Popen(
[sys.executable, "-m", "pip", "install", "--upgrade", "yt-dlp"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# Track progress through output lines
progress = 0
progress_steps = ['Collecting', 'Downloading', 'Installing', 'Successfully']
current_step = 0
while True:
line = process.stdout.readline() or process.stderr.readline()
if not line and process.poll() is not None:
break
line = line.strip()
if line:
# Update progress based on steps
if any(step in line for step in progress_steps[current_step:]):
current_step = next(i for i, step in enumerate(progress_steps) if step in line)
progress = (current_step + 1) * 25
self.signals.update_status.emit(f"Updating yt-dlp ({progress}%)...")
# Get the installed version
version_process = subprocess.run(
[sys.executable, "-m", "yt_dlp", "--version"],
capture_output=True,
text=True
)
version = version_process.stdout.strip()
self.signals.update_status.emit(f"yt-dlp updated successfully! (Version: {version})")
except subprocess.CalledProcessError as e:
error_msg = e.stderr.decode() if e.stderr else str(e)
self.signals.update_status.emit(f"Error updating yt-dlp: {error_msg}")
except Exception as e:
self.signals.update_status.emit(f"Error updating yt-dlp: {str(e)}")
def main(): def main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setStyle(QStyleFactory.create('Fusion')) app.setStyle(QStyleFactory.create('Fusion'))