Refactor analysis logic into AnalysisMixin
Moved all analysis-related methods from YTSageApp in ytsage_gui_main.py to a new AnalysisMixin in ytsage_gui_analysis.py. This improves code organization and separation of concerns by isolating analysis logic from the main application class.
This commit is contained in:
@@ -0,0 +1,257 @@
|
|||||||
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
from PySide6.QtCore import QMetaObject, Qt, Q_ARG
|
||||||
|
from PySide6.QtWidgets import QMessageBox
|
||||||
|
|
||||||
|
from src.core.ytsage_utils import validate_video_url, parse_yt_dlp_error
|
||||||
|
from src.core.ytsage_yt_dlp import get_yt_dlp_path
|
||||||
|
from src.utils.ytsage_constants import SUBPROCESS_CREATIONFLAGS
|
||||||
|
from src.utils.ytsage_localization import _
|
||||||
|
from src.utils.ytsage_logger import logger
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from src.gui.ytsage_gui_main import YTSageApp
|
||||||
|
|
||||||
|
class AnalysisMixin:
|
||||||
|
def analyze_url(self) -> None:
|
||||||
|
self = cast("YTSageApp", self)
|
||||||
|
if self.is_updating_ytdlp:
|
||||||
|
QMessageBox.warning(self, _("update.update_in_progress_title"), _("update.update_in_progress_message"))
|
||||||
|
return
|
||||||
|
|
||||||
|
url = self.url_input.text().strip()
|
||||||
|
if not url:
|
||||||
|
self.signals.update_status.emit(_("main_ui.invalid_url_or_enter"))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Validate URL before processing
|
||||||
|
is_valid, error_message = validate_video_url(url)
|
||||||
|
if not is_valid:
|
||||||
|
QMessageBox.warning(self, _("main_ui.error_title"), error_message)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Reset analysis state and disable controls
|
||||||
|
self.analysis_completed = False
|
||||||
|
self.toggle_analysis_dependent_controls(enabled=False)
|
||||||
|
|
||||||
|
self.signals.update_status.emit(_("main_ui.analyzing_preparing"))
|
||||||
|
self.is_analyzing = True
|
||||||
|
threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start()
|
||||||
|
|
||||||
|
def _analyze_url_thread(self, url) -> None:
|
||||||
|
self = cast("YTSageApp", self)
|
||||||
|
try:
|
||||||
|
self.signals.update_status.emit(_("main_ui.analyzing_extracting_basic"))
|
||||||
|
|
||||||
|
# Clean up the URL to handle both playlist and video URLs
|
||||||
|
if "list=" in url and "watch?v=" in url:
|
||||||
|
playlist_id = url.split("list=")[1].split("&")[0]
|
||||||
|
url = f"https://www.youtube.com/playlist?list={playlist_id}"
|
||||||
|
|
||||||
|
# Always use subprocess to call yt-dlp binary (Python package removed)
|
||||||
|
self._analyze_url_with_subprocess(url)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error in analysis: {e}")
|
||||||
|
self.signals.update_status.emit(_("errors.generic_error", error=str(e)))
|
||||||
|
# Ensure playlist UI is hidden on error too
|
||||||
|
self.signals.playlist_info_label_visible.emit(False)
|
||||||
|
self.signals.playlist_select_btn_visible.emit(False)
|
||||||
|
finally:
|
||||||
|
self.is_analyzing = False
|
||||||
|
|
||||||
|
|
||||||
|
def _analyze_url_with_subprocess(self, url) -> None:
|
||||||
|
"""Analyze URL using yt-dlp executable when Python module is not available"""
|
||||||
|
self = cast("YTSageApp", self)
|
||||||
|
|
||||||
|
try:
|
||||||
|
yt_dlp_path = get_yt_dlp_path()
|
||||||
|
if not yt_dlp_path:
|
||||||
|
logger.error("yt-dlp executable not found. Please install yt-dlp first.")
|
||||||
|
self.signals.update_status.emit(_("errors.ytdlp_not_found"))
|
||||||
|
self.signals.playlist_info_label_visible.emit(False)
|
||||||
|
self.signals.playlist_select_btn_visible.emit(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.signals.update_status.emit(_("main_ui.analyzing_extracting_ytdlp"))
|
||||||
|
|
||||||
|
# Clean up the URL to handle both playlist and video URLs
|
||||||
|
if "list=" in url and "watch?v=" in url:
|
||||||
|
playlist_id = url.split("list=")[1].split("&")[0]
|
||||||
|
url = f"https://www.youtube.com/playlist?list={playlist_id}"
|
||||||
|
|
||||||
|
# Build command for basic info extraction
|
||||||
|
# Use --flat-playlist for fast initial extraction of playlist info + --dump-single-json
|
||||||
|
# This fetches minimal info for all videos quickly without downloading full details for each
|
||||||
|
cmd = [yt_dlp_path, "--dump-single-json", "--flat-playlist", "--no-warnings", url]
|
||||||
|
|
||||||
|
# Add cookies if available
|
||||||
|
if self.cookie_file_path:
|
||||||
|
cmd.extend(["--cookies", str(self.cookie_file_path)])
|
||||||
|
elif self.browser_cookies_option:
|
||||||
|
cmd.extend(["--cookies-from-browser", self.browser_cookies_option])
|
||||||
|
|
||||||
|
# Add proxy settings if available
|
||||||
|
if self.proxy_url:
|
||||||
|
cmd.extend(["--proxy", self.proxy_url])
|
||||||
|
|
||||||
|
if self.geo_proxy_url:
|
||||||
|
cmd.extend(["--geo-verification-proxy", self.geo_proxy_url])
|
||||||
|
|
||||||
|
logger.debug(f"Executing yt-dlp command: {cmd}")
|
||||||
|
|
||||||
|
# Execute command with hidden console window on Windows
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, creationflags=SUBPROCESS_CREATIONFLAGS)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
logger.error(f"yt-dlp failed: {result.stderr}")
|
||||||
|
self.signals.update_status.emit(_("errors.ytdlp_failed", error=result.stderr))
|
||||||
|
self.signals.playlist_info_label_visible.emit(False)
|
||||||
|
self.signals.playlist_select_btn_visible.emit(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
json_lines = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
|
||||||
|
|
||||||
|
if not json_lines:
|
||||||
|
logger.error("No data returned from yt-dlp")
|
||||||
|
self.signals.update_status.emit(_("errors.no_data_returned"))
|
||||||
|
self.signals.playlist_info_label_visible.emit(False)
|
||||||
|
self.signals.playlist_select_btn_visible.emit(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
first_info = json.loads(json_lines[0])
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f"Failed to parse yt-dlp output: {e}")
|
||||||
|
self.signals.update_status.emit(_("errors.parse_failed", error=str(e)))
|
||||||
|
self.signals.playlist_info_label_visible.emit(False)
|
||||||
|
self.signals.playlist_select_btn_visible.emit(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.signals.update_status.emit(_("main_ui.analyzing_processing_data"))
|
||||||
|
|
||||||
|
if first_info.get("_type") == "playlist":
|
||||||
|
# Handle playlist
|
||||||
|
self.is_playlist = True
|
||||||
|
self.playlist_info = first_info
|
||||||
|
self.selected_playlist_items = None
|
||||||
|
self.playlist_entries = first_info.get("entries", [])
|
||||||
|
|
||||||
|
if not self.playlist_entries:
|
||||||
|
logger.error("Playlist contains no valid videos.")
|
||||||
|
self.signals.update_status.emit(_("errors.playlist_no_videos"))
|
||||||
|
self.signals.playlist_info_label_visible.emit(False)
|
||||||
|
self.signals.playlist_select_btn_visible.emit(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Even with flat-playlist, we need one video's formats to populate the table.
|
||||||
|
# Just use the first video URL to get full details quickly.
|
||||||
|
self.signals.update_status.emit(_("main_ui.analyzing_fetching_first_video"))
|
||||||
|
first_video_entry = self.playlist_entries[0]
|
||||||
|
first_video_url = first_video_entry.get("url")
|
||||||
|
|
||||||
|
# Fetch full info for just the first video to get formats
|
||||||
|
cmd_single = [yt_dlp_path, "--dump-single-json", "--no-warnings", first_video_url]
|
||||||
|
# Add cookies & proxy to this single request too
|
||||||
|
if self.cookie_file_path:
|
||||||
|
cmd_single.extend(["--cookies", str(self.cookie_file_path)])
|
||||||
|
elif self.browser_cookies_option:
|
||||||
|
cmd_single.extend(["--cookies-from-browser", self.browser_cookies_option])
|
||||||
|
if self.proxy_url:
|
||||||
|
cmd_single.extend(["--proxy", self.proxy_url])
|
||||||
|
if self.geo_proxy_url:
|
||||||
|
cmd_single.extend(["--geo-verification-proxy", self.geo_proxy_url])
|
||||||
|
|
||||||
|
result_single = subprocess.run(cmd_single, capture_output=True, text=True, timeout=60, creationflags=SUBPROCESS_CREATIONFLAGS)
|
||||||
|
|
||||||
|
if result_single.returncode == 0:
|
||||||
|
self.video_info = json.loads(result_single.stdout)
|
||||||
|
else:
|
||||||
|
# Fallback to whatever minimal info we have, might fail table population
|
||||||
|
self.video_info = first_video_entry
|
||||||
|
|
||||||
|
# Update playlist info label
|
||||||
|
playlist_text = _("playlist.display_format",
|
||||||
|
title=first_info.get('title', _('playlist.unknown')),
|
||||||
|
count=len(self.playlist_entries))
|
||||||
|
|
||||||
|
self.signals.playlist_info_label_text.emit(playlist_text)
|
||||||
|
self.signals.playlist_info_label_visible.emit(True)
|
||||||
|
|
||||||
|
# Show playlist selection button
|
||||||
|
self.signals.playlist_select_btn_text.emit(_("main_ui.select_videos_all"))
|
||||||
|
self.signals.playlist_select_btn_visible.emit(True)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Handle single video
|
||||||
|
self.is_playlist = False
|
||||||
|
self.video_info = first_info
|
||||||
|
self.playlist_entries = []
|
||||||
|
self.selected_playlist_items = None
|
||||||
|
|
||||||
|
# Hide playlist UI
|
||||||
|
self.signals.playlist_info_label_visible.emit(False)
|
||||||
|
self.signals.playlist_select_btn_visible.emit(False)
|
||||||
|
|
||||||
|
# Verify we have format information
|
||||||
|
if not self.video_info or "formats" not in self.video_info:
|
||||||
|
logger.error("No format information available")
|
||||||
|
self.signals.update_status.emit(_("errors.no_format_info"))
|
||||||
|
self.signals.playlist_info_label_visible.emit(False)
|
||||||
|
self.signals.playlist_select_btn_visible.emit(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.signals.update_status.emit(_("main_ui.analyzing_processing_formats_ytdlp"))
|
||||||
|
self.all_formats = self.video_info["formats"]
|
||||||
|
|
||||||
|
# Update UI
|
||||||
|
self.update_video_info(self.video_info)
|
||||||
|
|
||||||
|
# Update thumbnail
|
||||||
|
self.signals.update_status.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp"))
|
||||||
|
# Try to get thumbnail from playlist info first
|
||||||
|
# Fallback to video thumbnail if playlist thumbnail not found or not a playlist
|
||||||
|
thumbnail_url = (self.playlist_info or {}).get("thumbnail") or (self.video_info or {}).get("thumbnail")
|
||||||
|
|
||||||
|
self.download_thumbnail(thumbnail_url)
|
||||||
|
|
||||||
|
# Save thumbnail if enabled
|
||||||
|
if self.save_thumbnail:
|
||||||
|
self.download_thumbnail_file(self.video_url, self.last_path)
|
||||||
|
|
||||||
|
# Handle subtitles
|
||||||
|
self.signals.update_status.emit(_("main_ui.analyzing_processing_subtitles_ytdlp"))
|
||||||
|
self.selected_subtitles = []
|
||||||
|
self.available_subtitles = self.video_info.get("subtitles", {})
|
||||||
|
self.available_automatic_subtitles = self.video_info.get("automatic_captions", {})
|
||||||
|
|
||||||
|
# Update subtitle UI
|
||||||
|
self.signals.selected_subs_label_text.emit(_("main_ui.zero_selected"))
|
||||||
|
|
||||||
|
# Update format table
|
||||||
|
self.signals.update_status.emit(_("main_ui.analyzing_updating_table"))
|
||||||
|
self.video_button.setChecked(True)
|
||||||
|
self.audio_button.setChecked(False)
|
||||||
|
self.filter_formats()
|
||||||
|
|
||||||
|
self.signals.update_status.emit(_("main_ui.analysis_complete"))
|
||||||
|
|
||||||
|
# Mark analysis as complete and enable analysis-dependent controls
|
||||||
|
self.analysis_completed = True
|
||||||
|
QMetaObject.invokeMethod(
|
||||||
|
self,
|
||||||
|
"toggle_analysis_dependent_controls",
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
Q_ARG(bool, True),
|
||||||
|
)
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.error("Analysis timed out")
|
||||||
|
self.signals.update_status.emit(_("errors.timeout"))
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Unexpected error in analysis: {e}")
|
||||||
|
self.signals.update_status.emit(_("errors.generic_error", error=str(e)))
|
||||||
+2
-251
@@ -46,6 +46,7 @@ from src.gui.ytsage_gui_dialogs import ( # use of src\gui\ytsage_gui_dialogs\__
|
|||||||
)
|
)
|
||||||
from src.gui.ytsage_gui_format_table import FormatTableMixin
|
from src.gui.ytsage_gui_format_table import FormatTableMixin
|
||||||
from src.gui.ytsage_gui_video_info import VideoInfoMixin
|
from src.gui.ytsage_gui_video_info import VideoInfoMixin
|
||||||
|
from src.gui.ytsage_gui_analysis import AnalysisMixin
|
||||||
from src.utils.ytsage_constants import ICON_PATH, SOUND_PATH, SUBPROCESS_CREATIONFLAGS
|
from src.utils.ytsage_constants import ICON_PATH, SOUND_PATH, SUBPROCESS_CREATIONFLAGS
|
||||||
from src.utils.ytsage_logger import logger
|
from src.utils.ytsage_logger import logger
|
||||||
from src.utils.ytsage_config_manager import ConfigManager
|
from src.utils.ytsage_config_manager import ConfigManager
|
||||||
@@ -97,7 +98,7 @@ class UpdateCheckThread(QThread):
|
|||||||
logger.debug(f"Failed to check for updates: {e}")
|
logger.debug(f"Failed to check for updates: {e}")
|
||||||
|
|
||||||
|
|
||||||
class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from mixins
|
class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): # Inherit from mixins
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -457,51 +458,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
|
|||||||
"""Enable or disable the Analyze button based on URL input content."""
|
"""Enable or disable the Analyze button based on URL input content."""
|
||||||
self.analyze_button.setEnabled(bool(text.strip()))
|
self.analyze_button.setEnabled(bool(text.strip()))
|
||||||
|
|
||||||
def analyze_url(self) -> None:
|
|
||||||
if self.is_updating_ytdlp:
|
|
||||||
QMessageBox.warning(self, _("update.update_in_progress_title"), _("update.update_in_progress_message"))
|
|
||||||
return
|
|
||||||
|
|
||||||
url = self.url_input.text().strip()
|
|
||||||
if not url:
|
|
||||||
self.signals.update_status.emit(_("main_ui.invalid_url_or_enter"))
|
|
||||||
return
|
|
||||||
|
|
||||||
# Validate URL before processing
|
|
||||||
is_valid, error_message = validate_video_url(url)
|
|
||||||
if not is_valid:
|
|
||||||
QMessageBox.warning(self, _("main_ui.error_title"), error_message)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Reset analysis state and disable controls
|
|
||||||
self.analysis_completed = False
|
|
||||||
self.toggle_analysis_dependent_controls(enabled=False)
|
|
||||||
|
|
||||||
self.signals.update_status.emit(_("main_ui.analyzing_preparing"))
|
|
||||||
self.is_analyzing = True
|
|
||||||
threading.Thread(target=self._analyze_url_thread, args=(url,), daemon=True).start()
|
|
||||||
|
|
||||||
def _analyze_url_thread(self, url) -> None:
|
|
||||||
try:
|
|
||||||
self.signals.update_status.emit(_("main_ui.analyzing_extracting_basic"))
|
|
||||||
|
|
||||||
# Clean up the URL to handle both playlist and video URLs
|
|
||||||
if "list=" in url and "watch?v=" in url:
|
|
||||||
playlist_id = url.split("list=")[1].split("&")[0]
|
|
||||||
url = f"https://www.youtube.com/playlist?list={playlist_id}"
|
|
||||||
|
|
||||||
# Always use subprocess to call yt-dlp binary (Python package removed)
|
|
||||||
self._analyze_url_with_subprocess(url)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.exception(f"Error in analysis: {e}")
|
|
||||||
self.signals.update_status.emit(_("errors.generic_error", error=str(e)))
|
|
||||||
# Ensure playlist UI is hidden on error too
|
|
||||||
# update signal method from QMetaObject.invokeMethod to signals
|
|
||||||
self.signals.playlist_info_label_visible.emit(False)
|
|
||||||
self.signals.playlist_select_btn_visible.emit(False)
|
|
||||||
finally:
|
|
||||||
self.is_analyzing = False
|
|
||||||
|
|
||||||
def paste_url(self) -> None:
|
def paste_url(self) -> None:
|
||||||
clipboard = QApplication.clipboard()
|
clipboard = QApplication.clipboard()
|
||||||
@@ -1410,210 +1367,4 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
|
|||||||
success_dialog.setStyleSheet(StyleSheet.SETUP_SUCCESS_DIALOG)
|
success_dialog.setStyleSheet(StyleSheet.SETUP_SUCCESS_DIALOG)
|
||||||
success_dialog.exec()
|
success_dialog.exec()
|
||||||
|
|
||||||
def _analyze_url_with_subprocess(self, url) -> None:
|
|
||||||
"""Analyze URL using yt-dlp executable when Python module is not available"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
yt_dlp_path = get_yt_dlp_path()
|
|
||||||
if not yt_dlp_path:
|
|
||||||
logger.error("yt-dlp executable not found. Please install yt-dlp first.")
|
|
||||||
self.signals.update_status.emit(_("errors.ytdlp_not_found"))
|
|
||||||
self.signals.playlist_info_label_visible.emit(False)
|
|
||||||
self.signals.playlist_select_btn_visible.emit(False)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.signals.update_status.emit(_("main_ui.analyzing_extracting_ytdlp"))
|
|
||||||
|
|
||||||
# Clean up the URL to handle both playlist and video URLs
|
|
||||||
if "list=" in url and "watch?v=" in url:
|
|
||||||
playlist_id = url.split("list=")[1].split("&")[0]
|
|
||||||
url = f"https://www.youtube.com/playlist?list={playlist_id}"
|
|
||||||
|
|
||||||
# Build command for basic info extraction
|
|
||||||
# Use --flat-playlist for fast initial extraction of playlist info + --dump-single-json
|
|
||||||
# This fetches minimal info for all videos quickly without downloading full details for each
|
|
||||||
cmd = [yt_dlp_path, "--dump-single-json", "--flat-playlist", "--no-warnings", url]
|
|
||||||
|
|
||||||
# Add cookies if available
|
|
||||||
if self.cookie_file_path:
|
|
||||||
cmd.extend(["--cookies", str(self.cookie_file_path)])
|
|
||||||
elif self.browser_cookies_option:
|
|
||||||
cmd.extend(["--cookies-from-browser", self.browser_cookies_option])
|
|
||||||
|
|
||||||
# Add proxy settings if available
|
|
||||||
if self.proxy_url:
|
|
||||||
cmd.extend(["--proxy", self.proxy_url])
|
|
||||||
|
|
||||||
if self.geo_proxy_url:
|
|
||||||
cmd.extend(["--geo-verification-proxy", self.geo_proxy_url])
|
|
||||||
|
|
||||||
logger.debug(f"Executing yt-dlp command: {cmd}")
|
|
||||||
|
|
||||||
# Execute command with hidden console window on Windows
|
|
||||||
# Extra logic moved to src\utils\ytsage_constants.py
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300, creationflags=SUBPROCESS_CREATIONFLAGS)
|
|
||||||
|
|
||||||
if result.returncode != 0:
|
|
||||||
logger.error(f"yt-dlp failed: {result.stderr}")
|
|
||||||
self.signals.update_status.emit(_("errors.ytdlp_failed", error=result.stderr))
|
|
||||||
self.signals.playlist_info_label_visible.emit(False)
|
|
||||||
self.signals.playlist_select_btn_visible.emit(False)
|
|
||||||
return
|
|
||||||
|
|
||||||
json_lines = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
|
|
||||||
|
|
||||||
if not json_lines:
|
|
||||||
logger.error("No data returned from yt-dlp")
|
|
||||||
self.signals.update_status.emit(_("errors.no_data_returned"))
|
|
||||||
self.signals.playlist_info_label_visible.emit(False)
|
|
||||||
self.signals.playlist_select_btn_visible.emit(False)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
first_info = json.loads(json_lines[0])
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logger.error(f"Failed to parse yt-dlp output: {e}")
|
|
||||||
self.signals.update_status.emit(_("errors.parse_failed", error=str(e)))
|
|
||||||
self.signals.playlist_info_label_visible.emit(False)
|
|
||||||
self.signals.playlist_select_btn_visible.emit(False)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.signals.update_status.emit(_("main_ui.analyzing_processing_data"))
|
|
||||||
|
|
||||||
if first_info.get("_type") == "playlist":
|
|
||||||
# Handle playlist
|
|
||||||
self.is_playlist = True
|
|
||||||
self.playlist_info = first_info
|
|
||||||
self.selected_playlist_items = None
|
|
||||||
self.playlist_entries = first_info.get("entries", [])
|
|
||||||
|
|
||||||
if not self.playlist_entries:
|
|
||||||
logger.error("Playlist contains no valid videos.")
|
|
||||||
self.signals.update_status.emit(_("errors.playlist_no_videos"))
|
|
||||||
self.signals.playlist_info_label_visible.emit(False)
|
|
||||||
self.signals.playlist_select_btn_visible.emit(False)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Even with flat-playlist, we need one video's formats to populate the table.
|
|
||||||
# Just use the first video URL to get full details quickly.
|
|
||||||
self.signals.update_status.emit(_("main_ui.analyzing_fetching_first_video"))
|
|
||||||
first_video_entry = self.playlist_entries[0]
|
|
||||||
first_video_url = first_video_entry.get("url")
|
|
||||||
|
|
||||||
# Fetch full info for just the first video to get formats
|
|
||||||
cmd_single = [yt_dlp_path, "--dump-single-json", "--no-warnings", first_video_url]
|
|
||||||
# Add cookies & proxy to this single request too
|
|
||||||
if self.cookie_file_path:
|
|
||||||
cmd_single.extend(["--cookies", str(self.cookie_file_path)])
|
|
||||||
elif self.browser_cookies_option:
|
|
||||||
cmd_single.extend(["--cookies-from-browser", self.browser_cookies_option])
|
|
||||||
if self.proxy_url:
|
|
||||||
cmd_single.extend(["--proxy", self.proxy_url])
|
|
||||||
if self.geo_proxy_url:
|
|
||||||
cmd_single.extend(["--geo-verification-proxy", self.geo_proxy_url])
|
|
||||||
|
|
||||||
result_single = subprocess.run(cmd_single, capture_output=True, text=True, timeout=60, creationflags=SUBPROCESS_CREATIONFLAGS)
|
|
||||||
|
|
||||||
if result_single.returncode == 0:
|
|
||||||
self.video_info = json.loads(result_single.stdout)
|
|
||||||
else:
|
|
||||||
# Fallback to whatever minimal info we have, might fail table population
|
|
||||||
self.video_info = first_video_entry
|
|
||||||
|
|
||||||
# Update playlist info label
|
|
||||||
playlist_text = _("playlist.display_format",
|
|
||||||
title=first_info.get('title', _('playlist.unknown')),
|
|
||||||
count=len(self.playlist_entries))
|
|
||||||
# update signal method from QMetaObject.invokeMethod to signals
|
|
||||||
self.signals.playlist_info_label_text.emit(playlist_text)
|
|
||||||
self.signals.playlist_info_label_visible.emit(True)
|
|
||||||
|
|
||||||
# Show playlist selection button
|
|
||||||
# update signal method from QMetaObject.invokeMethod to signals
|
|
||||||
self.signals.playlist_select_btn_text.emit(_("main_ui.select_videos_all"))
|
|
||||||
|
|
||||||
# update signal method from QMetaObject.invokeMethod to signals
|
|
||||||
self.signals.playlist_select_btn_visible.emit(True)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Handle single video
|
|
||||||
self.is_playlist = False
|
|
||||||
self.video_info = first_info
|
|
||||||
self.playlist_entries = []
|
|
||||||
self.selected_playlist_items = None
|
|
||||||
|
|
||||||
# Hide playlist UI
|
|
||||||
# update signal method from QMetaObject.invokeMethod to signals
|
|
||||||
self.signals.playlist_info_label_visible.emit(False)
|
|
||||||
|
|
||||||
# update signal method from QMetaObject.invokeMethod to signals
|
|
||||||
self.signals.playlist_select_btn_visible.emit(False)
|
|
||||||
|
|
||||||
# Verify we have format information
|
|
||||||
if not self.video_info or "formats" not in self.video_info:
|
|
||||||
logger.error("No format information available")
|
|
||||||
self.signals.update_status.emit(_("errors.no_format_info"))
|
|
||||||
self.signals.playlist_info_label_visible.emit(False)
|
|
||||||
self.signals.playlist_select_btn_visible.emit(False)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.signals.update_status.emit(_("main_ui.analyzing_processing_formats_ytdlp"))
|
|
||||||
self.all_formats = self.video_info["formats"]
|
|
||||||
|
|
||||||
# Update UI
|
|
||||||
self.update_video_info(self.video_info)
|
|
||||||
|
|
||||||
# Update thumbnail
|
|
||||||
self.signals.update_status.emit(_("main_ui.analyzing_loading_thumbnail_ytdlp"))
|
|
||||||
# Try to get thumbnail from playlist info first
|
|
||||||
# Fallback to video thumbnail if playlist thumbnail not found or not a playlist
|
|
||||||
thumbnail_url = (self.playlist_info or {}).get("thumbnail") or (self.video_info or {}).get("thumbnail")
|
|
||||||
|
|
||||||
self.download_thumbnail(thumbnail_url)
|
|
||||||
|
|
||||||
# Save thumbnail if enabled
|
|
||||||
if self.save_thumbnail:
|
|
||||||
self.download_thumbnail_file(self.video_url, self.path_input.text()) # type: ignore[reportAttributeAccessIssue]
|
|
||||||
|
|
||||||
# Handle subtitles
|
|
||||||
self.signals.update_status.emit(_("main_ui.analyzing_processing_subtitles_ytdlp"))
|
|
||||||
self.selected_subtitles = []
|
|
||||||
self.available_subtitles = self.video_info.get("subtitles", {})
|
|
||||||
self.available_automatic_subtitles = self.video_info.get("automatic_captions", {})
|
|
||||||
|
|
||||||
# Update subtitle UI
|
|
||||||
# update signal method from QMetaObject.invokeMethod to signals
|
|
||||||
self.signals.selected_subs_label_text.emit(_("main_ui.zero_selected"))
|
|
||||||
|
|
||||||
# Update format table
|
|
||||||
self.signals.update_status.emit(_("main_ui.analyzing_updating_table"))
|
|
||||||
self.video_button.setChecked(True)
|
|
||||||
self.audio_button.setChecked(False)
|
|
||||||
self.filter_formats()
|
|
||||||
|
|
||||||
self.signals.update_status.emit(_("main_ui.analysis_complete"))
|
|
||||||
|
|
||||||
# Mark analysis as complete and enable analysis-dependent controls
|
|
||||||
self.analysis_completed = True
|
|
||||||
QMetaObject.invokeMethod(
|
|
||||||
self,
|
|
||||||
"toggle_analysis_dependent_controls",
|
|
||||||
Qt.ConnectionType.QueuedConnection,
|
|
||||||
Q_ARG(bool, True),
|
|
||||||
)
|
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
logger.error("Analysis timed out. Please try again.")
|
|
||||||
self.signals.update_status.emit(_("errors.analysis_timeout"))
|
|
||||||
self.signals.playlist_info_label_visible.emit(False)
|
|
||||||
self.signals.playlist_select_btn_visible.emit(False)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logger.error(f"Failed to parse yt-dlp output: {e}")
|
|
||||||
self.signals.update_status.emit(f"Error: Failed to parse yt-dlp output: {e}")
|
|
||||||
self.signals.playlist_info_label_visible.emit(False)
|
|
||||||
self.signals.playlist_select_btn_visible.emit(False)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Analysis failed: {e}")
|
|
||||||
self.signals.update_status.emit(_("errors.analysis_failed", error=str(e)))
|
|
||||||
self.signals.playlist_info_label_visible.emit(False)
|
|
||||||
self.signals.playlist_select_btn_visible.emit(False)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user