From 5b8873465fc20a6e0d80350eb5c9e6049a4f9adc Mon Sep 17 00:00:00 2001 From: mertemr <48881002+mertemr@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:06:54 +0300 Subject: [PATCH 1/2] Periodically re-check local file size while playing The reported file size for the current user's local file was only ever read once, at the moment the player reported the file as loaded (SyncplayClient.updateFile()). If the file kept growing on disk after that point - e.g. a media player streaming from a still-in-progress sequential/progressive download - peers would keep seeing the stale size captured at load time for the rest of the session, even after the file finished downloading. Add a lightweight periodic re-check (every FILESIZE_RECHECK_DELAY seconds, mirroring the existing askPlayer LoopingCall pattern) that re-stats the current file and re-broadcasts it only when the size actually changed, respecting the existing filename/filesize privacy settings. --- syncplay/client.py | 32 +++++++++++++++++++++++++++++++- syncplay/constants.py | 1 + 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/syncplay/client.py b/syncplay/client.py index 894222598..e1916a90f 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -119,6 +119,7 @@ def __init__(self, playerClass, ui, config): self._running = False self._askPlayerTimer = None + self._fileSizeRecheckTimer = None self._lastPlayerUpdate = None self._playerPosition = 0.0 @@ -168,6 +169,7 @@ def initPlayer(self, player): if not self._player.alertOSDSupported: constants.OSD_WARNING_MESSAGE_DURATION = constants.NO_ALERT_OSD_WARNING_DURATION self.scheduleAskPlayer() + self.scheduleFileSizeRecheck() self.__playerReady.callback(player) def addPlayerReadyCallback(self, lambdaToCall): @@ -187,6 +189,30 @@ def askPlayer(self): self._player.askForStatus() self.checkIfConnected() + def scheduleFileSizeRecheck(self, when=constants.FILESIZE_RECHECK_DELAY): + self._fileSizeRecheckTimer = task.LoopingCall(self.recheckFileSize) + self._fileSizeRecheckTimer.start(when, now=False) + + def recheckFileSize(self): + if not self._running: + return + file_ = self.userlist.currentUser.file + if not file_ or not file_.get('path'): + return + path = file_['path'] + if utils.isURL(path): + return + if self._config['filesizePrivacyMode'] == PRIVACY_DONTSEND_MODE: + return + try: + size = os.path.getsize(path) + except OSError: + return + size = self.__executeFilesizePrivacySettings(size) + if size != file_['size']: + file_['size'] = size + self.sendFile() + def checkIfConnected(self): if self._lastGlobalUpdate and self._protocol and time.time() - self._lastGlobalUpdate > constants.PROTOCOL_TIMEOUT: protocol = self._protocol @@ -648,11 +674,15 @@ def __executePrivacySettings(self, filename, size): filename = utils.hashFilename(filename) elif self._config['filenamePrivacyMode'] == PRIVACY_DONTSEND_MODE: filename = PRIVACY_HIDDENFILENAME + size = self.__executeFilesizePrivacySettings(size) + return filename, size + + def __executeFilesizePrivacySettings(self, size): if self._config['filesizePrivacyMode'] == PRIVACY_SENDHASHED_MODE: size = utils.hashFilesize(size) elif self._config['filesizePrivacyMode'] == PRIVACY_DONTSEND_MODE: size = 0 - return filename, size + return size def setServerVersion(self, version, featureList): self.serverVersion = version diff --git a/syncplay/constants.py b/syncplay/constants.py index 7673e2a88..92d2f60e2 100755 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -214,6 +214,7 @@ def getValueForOS(constantDict): # Changing these is usually not something you're looking for PLAYER_ASK_DELAY = 0.1 +FILESIZE_RECHECK_DELAY = 5 # How often to re-check the size of the currently playing local file, in seconds (for files that grow while playing, e.g. an in-progress download) PING_MOVING_AVERAGE_WEIGHT = 0.85 MPC_OPEN_MAX_WAIT_TIME = 10 MPC_LOCK_WAIT_TIME = 0.2 From 20f7df28b303ed0b714efddf65d4b1f7713f9b60 Mon Sep 17 00:00:00 2001 From: mertemr <48881002+mertemr@users.noreply.github.com> Date: Tue, 18 Aug 2026 02:34:23 +0300 Subject: [PATCH 2/2] Fix: Only send the updated size on a peer match or once it's stable for 60s, and stat the file off the reactor thread. --- syncplay/client.py | 94 ++++++++++++++++++++++++++++++++++++++----- syncplay/constants.py | 3 +- 2 files changed, 86 insertions(+), 11 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index e1916a90f..0e5171cfd 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -119,7 +119,10 @@ def __init__(self, playerClass, ui, config): self._running = False self._askPlayerTimer = None - self._fileSizeRecheckTimer = None + self._filesizeRecheckTimer = None + self._filesizeRecheckPath = None + self._lastObservedFilesize = None + self._filesizeStableSince = None self._lastPlayerUpdate = None self._playerPosition = 0.0 @@ -169,7 +172,7 @@ def initPlayer(self, player): if not self._player.alertOSDSupported: constants.OSD_WARNING_MESSAGE_DURATION = constants.NO_ALERT_OSD_WARNING_DURATION self.scheduleAskPlayer() - self.scheduleFileSizeRecheck() + self.scheduleFilesizeRecheck() self.__playerReady.callback(player) def addPlayerReadyCallback(self, lambdaToCall): @@ -189,28 +192,74 @@ def askPlayer(self): self._player.askForStatus() self.checkIfConnected() - def scheduleFileSizeRecheck(self, when=constants.FILESIZE_RECHECK_DELAY): - self._fileSizeRecheckTimer = task.LoopingCall(self.recheckFileSize) - self._fileSizeRecheckTimer.start(when, now=False) + def scheduleFilesizeRecheck(self, when=constants.FILESIZE_RECHECK_DELAY): + self._filesizeRecheckTimer = task.LoopingCall(self.recheckFilesize) + self._filesizeRecheckTimer.start(when, now=False) - def recheckFileSize(self): + def _resetFilesizeRecheckState(self, path=None): + self._filesizeRecheckPath = path + self._lastObservedFilesize = None + self._filesizeStableSince = None + + def _getFilesize(self, path): + try: + return os.path.getsize(path) + except OSError: + return None + + def recheckFilesize(self): if not self._running: return file_ = self.userlist.currentUser.file if not file_ or not file_.get('path'): + self._resetFilesizeRecheckState() return path = file_['path'] if utils.isURL(path): + self._resetFilesizeRecheckState() return if self._config['filesizePrivacyMode'] == PRIVACY_DONTSEND_MODE: + self._resetFilesizeRecheckState() return - try: - size = os.path.getsize(path) - except OSError: + if path != self._filesizeRecheckPath: + self._resetFilesizeRecheckState(path) + return threads.deferToThread( + self._getFilesize, path + ).addCallback(self._processFilesizeRecheck, path) + + def _processFilesizeRecheck(self, size, path): + if not self._running: + return + file_ = self.userlist.currentUser.file + if not file_ or file_.get('path') != path: + self._resetFilesizeRecheckState() + return + if self._config['filesizePrivacyMode'] == PRIVACY_DONTSEND_MODE: + self._resetFilesizeRecheckState(path) + return + if size is None: + self._resetFilesizeRecheckState(path) return size = self.__executeFilesizePrivacySettings(size) - if size != file_['size']: + if size == file_['size']: + self._resetFilesizeRecheckState(path) + return + if self.userlist.currentFilesizeMatchesUserInRoom(size): file_['size'] = size + self._resetFilesizeRecheckState(path) + self.sendFile() + return + now = time.monotonic() + if size != self._lastObservedFilesize: + self._lastObservedFilesize = size + self._filesizeStableSince = now + return + if ( + self._filesizeStableSince is not None + and now - self._filesizeStableSince >= constants.FILESIZE_STABLE_THRESHOLD + ): + file_['size'] = size + self._resetFilesizeRecheckState(path) self.sendFile() def checkIfConnected(self): @@ -1424,6 +1473,31 @@ def isRoomSame(self, room): else: return False + def currentFilesizeMatchesUserInRoom(self, size): + file_ = self.currentUser.file + + if ( + not file_ + or size == 0 + or file_['name'] == PRIVACY_HIDDENFILENAME + ): + return False + + candidateFile = file_.copy() + candidateFile['size'] = size + + for otherUser in self._users.values(): + if ( + otherUser.room == self.currentUser.room + and otherUser.file + and otherUser.file['size'] != 0 + and otherUser.file['name'] != PRIVACY_HIDDENFILENAME + and otherUser.isFileSame(candidateFile) + ): + return True + + return False + def __showUserChangeMessage(self, username, room, file_, oldRoom=None): if room: if self.isRoomSame(room) or self.isRoomSame(oldRoom): diff --git a/syncplay/constants.py b/syncplay/constants.py index 92d2f60e2..97faa4d6a 100755 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -214,7 +214,8 @@ def getValueForOS(constantDict): # Changing these is usually not something you're looking for PLAYER_ASK_DELAY = 0.1 -FILESIZE_RECHECK_DELAY = 5 # How often to re-check the size of the currently playing local file, in seconds (for files that grow while playing, e.g. an in-progress download) +FILESIZE_RECHECK_DELAY = 10 +FILESIZE_STABLE_THRESHOLD = 60 PING_MOVING_AVERAGE_WEIGHT = 0.85 MPC_OPEN_MAX_WAIT_TIME = 10 MPC_LOCK_WAIT_TIME = 0.2