From a92ffd3b011a3143d8c3c4e3bc1c01fc4ae26126 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Wed, 21 Jan 2026 17:16:35 +0200 Subject: [PATCH] Download video thumbnails asynchronously in GUI Refactored thumbnail downloading to use a QThread for asynchronous operation, preventing UI blocking. Added ThumbnailDownloadThread class and updated VideoInfoMixin to handle threaded download and error reporting. --- src/gui/ytsage_gui_video_info.py | 45 +++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/gui/ytsage_gui_video_info.py b/src/gui/ytsage_gui_video_info.py index 06b282f..8ab17f8 100644 --- a/src/gui/ytsage_gui_video_info.py +++ b/src/gui/ytsage_gui_video_info.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, cast import requests from PIL import Image -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtGui import QPixmap from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget @@ -21,6 +21,26 @@ if TYPE_CHECKING: from src.gui.ytsage_gui_main import YTSageApp +class ThumbnailDownloadThread(QThread): + """Thread to download thumbnail image asynchronously.""" + finished = Signal(bytes) + error = Signal(str) + + def __init__(self, url): + super().__init__() + self.url = url + + def run(self): + try: + response = requests.get(self.url, timeout=10) + if response.status_code == 200: + self.finished.emit(response.content) + else: + self.error.emit(f"HTTP Error: {response.status_code}") + except Exception as e: + self.error.emit(str(e)) + + class VideoInfoMixin: def setup_video_info_section(self) -> QHBoxLayout: self = cast("YTSageApp", self) # for autocompletion and type inference. @@ -368,14 +388,21 @@ class VideoInfoMixin: def download_thumbnail(self, url) -> None: self = cast("YTSageApp", self) # for autocompletion and type inference. - try: - # Store both thumbnail URL and video URL - self.thumbnail_url = url - self.video_url = self.url_input.text() # Get actual video URL + # Store both thumbnail URL and video URL + self.thumbnail_url = url + self.video_url = self.url_input.text() # Get actual video URL - # Download thumbnail but don't save yet - response = requests.get(url) - self.thumbnail_image = Image.open(BytesIO(response.content)) + # Create and start loader thread + # Keep reference to avoid garbage collection + self.thumbnail_thread = ThumbnailDownloadThread(url) + self.thumbnail_thread.finished.connect(self._on_thumbnail_downloaded) + self.thumbnail_thread.error.connect(lambda e: logger.error(f"Error loading thumbnail: {e}")) + self.thumbnail_thread.start() + + def _on_thumbnail_downloaded(self, content: bytes) -> None: + self = cast("YTSageApp", self) + try: + self.thumbnail_image = Image.open(BytesIO(content)) # Display thumbnail image = self.thumbnail_image.resize((320, 180), Image.Resampling.LANCZOS) @@ -385,7 +412,7 @@ class VideoInfoMixin: pixmap.loadFromData(img_byte_arr.getvalue()) self.thumbnail_label.setPixmap(pixmap) except Exception as e: - logger.exception(f"Error loading thumbnail: {e}") + logger.exception(f"Error processing thumbnail image: {e}") def download_thumbnail_file(self, video_url, path) -> bool: self = cast("YTSageApp", self) # for autocompletion and type inference.