From e43b1bbb2eeb1b31bd0e198b6937f2cfb5c38b88 Mon Sep 17 00:00:00 2001 From: oop7 <110548351+oop7@users.noreply.github.com> Date: Sat, 24 Jan 2026 17:21:24 +0200 Subject: [PATCH] Optimize format table filtering and rebuilding Refactors the format table logic to build the table once and use row visibility for fast filtering between video and audio formats. Adds internal flags to track table state and format types per row, improving performance and responsiveness when toggling format filters. Also streamlines table population and color-coding logic. --- src/gui/ytsage_gui_format_table.py | 204 +++++++++++++++-------------- 1 file changed, 106 insertions(+), 98 deletions(-) diff --git a/src/gui/ytsage_gui_format_table.py b/src/gui/ytsage_gui_format_table.py index 48c2c6a..bf03cd3 100644 --- a/src/gui/ytsage_gui_format_table.py +++ b/src/gui/ytsage_gui_format_table.py @@ -158,6 +158,8 @@ class FormatTableMixin: # Store format checkboxes and formats self.format_checkboxes = [] self.all_formats = [] + self._row_format_type = [] # Track format type per row: 'video' or 'audio' + self._table_built = False # Track if table has been built with current formats # Set table size policies self.format_table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) @@ -173,29 +175,42 @@ class FormatTableMixin: def filter_formats(self) -> None: self = cast("YTSageApp", self) # for autocompletion and type inference. - if not hasattr(self, "all_formats"): + if not hasattr(self, "all_formats") or not self.all_formats: return + # Check if we need to rebuild the table (first time or formats changed) + if not self._table_built: + self._build_full_format_table() + return + + # Use row visibility for fast filtering instead of rebuilding table + show_video = hasattr(self, "video_button") and self.video_button.isChecked() # type: ignore[reportAttributeAccessIssue] + show_audio = hasattr(self, "audio_button") and self.audio_button.isChecked() # type: ignore[reportAttributeAccessIssue] + + for row, format_type in enumerate(self._row_format_type): + if format_type == "video": + self.format_table.setRowHidden(row, not show_video) + else: # audio + self.format_table.setRowHidden(row, not show_audio) + + def _build_full_format_table(self) -> None: + """Build the complete format table once with all formats.""" + self = cast("YTSageApp", self) # for autocompletion and type inference. + # Clear current table self.format_table.setRowCount(0) self.format_checkboxes.clear() + self._row_format_type.clear() - # Determine which formats to show - filtered_formats = [] - - if hasattr(self, "video_button") and self.video_button.isChecked(): # type: ignore[reportAttributeAccessIssue] - filtered_formats.extend([f for f in self.all_formats if f.get("vcodec") != "none" and f.get("filesize") is not None]) - - if hasattr(self, "audio_button") and self.audio_button.isChecked(): # type: ignore[reportAttributeAccessIssue] - filtered_formats.extend( - [ - f - for f in self.all_formats - if (f.get("vcodec") == "none" or "audio only" in f.get("format_note", "").lower()) - and f.get("acodec") != "none" - and f.get("filesize") is not None - ] - ) + # Separate and filter formats + video_formats = [f for f in self.all_formats if f.get("vcodec") != "none" and f.get("filesize") is not None] + audio_formats = [ + f + for f in self.all_formats + if (f.get("vcodec") == "none" or "audio only" in f.get("format_note", "").lower()) + and f.get("acodec") != "none" + and f.get("filesize") is not None + ] # Sort formats by quality def get_quality(f): @@ -211,17 +226,23 @@ class FormatTableMixin: else: return f.get("abr", 0) - filtered_formats.sort(key=get_quality, reverse=True) + video_formats.sort(key=get_quality, reverse=True) + audio_formats.sort(key=get_quality, reverse=True) - # Update table with filtered formats - self.format_signals.format_update.emit(filtered_formats) + # Combine: video first, then audio (maintains logical grouping) + all_filtered = [(f, "video") for f in video_formats] + [(f, "audio") for f in audio_formats] - def _update_format_table(self, formats) -> None: + # Build table with format type tracking + self._populate_format_table(all_filtered) + self._table_built = True + + # Apply initial visibility based on current button states + self.filter_formats() + + def _populate_format_table(self, formats_with_types: list) -> None: + """Populate the format table with formats and their types.""" self = cast("YTSageApp", self) # for autocompletion and type inference. - self.format_table.setRowCount(0) - self.format_checkboxes.clear() - is_playlist_mode = hasattr(self, "is_playlist") and self.is_playlist # type: ignore[reportAttributeAccessIssue] # Configure columns based on mode @@ -252,119 +273,103 @@ class FormatTableMixin: _("formats.hdr"), ] self.format_table.setHorizontalHeaderLabels(header_labels) - - # Ensure all columns are visible - for i in range(2, 9): - self.format_table.setColumnHidden(i, False) - - # Apply responsive column widths for normal mode self._apply_column_widths(header_labels, is_playlist_mode=False) - - for f in formats: + for f, format_type in formats_with_types: row = self.format_table.rowCount() self.format_table.insertRow(row) + self._row_format_type.append(format_type) - # Column 0: Select Checkbox (Always shown) + # Create checkbox widget checkbox = QCheckBox() - checkbox.format_id = str(f.get("format_id", "")) # type: ignore[reportAttributeAccessIssue] - checkbox.is_audio_only = bool((f.get("vcodec") or "none").lower() == "none") # type: ignore[attr-defined] - checkbox.has_audio = bool(f.get("acodec") and f.get("acodec") != "none") # type: ignore[attr-defined] + checkbox.setStyleSheet("QCheckBox { margin-left: 8px; }") + checkbox.format_id = f["format_id"] + checkbox.is_audio_only = f.get("vcodec") == "none" + checkbox.has_audio = f.get("acodec") != "none" checkbox.clicked.connect(lambda checked, cb=checkbox: self.handle_checkbox_click(cb)) self.format_checkboxes.append(checkbox) - checkbox_widget = QWidget() - checkbox_widget.setStyleSheet("background-color: transparent;") - checkbox_layout = QHBoxLayout(checkbox_widget) + + # Create a container widget for the checkbox + checkbox_container = QWidget() + checkbox_layout = QHBoxLayout(checkbox_container) checkbox_layout.addWidget(checkbox) checkbox_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) checkbox_layout.setContentsMargins(0, 0, 0, 0) - checkbox_layout.setSpacing(0) - self.format_table.setCellWidget(row, 0, checkbox_widget) + self.format_table.setCellWidget(row, 0, checkbox_container) - # Column 1: Quality (Always shown) - quality_text = self.get_quality_label(f) - quality_item = QTableWidgetItem(quality_text) - # Set color based on quality (check English, Spanish, Portuguese, Russian, Chinese, German, French, Hindi, Indonesian, Turkish, Polish, Italian, Arabic, and Japanese terms) - quality_lower = quality_text.lower() # Make comparison case-insensitive + # Quality label with color coding + quality_label = self.get_quality_label(f) + quality_item = QTableWidgetItem(quality_label) + # Set color based on quality (check multiple language terms) + quality_lower = quality_label.lower() if any(term.lower() in quality_lower for term in ["Best", "Óptima", "Mejor", "Melhor", "Лучшее", "最佳", "Beste", "Meilleure", "सर्वोत्तम", "Terbaik", "En iyi", "Najlepsza", "Najlepszy", "Najlepsze", "Migliore", "Miglior", "الأفضل", "أفضل", "最高"]): quality_item.setForeground(QColor("#00ff00")) # Green for best quality - elif any(term.lower() in quality_lower for term in ["High", "Alta", "Alto", "Áudio Alto", "Audio Alto", "Высокое", "高清", "高质量", "Hoch", "Haute", "Élevé", "Audio élevé", "उच्च", "उच्च ऑडियो", "Tinggi", "Audio tinggi", "Yüksek", "Yüksek ses", "Wysoka", "Wysoki", "Wysokie", "Alta", "Audio alto", "عالية", "عالي", "صوت عالي", "高", "高音質"]): + elif any(term.lower() in quality_lower for term in ["High", "Alta", "Alto", "Áudio Alto", "Audio Alto", "Высокое", "高清", "高质量", "Hoch", "Haute", "Élevé", "Audio élevé", "उच्च", "उच्च ऑडियो", "Tinggi", "Audio tinggi", "Yüksek", "Yüksek ses", "Wysoka", "Wysoki", "Wysokie", "Audio alto", "عالية", "عالي", "صوت عالي", "高", "高音質"]): quality_item.setForeground(QColor("#00cc00")) # Light green for high quality - elif any(term.lower() in quality_lower for term in ["Medium", "Media", "Medio", "Média", "Áudio Médio", "Audio Medio", "Среднее", "中等", "Mittel", "Moyenne", "Audio moyen", "मध्यम", "मध्यम ऑडियो", "Sedang", "Audio sedang", "Orta", "Orta ses", "Średnia", "Średni", "Średnie", "Media", "Audio medio", "متوسطة", "متوسط", "صوت متوسط", "中", "中音質"]): + elif any(term.lower() in quality_lower for term in ["Medium", "Media", "Medio", "Média", "Áudio Médio", "Audio Medio", "Среднее", "中等", "Mittel", "Moyenne", "Audio moyen", "मध्यम", "मध्यम ऑडियो", "Sedang", "Audio sedang", "Orta", "Orta ses", "Średnia", "Średni", "Średnie", "Audio medio", "متوسطة", "متوسط", "صوت متوسط", "中", "中音質"]): quality_item.setForeground(QColor("#ffaa00")) # Orange for medium quality elif any(term.lower() in quality_lower for term in ["Low", "Baja", "Bajo", "Baixa", "Áudio Baixo", "Audio Bajo", "Низкое", "低质量", "Niedrig", "Niedriges Audio", "Faible", "Audio faible", "Qualité faible", "निम्न", "निम्न ऑडियो", "निम्न गुणवत्ता", "Rendah", "Audio rendah", "Kualitas rendah", "Düşük", "Düşük ses", "Düşük kalite", "Niska", "Niski", "Niskie", "Bassa", "Audio basso", "Bassa qualità", "منخفضة", "منخفض", "صوت منخفض", "جودة منخفضة", "低", "低音質", "低品質"]): quality_item.setForeground(QColor("#ff5555")) # Red for low quality self.format_table.setItem(row, 1, quality_item) - # --- Populate columns common to both modes (Moved outside the 'if not is_playlist_mode' block) --- - - # Column 2: Resolution (Always shown) + # Resolution resolution = f.get("resolution", "N/A") - if f.get("vcodec") == "none": - resolution = _("formats.audio_only_resolution") - self.format_table.setItem(row, 2, QTableWidgetItem(resolution)) - # Column 3: FPS for playlist mode, Extension for normal mode if is_playlist_mode: - # Get FPS for playlist mode + # Column 2 for playlist mode: Resolution + self.format_table.setItem(row, 2, QTableWidgetItem(resolution)) + + # Column 3: FPS (Frame Rate) fps_value = f.get("fps") - if fps_value is not None: - # Format FPS value appropriately - if fps_value >= 1: - fps_text = f"{fps_value:.0f}fps" - else: - fps_text = "N/A" # Very low fps like storyboards + if fps_value is not None and fps_value >= 1: + fps_text = f"{fps_value:.0f}fps" else: fps_text = "N/A" - fps_item = QTableWidgetItem(fps_text) - # Color code based on FPS value if fps_value and fps_value >= 60: - fps_item.setForeground(QColor("#00ff00")) # Green for 60+ fps + fps_item.setForeground(QColor("#00ff00")) elif fps_value and fps_value >= 30: - fps_item.setForeground(QColor("#ffaa00")) # Orange for 30+ fps + fps_item.setForeground(QColor("#ffaa00")) elif fps_value and fps_value >= 1: - fps_item.setForeground(QColor("#ff5555")) # Red for low fps + fps_item.setForeground(QColor("#ff5555")) else: - fps_item.setForeground(QColor("#888888")) # Gray for N/A + fps_item.setForeground(QColor("#888888")) self.format_table.setItem(row, 3, fps_item) - - # Column 4: HDR for playlist mode + + # Column 4: HDR if f.get("vcodec") == "none": - # Audio-only formats don't have HDR hdr_text = "N/A" hdr_item = QTableWidgetItem(hdr_text) - hdr_item.setForeground(QColor("#888888")) # Gray for N/A + hdr_item.setForeground(QColor("#888888")) else: hdr_value = f.get("dynamic_range") if hdr_value and hdr_value != "SDR": hdr_text = hdr_value hdr_item = QTableWidgetItem(hdr_text) - hdr_item.setForeground(QColor("#00ffff")) # Cyan for HDR + hdr_item.setForeground(QColor("#00ffff")) else: hdr_text = "SDR" hdr_item = QTableWidgetItem(hdr_text) - hdr_item.setForeground(QColor("#888888")) # Gray for SDR + hdr_item.setForeground(QColor("#888888")) self.format_table.setItem(row, 4, hdr_item) else: # Extension for normal mode (column 2) self.format_table.setItem(row, 2, QTableWidgetItem(f.get("ext", "").upper())) - # Column 4 in playlist mode, Column 6 in normal mode: Audio Status - needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none" # Only mark video-only as needing merge + # 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) if needs_audio: audio_item.setForeground(QColor("#ffa500")) elif audio_status == _("formats.audio_only"): - audio_item.setForeground(QColor("#cccccc")) # Neutral color for audio only - else: # Has Audio (Video+Audio) - audio_item.setForeground(QColor("#00cc00")) # Green for included audio - # Set item for correct column based on mode + audio_item.setForeground(QColor("#cccccc")) + else: + audio_item.setForeground(QColor("#00cc00")) audio_column_index = 5 if is_playlist_mode else 6 self.format_table.setItem(row, audio_column_index, audio_item) - # --- Populate columns only shown in non-playlist mode --- + # Populate columns only shown in non-playlist mode if not is_playlist_mode: # Column 3: Resolution self.format_table.setItem(row, 3, QTableWidgetItem(resolution)) @@ -384,45 +389,47 @@ class FormatTableMixin: # Column 7: FPS (Frame Rate) fps_value = f.get("fps") - if fps_value is not None: - # Format FPS value appropriately - if fps_value >= 1: - fps_text = f"{fps_value:.0f}fps" - else: - fps_text = "N/A" # Very low fps like storyboards + if fps_value is not None and fps_value >= 1: + fps_text = f"{fps_value:.0f}fps" else: fps_text = "N/A" - + fps_item = QTableWidgetItem(fps_text) - # Color code based on FPS value if fps_value and fps_value >= 60: - fps_item.setForeground(QColor("#00ff00")) # Green for 60+ fps + fps_item.setForeground(QColor("#00ff00")) elif fps_value and fps_value >= 30: - fps_item.setForeground(QColor("#ffaa00")) # Orange for 30+ fps + fps_item.setForeground(QColor("#ffaa00")) elif fps_value and fps_value >= 1: - fps_item.setForeground(QColor("#ff5555")) # Red for low fps + fps_item.setForeground(QColor("#ff5555")) else: - fps_item.setForeground(QColor("#888888")) # Gray for N/A + fps_item.setForeground(QColor("#888888")) self.format_table.setItem(row, 7, fps_item) - + # Column 8: HDR (Dynamic Range) if f.get("vcodec") == "none": - # Audio-only formats don't have HDR hdr_text = "N/A" hdr_item = QTableWidgetItem(hdr_text) - hdr_item.setForeground(QColor("#888888")) # Gray for N/A + hdr_item.setForeground(QColor("#888888")) else: hdr_value = f.get("dynamic_range") if hdr_value and hdr_value != "SDR": hdr_text = hdr_value hdr_item = QTableWidgetItem(hdr_text) - hdr_item.setForeground(QColor("#00ffff")) # Cyan for HDR + hdr_item.setForeground(QColor("#00ffff")) else: hdr_text = "SDR" hdr_item = QTableWidgetItem(hdr_text) - hdr_item.setForeground(QColor("#888888")) # Gray for SDR + hdr_item.setForeground(QColor("#888888")) self.format_table.setItem(row, 8, hdr_item) + def _update_format_table(self, formats) -> None: + """Signal handler that triggers a full table rebuild when formats change.""" + self = cast("YTSageApp", self) # for autocompletion and type inference. + + # Mark table as needing rebuild and trigger it + self._table_built = False + self._build_full_format_table() + def handle_checkbox_click(self, clicked_checkbox) -> None: self = cast("YTSageApp", self) # for autocompletion and type inference. @@ -446,6 +453,7 @@ class FormatTableMixin: self = cast("YTSageApp", self) # for autocompletion and type inference. self.all_formats = formats + self._table_built = False # Reset flag to trigger rebuild with new formats self.format_signals.format_update.emit(formats) def get_quality_label(self, format_info) -> str: