Add thumbnail status messages and refactor thumbnail saving

Added new localized status messages for thumbnail saving in all supported languages. Refactored the thumbnail saving logic in ytsage_gui_video_info.py to use cached images from analysis instead of extracting via yt-dlp subprocess, improving reliability and performance.
This commit is contained in:
oop7
2025-11-28 18:00:56 +02:00
parent 7a6aaa6704
commit fc719a8cc8
15 changed files with 77 additions and 62 deletions
+4 -1
View File
@@ -389,7 +389,10 @@
"video_file_exists": "⚠️ ملف الفيديو موجود بالفعل", "video_file_exists": "⚠️ ملف الفيديو موجود بالفعل",
"audio_file_exists": "⚠️ ملف الصوت موجود بالفعل", "audio_file_exists": "⚠️ ملف الصوت موجود بالفعل",
"subtitle_file_exists": "⚠️ ملف الترجمات موجود بالفعل", "subtitle_file_exists": "⚠️ ملف الترجمات موجود بالفعل",
"cancelling": "جاري إلغاء التنزيل..." "cancelling": "جاري إلغاء التنزيل...",
"thumbnail_saved": "✅ تم حفظ الصورة المصغرة: {filename}",
"thumbnail_error": "❌ خطأ في الصورة المصغرة: {error}",
"thumbnail_no_image": "لا توجد صورة مصغرة متاحة للحفظ"
}, },
"errors": { "errors": {
"playlist_no_videos": "خطأ: قائمة التشغيل لا تحتوي على مقاطع فيديو صالحة.", "playlist_no_videos": "خطأ: قائمة التشغيل لا تحتوي على مقاطع فيديو صالحة.",
+4 -1
View File
@@ -389,7 +389,10 @@
"video_file_exists": "⚠️ Video-Datei existiert bereits", "video_file_exists": "⚠️ Video-Datei existiert bereits",
"audio_file_exists": "⚠️ Audio-Datei existiert bereits", "audio_file_exists": "⚠️ Audio-Datei existiert bereits",
"subtitle_file_exists": "⚠️ Untertitel-Datei existiert bereits", "subtitle_file_exists": "⚠️ Untertitel-Datei existiert bereits",
"cancelling": "Download wird abgebrochen..." "cancelling": "Download wird abgebrochen...",
"thumbnail_saved": "✅ Miniaturbild gespeichert: {filename}",
"thumbnail_error": "❌ Miniaturbild-Fehler: {error}",
"thumbnail_no_image": "Kein Miniaturbild zum Speichern verfügbar"
}, },
"errors": { "errors": {
"playlist_no_videos": "Fehler: Playlist enthält keine gültigen Videos.", "playlist_no_videos": "Fehler: Playlist enthält keine gültigen Videos.",
+4 -1
View File
@@ -389,7 +389,10 @@
"video_file_exists": "⚠️ Video file already exists", "video_file_exists": "⚠️ Video file already exists",
"audio_file_exists": "⚠️ Audio file already exists", "audio_file_exists": "⚠️ Audio file already exists",
"subtitle_file_exists": "⚠️ Subtitle file already exists", "subtitle_file_exists": "⚠️ Subtitle file already exists",
"cancelling": "Cancelling download..." "cancelling": "Cancelling download...",
"thumbnail_saved": "✅ Thumbnail saved: {filename}",
"thumbnail_error": "❌ Thumbnail error: {error}",
"thumbnail_no_image": "No thumbnail available to save"
}, },
"errors": { "errors": {
"playlist_no_videos": "Error: Playlist contains no valid videos.", "playlist_no_videos": "Error: Playlist contains no valid videos.",
+4 -1
View File
@@ -372,7 +372,10 @@
"video_file_exists": "⚠️ El archivo de video ya existe", "video_file_exists": "⚠️ El archivo de video ya existe",
"audio_file_exists": "⚠️ El archivo de audio ya existe", "audio_file_exists": "⚠️ El archivo de audio ya existe",
"subtitle_file_exists": "⚠️ El archivo de subtítulos ya existe", "subtitle_file_exists": "⚠️ El archivo de subtítulos ya existe",
"cancelling": "Cancelando descarga..." "cancelling": "Cancelando descarga...",
"thumbnail_saved": "✅ Miniatura guardada: {filename}",
"thumbnail_error": "❌ Error de miniatura: {error}",
"thumbnail_no_image": "No hay miniatura disponible para guardar"
}, },
"errors": { "errors": {
"playlist_no_videos": "Error: La lista de reproducción no contiene videos válidos.", "playlist_no_videos": "Error: La lista de reproducción no contiene videos válidos.",
+4 -1
View File
@@ -389,7 +389,10 @@
"video_file_exists": "⚠️ Le fichier vidéo existe déjà", "video_file_exists": "⚠️ Le fichier vidéo existe déjà",
"audio_file_exists": "⚠️ Le fichier audio existe déjà", "audio_file_exists": "⚠️ Le fichier audio existe déjà",
"subtitle_file_exists": "⚠️ Le fichier de sous-titres existe déjà", "subtitle_file_exists": "⚠️ Le fichier de sous-titres existe déjà",
"cancelling": "Annulation du téléchargement..." "cancelling": "Annulation du téléchargement...",
"thumbnail_saved": "✅ Miniature enregistrée : {filename}",
"thumbnail_error": "❌ Erreur de miniature : {error}",
"thumbnail_no_image": "Aucune miniature disponible à enregistrer"
}, },
"errors": { "errors": {
"playlist_no_videos": "Erreur : La playlist ne contient aucune vidéo valide.", "playlist_no_videos": "Erreur : La playlist ne contient aucune vidéo valide.",
+4 -1
View File
@@ -389,7 +389,10 @@
"video_file_exists": "⚠️ वीडियो फ़ाइल पहले से मौजूद है", "video_file_exists": "⚠️ वीडियो फ़ाइल पहले से मौजूद है",
"audio_file_exists": "⚠️ ऑडियो फ़ाइल पहले से मौजूद है", "audio_file_exists": "⚠️ ऑडियो फ़ाइल पहले से मौजूद है",
"subtitle_file_exists": "⚠️ उपशीर्षक फ़ाइल पहले से मौजूद है", "subtitle_file_exists": "⚠️ उपशीर्षक फ़ाइल पहले से मौजूद है",
"cancelling": "डाउनलोड रद्द हो रहा है..." "cancelling": "डाउनलोड रद्द हो रहा है...",
"thumbnail_saved": "✅ थंबनेल सहेजा गया: {filename}",
"thumbnail_error": "❌ थंबनेल त्रुटि: {error}",
"thumbnail_no_image": "सहेजने के लिए कोई थंबनेल उपलब्ध नहीं"
}, },
"errors": { "errors": {
"playlist_no_videos": "त्रुटि: प्लेलिस्ट में कोई वैध वीडियो नहीं है।", "playlist_no_videos": "त्रुटि: प्लेलिस्ट में कोई वैध वीडियो नहीं है।",
+4 -1
View File
@@ -389,7 +389,10 @@
"video_file_exists": "⚠️ File video sudah ada", "video_file_exists": "⚠️ File video sudah ada",
"audio_file_exists": "⚠️ File audio sudah ada", "audio_file_exists": "⚠️ File audio sudah ada",
"subtitle_file_exists": "⚠️ File subtitle sudah ada", "subtitle_file_exists": "⚠️ File subtitle sudah ada",
"cancelling": "Membatalkan unduhan..." "cancelling": "Membatalkan unduhan...",
"thumbnail_saved": "✅ Thumbnail disimpan: {filename}",
"thumbnail_error": "❌ Kesalahan thumbnail: {error}",
"thumbnail_no_image": "Tidak ada thumbnail yang tersedia untuk disimpan"
}, },
"errors": { "errors": {
"playlist_no_videos": "Kesalahan: Playlist tidak berisi video yang valid.", "playlist_no_videos": "Kesalahan: Playlist tidak berisi video yang valid.",
+4 -1
View File
@@ -389,7 +389,10 @@
"video_file_exists": "⚠️ File video già esistente", "video_file_exists": "⚠️ File video già esistente",
"audio_file_exists": "⚠️ File audio già esistente", "audio_file_exists": "⚠️ File audio già esistente",
"subtitle_file_exists": "⚠️ File sottotitoli già esistente", "subtitle_file_exists": "⚠️ File sottotitoli già esistente",
"cancelling": "Annullamento download..." "cancelling": "Annullamento download...",
"thumbnail_saved": "✅ Miniatura salvata: {filename}",
"thumbnail_error": "❌ Errore miniatura: {error}",
"thumbnail_no_image": "Nessuna miniatura disponibile da salvare"
}, },
"errors": { "errors": {
"playlist_no_videos": "Errore: La playlist non contiene video validi.", "playlist_no_videos": "Errore: La playlist non contiene video validi.",
+4 -1
View File
@@ -389,7 +389,10 @@
"video_file_exists": "⚠️ 動画ファイルがすでに存在します", "video_file_exists": "⚠️ 動画ファイルがすでに存在します",
"audio_file_exists": "⚠️ 音声ファイルがすでに存在します", "audio_file_exists": "⚠️ 音声ファイルがすでに存在します",
"subtitle_file_exists": "⚠️ 字幕ファイルがすでに存在します", "subtitle_file_exists": "⚠️ 字幕ファイルがすでに存在します",
"cancelling": "ダウンロードをキャンセル中..." "cancelling": "ダウンロードをキャンセル中...",
"thumbnail_saved": "✅ サムネイルを保存しました: {filename}",
"thumbnail_error": "❌ サムネイルエラー: {error}",
"thumbnail_no_image": "保存できるサムネイルがありません"
}, },
"errors": { "errors": {
"playlist_no_videos": "エラー: 再生リストに有効な動画が含まれていません。", "playlist_no_videos": "エラー: 再生リストに有効な動画が含まれていません。",
+4 -1
View File
@@ -389,7 +389,10 @@
"video_file_exists": "⚠️ Plik wideo już istnieje", "video_file_exists": "⚠️ Plik wideo już istnieje",
"audio_file_exists": "⚠️ Plik audio już istnieje", "audio_file_exists": "⚠️ Plik audio już istnieje",
"subtitle_file_exists": "⚠️ Plik napisów już istnieje", "subtitle_file_exists": "⚠️ Plik napisów już istnieje",
"cancelling": "Anulowanie pobierania..." "cancelling": "Anulowanie pobierania...",
"thumbnail_saved": "✅ Miniatura zapisana: {filename}",
"thumbnail_error": "❌ Błąd miniatury: {error}",
"thumbnail_no_image": "Brak miniatury do zapisania"
}, },
"errors": { "errors": {
"playlist_no_videos": "Błąd: Playlista nie zawiera prawidłowych wideo.", "playlist_no_videos": "Błąd: Playlista nie zawiera prawidłowych wideo.",
+4 -1
View File
@@ -389,7 +389,10 @@
"video_file_exists": "⚠️ Arquivo de vídeo já existe", "video_file_exists": "⚠️ Arquivo de vídeo já existe",
"audio_file_exists": "⚠️ Arquivo de áudio já existe", "audio_file_exists": "⚠️ Arquivo de áudio já existe",
"subtitle_file_exists": "⚠️ Arquivo de legendas já existe", "subtitle_file_exists": "⚠️ Arquivo de legendas já existe",
"cancelling": "Cancelando download..." "cancelling": "Cancelando download...",
"thumbnail_saved": "✅ Miniatura salva: {filename}",
"thumbnail_error": "❌ Erro na miniatura: {error}",
"thumbnail_no_image": "Nenhuma miniatura disponível para salvar"
}, },
"errors": { "errors": {
"playlist_no_videos": "Erro: A playlist não contém vídeos válidos.", "playlist_no_videos": "Erro: A playlist não contém vídeos válidos.",
+4 -1
View File
@@ -389,7 +389,10 @@
"video_file_exists": "⚠️ Видеофайл уже существует", "video_file_exists": "⚠️ Видеофайл уже существует",
"audio_file_exists": "⚠️ Аудиофайл уже существует", "audio_file_exists": "⚠️ Аудиофайл уже существует",
"subtitle_file_exists": "⚠️ Файл субтитров уже существует", "subtitle_file_exists": "⚠️ Файл субтитров уже существует",
"cancelling": "Отмена загрузки..." "cancelling": "Отмена загрузки...",
"thumbnail_saved": "✅ Миниатюра сохранена: {filename}",
"thumbnail_error": "❌ Ошибка миниатюры: {error}",
"thumbnail_no_image": "Нет доступной миниатюры для сохранения"
}, },
"errors": { "errors": {
"playlist_no_videos": "Ошибка: Плейлист не содержит действительных видео.", "playlist_no_videos": "Ошибка: Плейлист не содержит действительных видео.",
+4 -1
View File
@@ -389,7 +389,10 @@
"video_file_exists": "⚠️ Video dosyası zaten mevcut", "video_file_exists": "⚠️ Video dosyası zaten mevcut",
"audio_file_exists": "⚠️ Ses dosyası zaten mevcut", "audio_file_exists": "⚠️ Ses dosyası zaten mevcut",
"subtitle_file_exists": "⚠️ Altyazı dosyası zaten mevcut", "subtitle_file_exists": "⚠️ Altyazı dosyası zaten mevcut",
"cancelling": "İndirme iptal ediliyor..." "cancelling": "İndirme iptal ediliyor...",
"thumbnail_saved": "✅ Küçük resim kaydedildi: {filename}",
"thumbnail_error": "❌ Küçük resim hatası: {error}",
"thumbnail_no_image": "Kaydedilecek küçük resim yok"
}, },
"errors": { "errors": {
"playlist_no_videos": "Hata: Oynatma listesi geçerli video içermiyor.", "playlist_no_videos": "Hata: Oynatma listesi geçerli video içermiyor.",
+4 -1
View File
@@ -389,7 +389,10 @@
"video_file_exists": "⚠️ 视频文件已存在", "video_file_exists": "⚠️ 视频文件已存在",
"audio_file_exists": "⚠️ 音频文件已存在", "audio_file_exists": "⚠️ 音频文件已存在",
"subtitle_file_exists": "⚠️ 字幕文件已存在", "subtitle_file_exists": "⚠️ 字幕文件已存在",
"cancelling": "正在取消下载..." "cancelling": "正在取消下载...",
"thumbnail_saved": "✅ 缩略图已保存: {filename}",
"thumbnail_error": "❌ 缩略图错误: {error}",
"thumbnail_no_image": "没有可保存的缩略图"
}, },
"errors": { "errors": {
"playlist_no_videos": "错误:播放列表不包含有效视频。", "playlist_no_videos": "错误:播放列表不包含有效视频。",
+21 -48
View File
@@ -389,74 +389,47 @@ class VideoInfoMixin:
def download_thumbnail_file(self, video_url, path) -> bool: def download_thumbnail_file(self, video_url, path) -> bool:
self = cast("YTSageApp", self) # for autocompletion and type inference. self = cast("YTSageApp", self) # for autocompletion and type inference.
def download_thumbnail_file(self, video_url, path) -> bool:
if not self.save_thumbnail: if not self.save_thumbnail:
return False return False
try: try:
# Note: yt_dlp Python package removed - this feature now uses subprocess # Use cached thumbnail image from analysis if available
# Extract thumbnail info using yt-dlp CLI instead if self.thumbnail_image is None:
from src.core.ytsage_yt_dlp import get_yt_dlp_path logger.info("No thumbnail image cached from analysis")
from src.utils.ytsage_constants import SUBPROCESS_CREATIONFLAGS self.signals.update_status.emit(_("status.thumbnail_no_image"))
import subprocess
import json
logger.debug(f"Attempting to save thumbnail for URL: {video_url}")
ytdlp_path = get_yt_dlp_path()
# Use yt-dlp CLI to extract thumbnail info
result = subprocess.run(
[ytdlp_path, "--dump-json", "--skip-download", video_url],
capture_output=True,
text=True,
timeout=30,
creationflags=SUBPROCESS_CREATIONFLAGS,
)
if result.returncode != 0:
logger.error(f"Failed to extract video info: {result.stderr}")
return False
info = json.loads(result.stdout)
thumbnails = info.get("thumbnails", [])
if not thumbnails:
logger.info("No thumbnails available")
return False return False
thumbnail_url = max( # Get video title from cached video_info
thumbnails, video_title = "thumbnail"
key=lambda t: (t.get("height", 0) or 0) * (t.get("width", 0) or 0), if self.video_info and "title" in self.video_info:
).get("url") video_title = self.video_info["title"]
elif self.playlist_info and "title" in self.playlist_info:
video_title = self.playlist_info["title"]
if not thumbnail_url: logger.debug(f"Saving cached thumbnail for: {video_title}")
logger.info("Failed to extract thumbnail URL")
return False
# Download using requests
response = requests.get(thumbnail_url)
response.raise_for_status()
# Save the thumbnail # Save the thumbnail
thumb_dir = Path(path).joinpath("Thumbnails") thumb_dir = Path(path).joinpath("Thumbnails")
thumb_dir.mkdir(exist_ok=True) thumb_dir.mkdir(exist_ok=True)
filename = f"{self.sanitize_filename(info['title'])}.jpg" filename = f"{self.sanitize_filename(video_title)}.jpg"
thumbnail_path = thumb_dir.joinpath(filename) thumbnail_path = thumb_dir.joinpath(filename)
with open(thumbnail_path, "wb") as f: # Save the cached PIL Image directly
f.write(response.content) # Convert to RGB if necessary (in case of RGBA or other modes)
if self.thumbnail_image.mode in ("RGBA", "P"):
rgb_image = self.thumbnail_image.convert("RGB")
rgb_image.save(thumbnail_path, "JPEG", quality=95)
else:
self.thumbnail_image.save(thumbnail_path, "JPEG", quality=95)
logger.info(f"Thumbnail saved to: {thumbnail_path}") logger.info(f"Thumbnail saved to: {thumbnail_path}")
self.signals.update_status.emit(f"✅ Thumbnail saved: {filename}") self.signals.update_status.emit(_("status.thumbnail_saved", filename=filename))
return True return True
except Exception as e: except Exception as e:
error_msg = f"❌ Thumbnail error: {e}"
logger.exception(f"Thumbnail Save Error: {e}") logger.exception(f"Thumbnail Save Error: {e}")
self.signals.update_status.emit(error_msg) self.signals.update_status.emit(_("status.thumbnail_error", error=str(e)))
return False return False
def sanitize_filename(self, name) -> str: def sanitize_filename(self, name) -> str: