This commit is contained in:
Muhamed
2025-01-30 17:12:52 +02:00
committed by GitHub
parent 989e9ce708
commit 01e4d7daae
+332 -67
View File
@@ -348,6 +348,282 @@ class DownloadThread(QThread):
except Exception as e:
self.error_signal.emit(str(e))
class YTDLPUpdateDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle('Update yt-dlp')
self.setMinimumWidth(400)
layout = QVBoxLayout(self)
# Version info and message
self.version_label = QLabel()
self.version_label.setWordWrap(True)
layout.addWidget(self.version_label)
self.message_label = QLabel(
"Would you like to update yt-dlp to the latest version?\n"
"This will download and install the latest yt-dlp executable."
)
self.message_label.setWordWrap(True)
layout.addWidget(self.message_label)
# Progress bar (hidden initially)
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
layout.addWidget(self.progress_bar)
# Status label
self.status_label = QLabel()
self.status_label.setWordWrap(True)
layout.addWidget(self.status_label)
# Buttons
button_layout = QHBoxLayout()
self.update_btn = QPushButton("Update yt-dlp")
self.update_btn.clicked.connect(self.start_update)
self.update_btn.setEnabled(False) # Disabled until version check
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 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;
}
QPushButton:disabled {
background-color: #666666;
}
QProgressBar {
border: 2px solid #3d3d3d;
border-radius: 4px;
text-align: center;
color: white;
}
QProgressBar::chunk {
background-color: #ff0000;
}
""")
# Check versions when dialog opens
self.check_versions()
def check_versions(self):
threading.Thread(target=self._check_versions_thread, daemon=True).start()
def _check_versions_thread(self):
try:
# Get current version with proper path
yt_dlp_path = self.get_yt_dlp_path()
if os.path.exists(yt_dlp_path):
try:
# Use the full path to yt-dlp executable
result = subprocess.run([yt_dlp_path, '--version'],
capture_output=True,
text=True,
timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0)
if result.returncode == 0:
current_version = result.stdout.strip()
else:
# Try using pip to get version as fallback
try:
import yt_dlp
current_version = yt_dlp.version.__version__
except:
current_version = "Not installed"
except (subprocess.SubprocessError, subprocess.TimeoutExpired):
# Try using pip to get version as fallback
try:
import yt_dlp
current_version = yt_dlp.version.__version__
except:
current_version = "Not installed"
else:
# Try using pip to get version as fallback
try:
import yt_dlp
current_version = yt_dlp.version.__version__
except:
current_version = "Not installed"
# Get latest version from GitHub API with timeout
try:
response = requests.get(
"https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest",
timeout=10
)
response.raise_for_status()
latest_version = response.json()["tag_name"]
except Exception as e:
raise Exception(f"Could not fetch latest version: {str(e)}")
self.version_label.setText(
f"Current version: {current_version}\n"
f"Latest version: {latest_version}"
)
if current_version == "Not installed" or current_version != latest_version:
self.update_btn.setEnabled(True)
self.message_label.setText("An update is available!")
else:
self.message_label.setText("You have the latest version installed.")
self.update_btn.setEnabled(False)
except Exception as e:
self.version_label.setText("Could not check versions")
self.message_label.setText(f"Error: {str(e)}")
self.update_btn.setEnabled(True)
def get_yt_dlp_path(self):
"""Get the appropriate yt-dlp path based on platform and deployment method"""
try:
if getattr(sys, 'frozen', False):
if sys.platform == 'darwin':
# For macOS .app bundle
if 'Contents/MacOS' in sys.executable:
# Inside .app bundle
return os.path.join(os.path.dirname(sys.executable), 'yt-dlp')
else:
# Fallback to user's home directory for macOS
base_path = os.path.expanduser('~/Library/Application Support/YTSage')
os.makedirs(base_path, exist_ok=True)
return os.path.join(base_path, 'yt-dlp')
elif sys.platform == 'win32':
# For Windows executable
app_data = os.getenv('APPDATA')
if app_data:
base_path = os.path.join(app_data, 'YTSage')
else:
base_path = os.path.dirname(sys.executable)
os.makedirs(base_path, exist_ok=True)
return os.path.join(base_path, 'yt-dlp.exe')
else:
# For Linux AppImage or binary
if 'APPIMAGE' in os.environ:
# Inside AppImage
xdg_data = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
base_path = os.path.join(xdg_data, 'YTSage')
else:
base_path = os.path.dirname(sys.executable)
os.makedirs(base_path, exist_ok=True)
return os.path.join(base_path, 'yt-dlp')
else:
# For development/script mode
return os.path.join(os.path.dirname(os.path.abspath(__file__)),
'yt-dlp.exe' if sys.platform == 'win32' else 'yt-dlp')
except Exception as e:
print(f"Error determining yt-dlp path: {e}")
# Fallback to current directory
return os.path.join(os.getcwd(), 'yt-dlp.exe' if sys.platform == 'win32' else 'yt-dlp')
def start_update(self):
self.update_btn.setEnabled(False)
self.progress_bar.setVisible(True)
self.status_label.setText("Starting update...")
# Start update in a separate thread
threading.Thread(target=self._update_thread, daemon=True).start()
def _update_thread(self):
temp_path = None
try:
# Create necessary directories
target_path = self.get_yt_dlp_path()
os.makedirs(os.path.dirname(target_path), exist_ok=True)
# Determine platform and get URLs
if sys.platform == 'win32':
url = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe'
else:
url = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp'
temp_path = target_path + '.download'
# Download with proper error handling
try:
response = requests.get(url, stream=True, timeout=30)
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
block_size = 8192 # Increased for better performance
with open(temp_path, 'wb') as f:
for data in response.iter_content(block_size):
if data: # Filter out keep-alive chunks
f.write(data)
progress = int((f.tell() * 100) / total_size) if total_size > 0 else 0
self.progress_bar.setValue(progress)
self.status_label.setText(f"Downloading: {progress}%")
except requests.exceptions.RequestException as e:
raise Exception(f"Download failed: {str(e)}")
# Set proper permissions
if sys.platform != 'win32':
try:
os.chmod(temp_path, 0o755)
except Exception as e:
print(f"Warning: Could not set executable permissions: {e}")
# Handle file replacement
try:
if os.path.exists(target_path):
if sys.platform == 'win32':
# Windows-specific file replacement
import ctypes
if not ctypes.windll.kernel32.MoveFileExW(target_path, None, 4): # MOVEFILE_DELAY_UNTIL_REBOOT
os.remove(target_path)
else:
os.remove(target_path)
except PermissionError:
raise Exception("Cannot update while yt-dlp is in use. Please close any active downloads and try again.")
except Exception as e:
raise Exception(f"Error removing existing file: {str(e)}")
# Move temporary file to final location
try:
os.rename(temp_path, target_path)
except Exception as e:
raise Exception(f"Error moving new file into place: {str(e)}")
self.status_label.setText("yt-dlp updated successfully!")
self.close_btn.setText("Done")
# Refresh version info
self.check_versions()
except Exception as e:
self.status_label.setText(f"Error updating yt-dlp: {str(e)}")
finally:
# Clean up temp file if it exists
if temp_path and os.path.exists(temp_path):
try:
os.remove(temp_path)
except:
pass
self.update_btn.setEnabled(True)
class YTSage(QMainWindow):
def __init__(self):
super().__init__()
@@ -355,7 +631,7 @@ class YTSage(QMainWindow):
if not self.check_ffmpeg():
self.show_ffmpeg_dialog()
self.version = "3.0.0"
self.version = "3.5.0"
self.check_for_updates() # Check for updates on startup
self.config_file = Path.home() / '.ytsage_config.json'
self.load_saved_path()
@@ -363,9 +639,8 @@ class YTSage(QMainWindow):
self.signals = SignalManager()
self.download_paused = False
self.current_download = None
self.log_window = None
self.show_log = False
self.download_cancelled = False
self.save_thumbnail = False
self.init_ui()
self.setStyleSheet("""
QMainWindow {
@@ -558,7 +833,7 @@ class YTSage(QMainWindow):
print(f"Error saving settings: {e}")
def init_ui(self):
self.setWindowTitle('YTSage v3.0.0')
self.setWindowTitle('YTSage v3.5.0')
self.setMinimumSize(900, 600)
# Main widget and layout
@@ -660,6 +935,33 @@ class YTSage(QMainWindow):
}
""")
subtitle_layout.addWidget(self.subtitle_combo)
# Add thumbnail save toggle button
self.save_thumbnail_btn = QPushButton('Save Thumbnail')
self.save_thumbnail_btn.setCheckable(True)
self.save_thumbnail_btn.setStyleSheet("""
QPushButton {
background-color: #363636;
border: 2px solid #3d3d3d;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:checked {
background-color: #ff0000;
border-color: #cc0000;
}
QPushButton:hover {
background-color: #444444;
}
QPushButton:checked:hover {
background-color: #cc0000;
}
""")
self.save_thumbnail_btn.clicked.connect(self.toggle_save_thumbnail)
# Add the button to the subtitle layout
subtitle_layout.addWidget(self.save_thumbnail_btn)
subtitle_layout.addStretch()
# Add all info widgets to the video info layout
@@ -801,11 +1103,6 @@ class YTSage(QMainWindow):
self.cancel_btn.setVisible(False)
self.cancel_btn.setStyleSheet(button_style)
self.log_btn = QPushButton('yt-dlp Log')
self.log_btn.setCheckable(True)
self.log_btn.clicked.connect(self.toggle_log_window)
self.log_btn.setStyleSheet(button_style)
self.custom_cmd_btn = QPushButton('Custom Command')
self.custom_cmd_btn.clicked.connect(self.show_custom_command)
self.custom_cmd_btn.setStyleSheet(button_style)
@@ -815,7 +1112,6 @@ class YTSage(QMainWindow):
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)
@@ -910,7 +1206,7 @@ class YTSage(QMainWindow):
raise Exception("Could not extract basic video information")
except Exception as e:
print(f"First extraction failed: {str(e)}")
raise Exception("Could not extract video information")
raise Exception("Could not extract video information, please check your link")
self.signals.update_status.emit("Analyzing (40%)... Extracting detailed info")
# Configure options for detailed extraction
@@ -1077,9 +1373,10 @@ class YTSage(QMainWindow):
def download_thumbnail(self, url):
try:
self.thumbnail_url = url # Store the URL for later use
response = requests.get(url)
image = Image.open(BytesIO(response.content))
image = image.resize((320, 180), Image.Resampling.LANCZOS)
self.thumbnail_image = Image.open(BytesIO(response.content)) # Store the image
image = self.thumbnail_image.resize((320, 180), Image.Resampling.LANCZOS)
img_byte_arr = BytesIO()
image.save(img_byte_arr, format='PNG')
img_byte_arr = img_byte_arr.getvalue()
@@ -1214,6 +1511,10 @@ class YTSage(QMainWindow):
self.download_thread.start()
# Add thumbnail download if enabled
if self.save_thumbnail and hasattr(self, 'thumbnail_url'):
self.download_thumbnail_file(self.thumbnail_url, path)
def download_finished(self):
self.download_btn.setEnabled(True)
self.pause_btn.setVisible(False)
@@ -1243,17 +1544,6 @@ class YTSage(QMainWindow):
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
@@ -1358,55 +1648,30 @@ class YTSage(QMainWindow):
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()
dialog = YTDLPUpdateDialog(self)
dialog.exec()
def _update_ytdlp_thread(self):
def toggle_save_thumbnail(self):
self.save_thumbnail = self.save_thumbnail_btn.isChecked()
def download_thumbnail_file(self, url, path):
try:
# Show initial status
self.signals.update_status.emit("Updating yt-dlp (0%)...")
if not hasattr(self, 'thumbnail_image'):
response = requests.get(url)
self.thumbnail_image = Image.open(BytesIO(response.content))
# 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
)
# Create thumbnails directory if it doesn't exist
thumbnails_path = os.path.join(path, 'thumbnails')
os.makedirs(thumbnails_path, exist_ok=True)
# Track progress through output lines
progress = 0
progress_steps = ['Collecting', 'Downloading', 'Installing', 'Successfully']
current_step = 0
# Save the thumbnail
video_title = self.video_info.get('title', 'thumbnail').replace('/', '_')
thumbnail_path = os.path.join(thumbnails_path, f"{video_title}.jpg")
self.thumbnail_image.save(thumbnail_path)
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}")
self.signals.update_status.emit(f"Thumbnail saved to {thumbnail_path}")
except Exception as e:
self.signals.update_status.emit(f"Error updating yt-dlp: {str(e)}")
self.signals.update_status.emit(f"Error saving thumbnail: {str(e)}")
def main():
app = QApplication(sys.argv)