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},