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:
@@ -201,6 +201,10 @@ class DenoUpdateThread(QThread):
|
||||
class UpdaterTabWidget(QWidget):
|
||||
"""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:
|
||||
super().__init__(parent)
|
||||
self._parent: "CustomOptionsDialog" = cast("CustomOptionsDialog", self.parent())
|
||||
@@ -212,6 +216,7 @@ class UpdaterTabWidget(QWidget):
|
||||
|
||||
self._init_ui()
|
||||
self._load_auto_update_settings()
|
||||
self._channel_switch_finished.connect(self._on_channel_switch_finished)
|
||||
|
||||
def _init_ui(self) -> None:
|
||||
"""Initialize the UI components."""
|
||||
@@ -816,67 +821,53 @@ class UpdaterTabWidget(QWidget):
|
||||
ConfigManager.set("ytdlp_channel", new_channel)
|
||||
logger.info(f"Successfully switched to {new_channel} channel")
|
||||
|
||||
# Update UI
|
||||
# Make executable on Unix systems
|
||||
if OS_NAME != "Windows":
|
||||
import os
|
||||
os.chmod(yt_dlp_path, 0o755)
|
||||
|
||||
self._channel_switch_finished.emit(True, new_channel, current_channel, "")
|
||||
else:
|
||||
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}")
|
||||
self._channel_switch_finished.emit(False, new_channel, current_channel, error_msg)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("Channel switch timed out")
|
||||
self._channel_switch_finished.emit(False, new_channel, current_channel, "Timeout")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error switching channel: {e}")
|
||||
self._channel_switch_finished.emit(False, new_channel, current_channel, str(e))
|
||||
|
||||
# Start the thread
|
||||
thread = threading.Thread(target=switch_channel, daemon=True)
|
||||
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;"
|
||||
)
|
||||
|
||||
# Make executable on Unix systems
|
||||
if OS_NAME != "Windows":
|
||||
import os
|
||||
os.chmod(yt_dlp_path, 0o755)
|
||||
else:
|
||||
# Failed - revert radio button
|
||||
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}")
|
||||
|
||||
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:
|
||||
logger.error("Channel switch timed out")
|
||||
self.channel_status_label.setText(_("settings.ytdlp_channel_switch_failed", error="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:
|
||||
logger.exception(f"Error switching channel: {e}")
|
||||
self.channel_status_label.setText(_("settings.ytdlp_channel_switch_failed", error=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
|
||||
thread = threading.Thread(target=switch_channel, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def _update_channel_status(self, channel: str) -> None:
|
||||
"""Update the channel status label."""
|
||||
|
||||
Reference in New Issue
Block a user