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.
This commit is contained in:
oop7
2026-01-24 17:21:24 +02:00
parent 66870d062b
commit e43b1bbb2e
+106 -98
View File
@@ -158,6 +158,8 @@ class FormatTableMixin:
# Store format checkboxes and formats # Store format checkboxes and formats
self.format_checkboxes = [] self.format_checkboxes = []
self.all_formats = [] 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 # Set table size policies
self.format_table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.format_table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
@@ -173,29 +175,42 @@ class FormatTableMixin:
def filter_formats(self) -> None: def filter_formats(self) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference. 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 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 # Clear current table
self.format_table.setRowCount(0) self.format_table.setRowCount(0)
self.format_checkboxes.clear() self.format_checkboxes.clear()
self._row_format_type.clear()
# Determine which formats to show # Separate and filter formats
filtered_formats = [] video_formats = [f for f in self.all_formats if f.get("vcodec") != "none" and f.get("filesize") is not None]
audio_formats = [
if hasattr(self, "video_button") and self.video_button.isChecked(): # type: ignore[reportAttributeAccessIssue] f
filtered_formats.extend([f for f in self.all_formats if f.get("vcodec") != "none" and f.get("filesize") is not None]) for f in self.all_formats
if (f.get("vcodec") == "none" or "audio only" in f.get("format_note", "").lower())
if hasattr(self, "audio_button") and self.audio_button.isChecked(): # type: ignore[reportAttributeAccessIssue] and f.get("acodec") != "none"
filtered_formats.extend( and f.get("filesize") is not None
[ ]
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 # Sort formats by quality
def get_quality(f): def get_quality(f):
@@ -211,17 +226,23 @@ class FormatTableMixin:
else: else:
return f.get("abr", 0) 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 # Combine: video first, then audio (maintains logical grouping)
self.format_signals.format_update.emit(filtered_formats) 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 = 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] is_playlist_mode = hasattr(self, "is_playlist") and self.is_playlist # type: ignore[reportAttributeAccessIssue]
# Configure columns based on mode # Configure columns based on mode
@@ -252,119 +273,103 @@ class FormatTableMixin:
_("formats.hdr"), _("formats.hdr"),
] ]
self.format_table.setHorizontalHeaderLabels(header_labels) 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) self._apply_column_widths(header_labels, is_playlist_mode=False)
for f, format_type in formats_with_types:
for f in formats:
row = self.format_table.rowCount() row = self.format_table.rowCount()
self.format_table.insertRow(row) self.format_table.insertRow(row)
self._row_format_type.append(format_type)
# Column 0: Select Checkbox (Always shown) # Create checkbox widget
checkbox = QCheckBox() checkbox = QCheckBox()
checkbox.format_id = str(f.get("format_id", "")) # type: ignore[reportAttributeAccessIssue] checkbox.setStyleSheet("QCheckBox { margin-left: 8px; }")
checkbox.is_audio_only = bool((f.get("vcodec") or "none").lower() == "none") # type: ignore[attr-defined] checkbox.format_id = f["format_id"]
checkbox.has_audio = bool(f.get("acodec") and f.get("acodec") != "none") # type: ignore[attr-defined] 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)) checkbox.clicked.connect(lambda checked, cb=checkbox: self.handle_checkbox_click(cb))
self.format_checkboxes.append(checkbox) self.format_checkboxes.append(checkbox)
checkbox_widget = QWidget()
checkbox_widget.setStyleSheet("background-color: transparent;") # Create a container widget for the checkbox
checkbox_layout = QHBoxLayout(checkbox_widget) checkbox_container = QWidget()
checkbox_layout = QHBoxLayout(checkbox_container)
checkbox_layout.addWidget(checkbox) checkbox_layout.addWidget(checkbox)
checkbox_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) checkbox_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
checkbox_layout.setContentsMargins(0, 0, 0, 0) checkbox_layout.setContentsMargins(0, 0, 0, 0)
checkbox_layout.setSpacing(0) self.format_table.setCellWidget(row, 0, checkbox_container)
self.format_table.setCellWidget(row, 0, checkbox_widget)
# Column 1: Quality (Always shown) # Quality label with color coding
quality_text = self.get_quality_label(f) quality_label = self.get_quality_label(f)
quality_item = QTableWidgetItem(quality_text) quality_item = QTableWidgetItem(quality_label)
# Set color based on quality (check English, Spanish, Portuguese, Russian, Chinese, German, French, Hindi, Indonesian, Turkish, Polish, Italian, Arabic, and Japanese terms) # Set color based on quality (check multiple language terms)
quality_lower = quality_text.lower() # Make comparison case-insensitive 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", "الأفضل", "أفضل", "最高"]): 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 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 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 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à", "منخفضة", "منخفض", "صوت منخفض", "جودة منخفضة", "", "低音質", "低品質"]): 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 quality_item.setForeground(QColor("#ff5555")) # Red for low quality
self.format_table.setItem(row, 1, quality_item) self.format_table.setItem(row, 1, quality_item)
# --- Populate columns common to both modes (Moved outside the 'if not is_playlist_mode' block) --- # Resolution
# Column 2: Resolution (Always shown)
resolution = f.get("resolution", "N/A") 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: 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") fps_value = f.get("fps")
if fps_value is not None: if fps_value is not None and fps_value >= 1:
# Format FPS value appropriately fps_text = f"{fps_value:.0f}fps"
if fps_value >= 1:
fps_text = f"{fps_value:.0f}fps"
else:
fps_text = "N/A" # Very low fps like storyboards
else: else:
fps_text = "N/A" fps_text = "N/A"
fps_item = QTableWidgetItem(fps_text) fps_item = QTableWidgetItem(fps_text)
# Color code based on FPS value
if fps_value and fps_value >= 60: 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: 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: elif fps_value and fps_value >= 1:
fps_item.setForeground(QColor("#ff5555")) # Red for low fps fps_item.setForeground(QColor("#ff5555"))
else: else:
fps_item.setForeground(QColor("#888888")) # Gray for N/A fps_item.setForeground(QColor("#888888"))
self.format_table.setItem(row, 3, fps_item) self.format_table.setItem(row, 3, fps_item)
# Column 4: HDR for playlist mode # Column 4: HDR
if f.get("vcodec") == "none": if f.get("vcodec") == "none":
# Audio-only formats don't have HDR
hdr_text = "N/A" hdr_text = "N/A"
hdr_item = QTableWidgetItem(hdr_text) hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#888888")) # Gray for N/A hdr_item.setForeground(QColor("#888888"))
else: else:
hdr_value = f.get("dynamic_range") hdr_value = f.get("dynamic_range")
if hdr_value and hdr_value != "SDR": if hdr_value and hdr_value != "SDR":
hdr_text = hdr_value hdr_text = hdr_value
hdr_item = QTableWidgetItem(hdr_text) hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#00ffff")) # Cyan for HDR hdr_item.setForeground(QColor("#00ffff"))
else: else:
hdr_text = "SDR" hdr_text = "SDR"
hdr_item = QTableWidgetItem(hdr_text) 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) self.format_table.setItem(row, 4, hdr_item)
else: else:
# Extension for normal mode (column 2) # Extension for normal mode (column 2)
self.format_table.setItem(row, 2, QTableWidgetItem(f.get("ext", "").upper())) self.format_table.setItem(row, 2, QTableWidgetItem(f.get("ext", "").upper()))
# Column 4 in playlist mode, Column 6 in normal mode: Audio Status # Audio Status column
needs_audio = f.get("acodec") == "none" and f.get("vcodec") != "none" # Only mark video-only as needing merge 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_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) audio_item = QTableWidgetItem(audio_status)
if needs_audio: if needs_audio:
audio_item.setForeground(QColor("#ffa500")) audio_item.setForeground(QColor("#ffa500"))
elif audio_status == _("formats.audio_only"): elif audio_status == _("formats.audio_only"):
audio_item.setForeground(QColor("#cccccc")) # Neutral color for audio only audio_item.setForeground(QColor("#cccccc"))
else: # Has Audio (Video+Audio) else:
audio_item.setForeground(QColor("#00cc00")) # Green for included audio audio_item.setForeground(QColor("#00cc00"))
# Set item for correct column based on mode
audio_column_index = 5 if is_playlist_mode else 6 audio_column_index = 5 if is_playlist_mode else 6
self.format_table.setItem(row, audio_column_index, audio_item) 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: if not is_playlist_mode:
# Column 3: Resolution # Column 3: Resolution
self.format_table.setItem(row, 3, QTableWidgetItem(resolution)) self.format_table.setItem(row, 3, QTableWidgetItem(resolution))
@@ -384,45 +389,47 @@ class FormatTableMixin:
# Column 7: FPS (Frame Rate) # Column 7: FPS (Frame Rate)
fps_value = f.get("fps") fps_value = f.get("fps")
if fps_value is not None: if fps_value is not None and fps_value >= 1:
# Format FPS value appropriately fps_text = f"{fps_value:.0f}fps"
if fps_value >= 1:
fps_text = f"{fps_value:.0f}fps"
else:
fps_text = "N/A" # Very low fps like storyboards
else: else:
fps_text = "N/A" fps_text = "N/A"
fps_item = QTableWidgetItem(fps_text) fps_item = QTableWidgetItem(fps_text)
# Color code based on FPS value
if fps_value and fps_value >= 60: 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: 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: elif fps_value and fps_value >= 1:
fps_item.setForeground(QColor("#ff5555")) # Red for low fps fps_item.setForeground(QColor("#ff5555"))
else: else:
fps_item.setForeground(QColor("#888888")) # Gray for N/A fps_item.setForeground(QColor("#888888"))
self.format_table.setItem(row, 7, fps_item) self.format_table.setItem(row, 7, fps_item)
# Column 8: HDR (Dynamic Range) # Column 8: HDR (Dynamic Range)
if f.get("vcodec") == "none": if f.get("vcodec") == "none":
# Audio-only formats don't have HDR
hdr_text = "N/A" hdr_text = "N/A"
hdr_item = QTableWidgetItem(hdr_text) hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#888888")) # Gray for N/A hdr_item.setForeground(QColor("#888888"))
else: else:
hdr_value = f.get("dynamic_range") hdr_value = f.get("dynamic_range")
if hdr_value and hdr_value != "SDR": if hdr_value and hdr_value != "SDR":
hdr_text = hdr_value hdr_text = hdr_value
hdr_item = QTableWidgetItem(hdr_text) hdr_item = QTableWidgetItem(hdr_text)
hdr_item.setForeground(QColor("#00ffff")) # Cyan for HDR hdr_item.setForeground(QColor("#00ffff"))
else: else:
hdr_text = "SDR" hdr_text = "SDR"
hdr_item = QTableWidgetItem(hdr_text) 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) 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: def handle_checkbox_click(self, clicked_checkbox) -> None:
self = cast("YTSageApp", self) # for autocompletion and type inference. 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 = cast("YTSageApp", self) # for autocompletion and type inference.
self.all_formats = formats self.all_formats = formats
self._table_built = False # Reset flag to trigger rebuild with new formats
self.format_signals.format_update.emit(formats) self.format_signals.format_update.emit(formats)
def get_quality_label(self, format_info) -> str: def get_quality_label(self, format_info) -> str: