Marshal channel-switch UI updates back to the GUI thread

The yt-dlp stable/nightly channel switcher mutated QLabel/QRadioButton
state directly from a raw threading.Thread, which is undefined behavior
in Qt. The worker now only runs the subprocess and emits a signal; the
connected slot applies all widget updates on the GUI thread via Qt's
queued delivery.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-07-25 01:26:00 +02:00
parent dff14ec3e8
commit 5064b38315
@@ -200,18 +200,23 @@ class DenoUpdateThread(QThread):
class UpdaterTabWidget(QWidget): class UpdaterTabWidget(QWidget):
"""Widget for the Updater tab in Custom Options dialog.""" """Widget for the Updater tab in Custom Options dialog."""
# Emitted from the channel-switch worker thread; queued back to the GUI
# thread so widget updates never happen off-thread.
_channel_switch_finished = Signal(bool, str, str, str) # success, new_channel, current_channel, error
def __init__(self, parent=None) -> None: def __init__(self, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self._parent: "CustomOptionsDialog" = cast("CustomOptionsDialog", self.parent()) self._parent: "CustomOptionsDialog" = cast("CustomOptionsDialog", self.parent())
# State variables # State variables
self.current_version = "Unknown" self.current_version = "Unknown"
self.latest_version = "Unknown" self.latest_version = "Unknown"
self.update_available = False self.update_available = False
self._init_ui() self._init_ui()
self._load_auto_update_settings() self._load_auto_update_settings()
self._channel_switch_finished.connect(self._on_channel_switch_finished)
def _init_ui(self) -> None: def _init_ui(self) -> None:
"""Initialize the UI components.""" """Initialize the UI components."""
@@ -815,69 +820,55 @@ class UpdaterTabWidget(QWidget):
# Success - save the preference # Success - save the preference
ConfigManager.set("ytdlp_channel", new_channel) ConfigManager.set("ytdlp_channel", new_channel)
logger.info(f"Successfully switched to {new_channel} channel") logger.info(f"Successfully switched to {new_channel} channel")
# Update UI
self.channel_status_label.setText(_("settings.ytdlp_channel_switched", channel=new_channel))
self.channel_status_label.setStyleSheet(
"color: #00cc00; font-size: 11px; padding: 5px; "
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
)
# Make executable on Unix systems # Make executable on Unix systems
if OS_NAME != "Windows": if OS_NAME != "Windows":
import os import os
os.chmod(yt_dlp_path, 0o755) os.chmod(yt_dlp_path, 0o755)
self._channel_switch_finished.emit(True, new_channel, current_channel, "")
else: else:
# Failed - revert radio button
error_msg = result.stderr.strip() if result.stderr else result.stdout.strip() if result.stdout else "Unknown error" error_msg = result.stderr.strip() if result.stderr else result.stdout.strip() if result.stdout else "Unknown error"
logger.error(f"Failed to switch channel: {error_msg}") logger.error(f"Failed to switch channel: {error_msg}")
self._channel_switch_finished.emit(False, new_channel, current_channel, error_msg)
self.channel_status_label.setText(_("settings.ytdlp_channel_switch_failed", error=error_msg))
self.channel_status_label.setStyleSheet(
"color: #ff6666; font-size: 11px; padding: 5px; "
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
)
# Revert radio selection
if current_channel == "nightly":
self.channel_nightly_radio.setChecked(True)
else:
self.channel_stable_radio.setChecked(True)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
logger.error("Channel switch timed out") logger.error("Channel switch timed out")
self.channel_status_label.setText(_("settings.ytdlp_channel_switch_failed", error="Timeout")) self._channel_switch_finished.emit(False, new_channel, current_channel, "Timeout")
self.channel_status_label.setStyleSheet(
"color: #ff6666; font-size: 11px; padding: 5px; "
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
)
# Revert radio selection
if current_channel == "nightly":
self.channel_nightly_radio.setChecked(True)
else:
self.channel_stable_radio.setChecked(True)
except Exception as e: except Exception as e:
logger.exception(f"Error switching channel: {e}") logger.exception(f"Error switching channel: {e}")
self.channel_status_label.setText(_("settings.ytdlp_channel_switch_failed", error=str(e))) self._channel_switch_finished.emit(False, new_channel, current_channel, str(e))
self.channel_status_label.setStyleSheet(
"color: #ff6666; font-size: 11px; padding: 5px; "
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
)
# Revert radio selection
if current_channel == "nightly":
self.channel_nightly_radio.setChecked(True)
else:
self.channel_stable_radio.setChecked(True)
finally:
# Re-enable radio buttons
self.channel_stable_radio.setEnabled(True)
self.channel_nightly_radio.setEnabled(True)
# Start the thread # Start the thread
thread = threading.Thread(target=switch_channel, daemon=True) thread = threading.Thread(target=switch_channel, daemon=True)
thread.start() thread.start()
@Slot(bool, str, str, str)
def _on_channel_switch_finished(self, success: bool, new_channel: str, current_channel: str, error_msg: str) -> None:
"""Apply the channel-switch outcome to the UI (always on the GUI thread)."""
if success:
self.channel_status_label.setText(_("settings.ytdlp_channel_switched", channel=new_channel))
self.channel_status_label.setStyleSheet(
"color: #00cc00; font-size: 11px; padding: 5px; "
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
)
else:
self.channel_status_label.setText(_("settings.ytdlp_channel_switch_failed", error=error_msg))
self.channel_status_label.setStyleSheet(
"color: #ff6666; font-size: 11px; padding: 5px; "
"background-color: #2a2d36; border-radius: 4px; margin: 5px 0;"
)
# Revert radio selection
if current_channel == "nightly":
self.channel_nightly_radio.setChecked(True)
else:
self.channel_stable_radio.setChecked(True)
# Re-enable radio buttons
self.channel_stable_radio.setEnabled(True)
self.channel_nightly_radio.setEnabled(True)
def _update_channel_status(self, channel: str) -> None: def _update_channel_status(self, channel: str) -> None:
"""Update the channel status label.""" """Update the channel status label."""
self.channel_status_label.setText(_("settings.ytdlp_current_channel", channel=channel)) self.channel_status_label.setText(_("settings.ytdlp_current_channel", channel=channel))