Merge branch 'beta'
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "ytsage"
|
||||
version = "5.1.0"
|
||||
version = "5.2.0b"
|
||||
description = "Modern YouTube downloader with a clean PySide6 interface."
|
||||
authors = [
|
||||
{ name = "oop7", email = "oop7_support@proton.me" },
|
||||
|
||||
+1
-1
@@ -4,5 +4,5 @@ YTSage - YouTube Video Downloader
|
||||
A modern, user-friendly YouTube video downloader built with PySide6.
|
||||
"""
|
||||
|
||||
__version__ = "5.1.0"
|
||||
__version__ = "5.2.0b"
|
||||
__author__ = "oop7"
|
||||
|
||||
@@ -402,25 +402,26 @@ def load_saved_path(main_window_instance: Any) -> None:
|
||||
main_window_instance.last_path = tempfile.gettempdir()
|
||||
|
||||
|
||||
from ..utils.ytsage_config_manager import ConfigManager
|
||||
|
||||
def save_path(main_window_instance: Any, path: Union[str, Path]) -> bool:
|
||||
"""Save download path with enhanced error handling."""
|
||||
try:
|
||||
# Verify the path is valid and writable
|
||||
if not Path(path).exists():
|
||||
path_str = str(path)
|
||||
if not Path(path_str).exists():
|
||||
try:
|
||||
Path(path).mkdir(exist_ok=True)
|
||||
Path(path_str).mkdir(parents=True, exist_ok=True)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error creating directory: {e}")
|
||||
logger.error(f"Error creating directory: {e}")
|
||||
return False
|
||||
|
||||
if not os.access(path, os.W_OK):
|
||||
logger.info("Path is not writable")
|
||||
if not os.access(path_str, os.W_OK):
|
||||
logger.error("Path is not writable")
|
||||
return False
|
||||
|
||||
# Save the config
|
||||
config = {"download_path": path}
|
||||
with open(APP_CONFIG_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, ensure_ascii=False)
|
||||
# Save the config using ConfigManager
|
||||
ConfigManager.set("download_path", path_str)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -255,7 +255,8 @@ class DownloadSettingsDialog(QDialog):
|
||||
generic_mode_group_box = QGroupBox(_("settings.generic_mode"))
|
||||
generic_mode_layout = QVBoxLayout()
|
||||
|
||||
self.generic_mode_enabled = ConfigManager.get("generic_mode") or False
|
||||
generic_val = ConfigManager.get("generic_mode")
|
||||
self.generic_mode_enabled = generic_val if generic_val is not None else True
|
||||
|
||||
self.generic_mode_checkbox = QCheckBox(_("settings.enable_generic_mode"))
|
||||
self.generic_mode_checkbox.setChecked(self.generic_mode_enabled)
|
||||
|
||||
@@ -22,6 +22,34 @@ class FormatTableMixin:
|
||||
text_width = font_metrics.horizontalAdvance(label)
|
||||
return max(text_width + padding, min_width)
|
||||
|
||||
def _get_audio_codec_display(self, format_info: dict) -> str:
|
||||
"""
|
||||
Generate a display string for audio codec information.
|
||||
Highlights EAC3/AC3 and other surround sound formats with channel info.
|
||||
"""
|
||||
acodec = format_info.get("acodec")
|
||||
if not acodec or acodec == "none":
|
||||
return "N/A"
|
||||
|
||||
channels = format_info.get("audio_channels")
|
||||
abr = format_info.get("abr")
|
||||
|
||||
# Build codec string
|
||||
codec_str = str(acodec)
|
||||
|
||||
# Add channel info for surround sound formats
|
||||
if channels:
|
||||
codec_str += f" ({channels}ch)"
|
||||
|
||||
# Add bitrate if available
|
||||
if abr and isinstance(abr, (int, float)):
|
||||
if abr >= 1000:
|
||||
codec_str += f" {abr/1000:.1f}Mbps"
|
||||
else:
|
||||
codec_str += f" {int(abr)}kbps"
|
||||
|
||||
return codec_str
|
||||
|
||||
def _apply_column_widths(self, header_labels: list[str], is_playlist_mode: bool = False) -> None:
|
||||
"""Apply responsive column widths to format table."""
|
||||
self = cast("YTSageApp", self)
|
||||
@@ -209,7 +237,8 @@ class FormatTableMixin:
|
||||
for f in self.all_formats
|
||||
if (f.get("vcodec") == "none" or "audio only" in str(f.get("format_note") or "").lower())
|
||||
and f.get("acodec") != "none"
|
||||
and f.get("filesize") is not None
|
||||
# Removed filesize requirement - many audio-only formats (including EAC3/AC3) may not have filesize in yt-dlp output
|
||||
# Include all audio formats to detect EAC3/AC3 and other surround sound codecs
|
||||
]
|
||||
|
||||
# Sort formats by quality
|
||||
@@ -399,14 +428,41 @@ class FormatTableMixin:
|
||||
|
||||
# Audio Status column
|
||||
needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none"
|
||||
audio_status = _("formats.will_merge_audio") if needs_audio else (_("formats.has_audio") if f.get("vcodec") != "none" else _("formats.audio_only"))
|
||||
audio_item = QTableWidgetItem(audio_status)
|
||||
acodec = f.get("acodec")
|
||||
|
||||
# Enhanced audio status with support for surround sound formats (EAC3/AC3)
|
||||
if needs_audio:
|
||||
audio_item.setForeground(QColor("#ffa500"))
|
||||
elif audio_status == _("formats.audio_only"):
|
||||
audio_item.setForeground(QColor("#cccccc"))
|
||||
audio_status = _("formats.will_merge_audio")
|
||||
audio_color = QColor("#ffa500")
|
||||
elif f.get("vcodec") != "none":
|
||||
# Video format with audio
|
||||
if acodec in ["ac3", "eac3"]:
|
||||
# Highlight surround sound codecs
|
||||
channels = f.get("audio_channels", "")
|
||||
if channels:
|
||||
audio_status = f"{_('formats.has_audio')} - {acodec.upper()} {channels}ch"
|
||||
else:
|
||||
audio_status = f"{_('formats.has_audio')} - {acodec.upper()}"
|
||||
audio_color = QColor("#00ccff") # Cyan for surround sound
|
||||
else:
|
||||
audio_status = _("formats.has_audio")
|
||||
audio_color = QColor("#00cc00")
|
||||
else:
|
||||
audio_item.setForeground(QColor("#00cc00"))
|
||||
# Audio-only format
|
||||
if acodec in ["ac3", "eac3"]:
|
||||
# Highlight surround sound audio-only
|
||||
channels = f.get("audio_channels", "")
|
||||
if channels:
|
||||
audio_status = f"{acodec.upper()} {channels}ch"
|
||||
else:
|
||||
audio_status = acodec.upper()
|
||||
audio_color = QColor("#00ccff") # Cyan for surround sound
|
||||
else:
|
||||
audio_status = _("formats.audio_only")
|
||||
audio_color = QColor("#cccccc")
|
||||
|
||||
audio_item = QTableWidgetItem(audio_status)
|
||||
audio_item.setForeground(audio_color)
|
||||
audio_column_index = 5 if is_playlist_mode else 6
|
||||
self.format_table.setItem(row, audio_column_index, audio_item)
|
||||
|
||||
@@ -421,11 +477,22 @@ class FormatTableMixin:
|
||||
|
||||
# Column 5: Codec
|
||||
if f.get("vcodec") == "none":
|
||||
# Audio-only format - display codec with channel info
|
||||
codec = str(f.get("acodec") or "N/A")
|
||||
# Add audio channel information if available (e.g., for 5.1 detection)
|
||||
channels = f.get("audio_channels")
|
||||
if channels:
|
||||
codec += f" ({channels}ch)"
|
||||
else:
|
||||
# Video format - display video codec and audio codec if present
|
||||
codec = str(f.get("vcodec") or "N/A")
|
||||
if f.get("acodec") != "none":
|
||||
codec += f" / {str(f.get('acodec') or 'N/A')}"
|
||||
acodec = str(f.get("acodec") or "N/A")
|
||||
# Add audio channel info for video with audio
|
||||
channels = f.get("audio_channels")
|
||||
if channels:
|
||||
acodec += f" ({channels}ch)"
|
||||
codec += f" / {acodec}"
|
||||
self.format_table.setItem(row, 5, QTableWidgetItem(codec))
|
||||
|
||||
# Column 7: FPS (Frame Rate)
|
||||
|
||||
@@ -249,8 +249,9 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
# Initialize proxy settings from config
|
||||
self.proxy_url = ConfigManager.get("proxy_url")
|
||||
self.geo_proxy_url = ConfigManager.get("geo_proxy_url")
|
||||
self.speed_limit_value = None # Store speed limit value
|
||||
self.speed_limit_unit_index = 0 # Store speed limit unit index (0: KB/s, 1: MB/s)
|
||||
# Initialize speed limit settings from config
|
||||
self.speed_limit_value = ConfigManager.get("speed_limit_value") # Store speed limit value
|
||||
self.speed_limit_unit_index = ConfigManager.get("speed_limit_unit_index") or 0 # Store speed limit unit index (0: KB/s, 1: MB/s)
|
||||
self.download_section = None
|
||||
self.force_keyframes = False
|
||||
# Initialize output format settings
|
||||
@@ -259,7 +260,10 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
self.force_audio_format = ConfigManager.get("force_audio_format") or False
|
||||
self.preferred_audio_format = ConfigManager.get("preferred_audio_format") or "best"
|
||||
self.audio_normalization = ConfigManager.get("audio_normalization") or False
|
||||
self.generic_mode_enabled = ConfigManager.get("generic_mode") or False
|
||||
|
||||
generic_val = ConfigManager.get("generic_mode")
|
||||
self.generic_mode_enabled = generic_val if generic_val is not None else True
|
||||
|
||||
# Track if video analysis is completed
|
||||
self.analysis_completed = False
|
||||
|
||||
@@ -269,6 +273,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
self.player.setAudioOutput(self.audio_output)
|
||||
|
||||
self.init_ui()
|
||||
self._load_window_state()
|
||||
|
||||
# Defer heavy start-up tasks to ensure UI renders immediately
|
||||
QTimer.singleShot(100, self._perform_startup_checks)
|
||||
@@ -287,6 +292,22 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
# Initialize UI state based on current mode
|
||||
self.handle_mode_change()
|
||||
|
||||
def _load_window_state(self):
|
||||
"""Restore previous window geometry and state from config."""
|
||||
from PySide6.QtCore import QByteArray
|
||||
geo_b64 = ConfigManager.get("window_geometry")
|
||||
if geo_b64:
|
||||
try:
|
||||
self.restoreGeometry(QByteArray.fromBase64(geo_b64.encode("ascii")))
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to restore geometry: {e}")
|
||||
|
||||
state_b64 = ConfigManager.get("window_state")
|
||||
if state_b64:
|
||||
try:
|
||||
self.restoreState(QByteArray.fromBase64(state_b64.encode("ascii")))
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to restore state: {e}")
|
||||
|
||||
def _perform_startup_checks(self):
|
||||
"""Perform potentially blocking startup checks after UI is shown."""
|
||||
@@ -659,6 +680,9 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
logger.info(
|
||||
f"Speed limit updated to: {self.speed_limit_value} {['KB/s', 'MB/s'][self.speed_limit_unit_index] if self.speed_limit_value else 'None'}"
|
||||
)
|
||||
# Save speed limit settings to config
|
||||
ConfigManager.set("speed_limit_value", self.speed_limit_value)
|
||||
ConfigManager.set("speed_limit_unit_index", self.speed_limit_unit_index)
|
||||
|
||||
# Update Output Format Settings
|
||||
new_force_format = dialog.get_force_format_enabled()
|
||||
@@ -1191,6 +1215,13 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin):
|
||||
self.current_download.terminate()
|
||||
self.current_download.wait(1000) # Wait for termination
|
||||
|
||||
# Save the window size and state
|
||||
try:
|
||||
ConfigManager.set("window_geometry", self.saveGeometry().toBase64().data().decode("ascii"))
|
||||
ConfigManager.set("window_state", self.saveState().toBase64().data().decode("ascii"))
|
||||
except Exception as w_e:
|
||||
logger.debug(f"Failed to save window state: {w_e}")
|
||||
|
||||
logger.info("Application closing...")
|
||||
event.accept()
|
||||
except Exception as e:
|
||||
|
||||
@@ -71,7 +71,7 @@ class ConfigManager:
|
||||
_settings: Dict[str, Any] = {}
|
||||
_default_config: Dict[str, Any] = {
|
||||
"download_path": str(USER_HOME_DIR / "Downloads"),
|
||||
"generic_mode": False,
|
||||
"generic_mode": True,
|
||||
"speed_limit_value": None,
|
||||
"speed_limit_unit_index": 0,
|
||||
"cookie_source": "browser", # "browser" or "file"
|
||||
@@ -96,6 +96,8 @@ class ConfigManager:
|
||||
"preferred_audio_format": "best",
|
||||
"audio_normalization": False,
|
||||
"filename_format": "%(title)s_%(resolution)s_[%(id)s].%(ext)s",
|
||||
"window_geometry": None,
|
||||
"window_state": None,
|
||||
"cached_versions": {
|
||||
"ytdlp": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
|
||||
"ffmpeg": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},
|
||||
|
||||
Reference in New Issue
Block a user