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
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: