From 3986ca7886e3c138e1d21108b2a3ceb4e7fd26b6 Mon Sep 17 00:00:00 2001 From: Jake Weinstein Date: Sun, 14 Sep 2025 12:07:12 -0500 Subject: [PATCH] Add manual reconnect button to File menu Resolves issues where users had to quit and restart Syncplay after connection times out. --- syncplay/client.py | 43 ++++++++++++++++++++++++++++++++++++----- syncplay/messages_en.py | 5 +++++ syncplay/ui/gui.py | 16 +++++++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index 54f12950..92ae5508 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -871,9 +871,8 @@ def start(self, host, port): self._clientSupportsTLS = False def retry(retries): - self._lastGlobalUpdate = None - self.ui.setSSLMode(False) - self.playlistMayNeedRestoring = True + # Use shared state reset method + self._performRetryStateReset() if retries == 0: self.onDisconnect() if retries > constants.RECONNECT_RETRIES: @@ -882,8 +881,6 @@ def retry(retries): reactor.callLater(0.1, self.stop, True) return None - self.ui.showMessage(getMessage("reconnection-attempt-notification")) - self.reconnecting = True return(0.1 * (2 ** min(retries, 5))) self._reconnectingService = ClientService(self._endpoint, self.protocolFactory, retryPolicy=retry) @@ -920,6 +917,42 @@ def stop(self, promptForAction=False): if promptForAction: self.ui.promptFor(getMessage("enter-to-exit-prompt")) + def _performRetryStateReset(self): + """ + Shared method to reset connection state for both automatic and manual retries. + This contains the common logic from the original retry function. + """ + self._lastGlobalUpdate = None + self.ui.setSSLMode(False) + self.playlistMayNeedRestoring = True + self.ui.showMessage(getMessage("reconnection-attempt-notification")) + self.reconnecting = True + + def manualReconnect(self): + """ + Trigger a manual reconnection by forcing the retry mechanism. + This performs the same steps as the automatic retry function. + """ + if not self._running or not hasattr(self, '_reconnectingService'): + self.ui.showErrorMessage(getMessage("connection-failed-notification")) + return + + from twisted.internet import reactor + + def performReconnect(): + # Apply the shared state reset logic + self._performRetryStateReset() + + # Stop current service and restart it to trigger reconnection + if self._reconnectingService and self._reconnectingService.running: + self._reconnectingService.stopService() + + # Restart the service to trigger a reconnection attempt + self._reconnectingService.startService() + + # Use callLater for threading purposes as suggested + reactor.callLater(0.1, performReconnect) + def requireServerFeature(featureRequired): def requireServerFeatureDecorator(f): @wraps(f) diff --git a/syncplay/messages_en.py b/syncplay/messages_en.py index f568cbf9..263c879b 100644 --- a/syncplay/messages_en.py +++ b/syncplay/messages_en.py @@ -25,6 +25,10 @@ "connection-attempt-notification": "Attempting to connect to {}:{}", # Port, IP "reconnection-attempt-notification": "Connection with server lost, attempting to reconnect", + "reconnect-menu-triggered-notification": "Manual reconnect initiated - will attempt fresh connection to {}:{} in 2 seconds...", + "reconnect-failed-no-host-error": "Cannot reconnect: no server information available", + "reconnect-failed-no-port-error": "Cannot reconnect: invalid server configuration", + "reconnect-failed-error": "Reconnection failed: {}", "disconnection-notification": "Disconnected from server", "connection-failed-notification": "Connection with server failed", "connected-successful-notification": "Successfully connected to server", @@ -334,6 +338,7 @@ "setmediadirectories-menu-label": "Set media &directories", "loadplaylistfromfile-menu-label": "&Load playlist from file", "saveplaylisttofile-menu-label": "&Save playlist to file", + "reconnect-menu-label": "&Reconnect to server", "exit-menu-label": "E&xit", "advanced-menu-label": "&Advanced", "window-menu-label": "&Window", diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index a647feba..a676c177 100755 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -1007,6 +1007,19 @@ def pause(self): self._syncplayClient.setPaused(True) @needsClient + def reconnectToServer(self): + """ + Trigger a manual reconnection using the client's built-in retry mechanism. + This is simpler and more reliable than doing a complete restart. + """ + try: + if self._syncplayClient: + self._syncplayClient.manualReconnect() + else: + self.showErrorMessage(getMessage("connection-failed-notification")) + except Exception as e: + self.showErrorMessage(getMessage("reconnect-failed-error").format(str(e))) + def exitSyncplay(self): self._syncplayClient.stop() @@ -1715,6 +1728,9 @@ def populateMenubar(self, window): getMessage("setmediadirectories-menu-label")) window.openAction.triggered.connect(self.openSetMediaDirectoriesDialog) + window.reconnectAction = window.fileMenu.addAction(getMessage("reconnect-menu-label")) + window.reconnectAction.triggered.connect(self.reconnectToServer) + window.exitAction = window.fileMenu.addAction(getMessage("exit-menu-label")) if isMacOS(): window.exitAction.setMenuRole(QtWidgets.QAction.QuitRole)