From ed1469898c4cac6ca6b9070811b9407c6fecae49 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:28:06 +0200 Subject: [PATCH] Add configurable output filename format Introduce a new filename_format setting and UI to control yt-dlp output templates. ConfigManager now includes a default filename_format (%(title)s_%(resolution)s.%(ext)s). The DownloadSettingsDialog exposes a text input and help text for the format and saves the value to ConfigManager. DownloadThread now accepts a filename_format argument and uses it when building output templates for single videos and playlists. YTSageApp reads the config and passes the filename format into the download thread. Added corresponding English language strings. --- ytsage/core/ytsage_downloader.py | 9 ++++-- .../ytsage_dialogs_settings.py | 28 +++++++++++++++++++ ytsage/gui/ytsage_gui_main.py | 4 +++ ytsage/languages/en.json | 2 ++ ytsage/utils/ytsage_config_manager.py | 1 + 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/ytsage/core/ytsage_downloader.py b/ytsage/core/ytsage_downloader.py index a1b1252..0647152 100644 --- a/ytsage/core/ytsage_downloader.py +++ b/ytsage/core/ytsage_downloader.py @@ -72,6 +72,7 @@ class DownloadThread(QThread): preferred_output_format="mp4", force_audio_format=False, preferred_audio_format="best", + filename_format=None, ) -> None: super().__init__() self.url = url @@ -99,6 +100,7 @@ class DownloadThread(QThread): self.preferred_output_format = preferred_output_format self.force_audio_format = force_audio_format self.preferred_audio_format = preferred_audio_format + self.filename_format = filename_format self.paused: bool = False self.cancelled: bool = False self.process: Optional[subprocess.Popen] = None @@ -274,11 +276,14 @@ class DownloadThread(QThread): # Use string concatenation instead of Path.joinpath to avoid Path object issues base_path: str = self.path.as_posix() + # Determine the filename part of the template + filename_part = self.filename_format if self.filename_format else "%(title)s_%(resolution)s.%(ext)s" + if self.is_playlist: # Create output template with playlist subfolder - output_template: str = f"{base_path}/%(playlist_title)s/%(title)s_%(resolution)s.%(ext)s" + output_template: str = f"{base_path}/%(playlist_title)s/{filename_part}" else: - output_template: str = f"{base_path}/%(title)s_%(resolution)s.%(ext)s" + output_template: str = f"{base_path}/{filename_part}" cmd.extend(["-o", str(output_template)]) diff --git a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py index 3b68fd7..535f73a 100644 --- a/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py +++ b/ytsage/gui/ytsage_gui_dialogs/ytsage_dialogs_settings.py @@ -280,6 +280,25 @@ class DownloadSettingsDialog(QDialog): audio_format_group_box.setLayout(audio_format_layout) layout.addWidget(audio_format_group_box) + # --- Filename Format Section --- + filename_format_group_box = QGroupBox(_("settings.filename_format")) + filename_layout = QVBoxLayout() + + # Load current filename format from ConfigManager + self.filename_format_value = ConfigManager.get("filename_format") or "%(title)s_%(resolution)s.%(ext)s" + + self.filename_format_input = QLineEdit(self.filename_format_value) + self.filename_format_input.setPlaceholderText("%(title)s_%(resolution)s.%(ext)s") + filename_layout.addWidget(self.filename_format_input) + + filename_help_label = QLabel(_("settings.filename_format_help")) + filename_help_label.setWordWrap(True) + filename_help_label.setStyleSheet("color: #cccccc; margin: 5px; font-size: 10px;") + filename_layout.addWidget(filename_help_label) + + filename_format_group_box.setLayout(filename_layout) + layout.addWidget(filename_format_group_box) + # Dialog buttons (OK/Cancel) button_box = QDialogButtonBox() ok_button = button_box.addButton(_("buttons.ok"), QDialogButtonBox.ButtonRole.AcceptRole) @@ -332,6 +351,10 @@ class DownloadSettingsDialog(QDialog): audio_format_map = {0: "best", 1: "aac", 2: "mp3", 3: "flac", 4: "wav", 5: "opus", 6: "m4a", 7: "vorbis"} return audio_format_map.get(self.audio_format_combo.currentIndex(), "best") + def get_filename_format(self) -> str: + """Returns the filename format string.""" + return self.filename_format_input.text().strip() + def _create_styled_message_box(self, icon, title, text) -> QMessageBox: """Create a styled QMessageBox that matches the app theme.""" msg_box = QMessageBox(self) @@ -382,6 +405,11 @@ class DownloadSettingsDialog(QDialog): ConfigManager.set("force_audio_format", force_audio_format) ConfigManager.set("preferred_audio_format", preferred_audio_format) + # Save filename format + filename_format = self.get_filename_format() + if filename_format: + ConfigManager.set("filename_format", filename_format) + QMessageBox.information( self, _("settings.settings_saved_title"), diff --git a/ytsage/gui/ytsage_gui_main.py b/ytsage/gui/ytsage_gui_main.py index 5b1c7bb..3a55ec9 100644 --- a/ytsage/gui/ytsage_gui_main.py +++ b/ytsage/gui/ytsage_gui_main.py @@ -678,6 +678,9 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): logger.warning(f"Thumbnail download failed: {e}", exc_info=True) # Optionally inform the user, but don't stop the main download + # Get filename format from config + filename_format = ConfigManager.get("filename_format") + # Create download thread with resolution in output template self.download_thread = DownloadThread( url=url, @@ -705,6 +708,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin, AnalysisMixin): preferred_output_format=self.preferred_output_format, # Pass preferred format force_audio_format=self.force_audio_format, # Pass force audio format setting preferred_audio_format=self.preferred_audio_format, # Pass preferred audio format + filename_format=filename_format, # Pass the filename format ) # Connect signals diff --git a/ytsage/languages/en.json b/ytsage/languages/en.json index 9281bb3..fef8ddc 100644 --- a/ytsage/languages/en.json +++ b/ytsage/languages/en.json @@ -318,6 +318,8 @@ "format_mkv": "MKV (Feature-rich)", "audio_format_settings": "Audio Format Settings", "force_audio_format": "Force audio format for audio-only downloads", + "filename_format": "Output Filename Format", + "filename_format_help": "Available variables: %(title)s, %(uploader)s, %(upload_date)s, %(resolution)s, %(id)s, %(ext)s. Standard yt-dlp output template syntax is supported.", "preferred_audio_format": "Preferred audio format:", "force_audio_format_help": "When enabled, audio-only downloads will be converted to your preferred format. This only applies when downloading audio formats.", "audio_format_best": "Best (No conversion)", diff --git a/ytsage/utils/ytsage_config_manager.py b/ytsage/utils/ytsage_config_manager.py index c4b5d3d..919d546 100644 --- a/ytsage/utils/ytsage_config_manager.py +++ b/ytsage/utils/ytsage_config_manager.py @@ -90,6 +90,7 @@ class ConfigManager: "preferred_output_format": "mp4", "force_audio_format": False, "preferred_audio_format": "best", + "filename_format": "%(title)s_%(resolution)s.%(ext)s", "cached_versions": { "ytdlp": {"version": None, "path": None, "last_check": 0, "path_mtime": 0}, "ffmpeg": {"version": None, "path": None, "last_check": 0, "path_mtime": 0},