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:
@@ -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(
|
||||
[
|
||||
# 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")
|
||||
|
||||
if is_playlist_mode:
|
||||
# Column 2 for playlist mode: 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 3: FPS (Frame Rate)
|
||||
fps_value = f.get("fps")
|
||||
if fps_value is not None:
|
||||
# Format FPS value appropriately
|
||||
if fps_value >= 1:
|
||||
if fps_value is not None and fps_value >= 1:
|
||||
fps_text = f"{fps_value:.0f}fps"
|
||||
else:
|
||||
fps_text = "N/A" # Very low fps like storyboards
|
||||
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:
|
||||
if fps_value is not None and fps_value >= 1:
|
||||
fps_text = f"{fps_value:.0f}fps"
|
||||
else:
|
||||
fps_text = "N/A" # Very low fps like storyboards
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user