diff --git a/syncplay/client.py b/syncplay/client.py index 92ae5508..00c98f9d 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -1,2378 +1 @@ - -import ast -import collections -import hashlib -import os -import os.path -import random -import re -import sys -import threading -import time -from copy import deepcopy -from functools import wraps -from urllib.parse import urlparse - -from twisted.application.internet import ClientService -from twisted.internet.endpoints import HostnameEndpoint -from twisted.internet.protocol import ClientFactory -from twisted.internet import reactor, task, defer, threads - -try: - SSL_CERT_FILE = None - import certifi - import pem - from twisted.internet.ssl import Certificate, optionsForClientTLS, trustRootFromCertificates - certPath = certifi.where() - if os.path.exists(certPath): - SSL_CERT_FILE = certPath - elif 'zip' in certPath: - import tempfile - import zipfile - zipPath, memberPath = certPath.split('.zip/') - zipPath += '.zip' - archive = zipfile.ZipFile(zipPath, 'r') - tmpDir = tempfile.gettempdir() - extractedPath = archive.extract(memberPath, tmpDir) - SSL_CERT_FILE = extractedPath -except: - pass - -from syncplay import utils, constants, version -from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \ - PRIVACY_HIDDENFILENAME -from syncplay.messages import getMissingStrings, getMessage, isNoOSDMessage -from syncplay.protocols import SyncClientProtocol -from syncplay.utils import isMacOS -class SyncClientFactory(ClientFactory): - def __init__(self, client, retry=constants.RECONNECT_RETRIES): - self._client = client - self.retry = retry - self._timesTried = 0 - - def buildProtocol(self, addr): - self._timesTried = 0 - return SyncClientProtocol(self._client) - - def stopRetrying(self): - self._client._reconnectingService.stopService() - self._client.ui.showErrorMessage(getMessage("disconnection-notification")) - - -class SyncplayClient(object): - def __init__(self, playerClass, ui, config): - self.delayedLoadPath = None - constants.SHOW_OSD = config['showOSD'] - constants.SHOW_OSD_WARNINGS = config['showOSDWarnings'] - constants.SHOW_SLOWDOWN_OSD = config['showSlowdownOSD'] - constants.SHOW_DIFFERENT_ROOM_OSD = config['showDifferentRoomOSD'] - constants.SHOW_SAME_ROOM_OSD = config['showSameRoomOSD'] - constants.SHOW_DURATION_NOTIFICATION = config['showDurationNotification'] - constants.DEBUG_MODE = config['debug'] - constants.FOLDER_SEARCH_FIRST_FILE_TIMEOUT = config['folderSearchFirstFileTimeout'] - constants.FOLDER_SEARCH_TIMEOUT = config['folderSearchTimeout'] - constants.FOLDER_SEARCH_DOUBLE_CHECK_INTERVAL = config['folderSearchDoubleCheckInterval'] - constants.FOLDER_SEARCH_WARNING_THRESHOLD = config['folderSearchWarningThreshold'] - - self.controlpasswords = {} - self.lastControlPasswordAttempt = None - self.serverVersion = "0.0.0" - - self.serverFeatures = {} - - self.lastRewindTime = None - self.lastUpdatedFileTime = None - self.lastAdvanceTime = None - self.fileOpenBeforeChangingPlaylistIndex = None - self.waitingToLoadNewfile = False - self.waitingToLoadNewfileSince = None - self.lastConnectTime = None - self.lastSetRoomTime = None - self.hadFirstPlaylistIndex = False - self.hadFirstStateUpdate = False - self.lastLeftTime = 0 - self.lastPausedOnLeaveTime = None - self.lastLeftUser = "" - self.protocolFactory = SyncClientFactory(self) - self.ui = UiManager(self, ui) - self.userlist = SyncplayUserlist(self.ui, self) - self._protocol = None - """:type : SyncClientProtocol|None""" - self._player = None - if config['room'] is None or config['room'] == '': - config['room'] = config['name'] # ticket #58 - self.defaultRoom = config['room'] - self.playerPositionBeforeLastSeek = 0.0 - self.setUsername(config['name']) - self.setRoom(config['room']) - if config['password']: - config['password'] = hashlib.md5(config['password'].encode('utf-8')).hexdigest() - self._serverPassword = config['password'] - self._host = "{}:{}".format(config['host'], config['port']) - self._publicServers = config["publicServers"] - if not config['file']: - self.__getUserlistOnLogon = True - else: - self.__getUserlistOnLogon = False - self._playerClass = playerClass - self._config = config - - self._running = False - self._askPlayerTimer = None - - self._lastPlayerUpdate = None - self._playerPosition = 0.0 - self._playerPaused = True - - self._lastGlobalUpdate = None - self._globalPosition = 0.0 - self._globalPaused = 0.0 - self._userOffset = 0.0 - self._speedChanged = False - self.behindFirstDetected = None - self.autoPlay = False - self.autoPlayThreshold = None - - self.autoplayTimer = task.LoopingCall(self.autoplayCountdown) - self.autoplayTimeLeft = constants.AUTOPLAY_DELAY - - self.__playerReady = defer.Deferred() - - self._warnings = self._WarningManager(self._player, self.userlist, self.ui, self) - self.fileSwitch = FileSwitchManager(self) - self.playlist = SyncplayPlaylist(self) - self.playlistMayNeedRestoring = False - - self._serverSupportsTLS = True - - if constants.LIST_RELATIVE_CONFIGS and 'loadedRelativePaths' in self._config and self._config['loadedRelativePaths']: - paths = "; ".join(self._config['loadedRelativePaths']) - self.ui.showMessage(getMessage("relative-config-notification").format(paths), noPlayer=True, noTimestamp=True) - - if constants.DEBUG_MODE and constants.WARN_ABOUT_MISSING_STRINGS: - missingStrings = getMissingStrings() - if missingStrings is not None and missingStrings != "": - self.ui.showDebugMessage("MISSING/UNUSED STRINGS DETECTED:\n{}".format(missingStrings)) - - def initProtocol(self, protocol): - self._protocol = protocol - - def destroyProtocol(self): - if self._protocol: - self._protocol.drop() - self._protocol = None - - def initPlayer(self, player): - self._player = player - if not self._player.alertOSDSupported: - constants.OSD_WARNING_MESSAGE_DURATION = constants.NO_ALERT_OSD_WARNING_DURATION - self.scheduleAskPlayer() - self.__playerReady.callback(player) - - def addPlayerReadyCallback(self, lambdaToCall): - self.__playerReady.addCallback(lambdaToCall) - - def playerIsNotReady(self): - return self._player is None - - def scheduleAskPlayer(self, when=constants.PLAYER_ASK_DELAY): - self._askPlayerTimer = task.LoopingCall(self.askPlayer) - self._askPlayerTimer.start(when) - - def askPlayer(self): - if not self._running: - return - if self._player: - self._player.askForStatus() - self.checkIfConnected() - - def checkIfConnected(self): - if self._lastGlobalUpdate and self._protocol and time.time() - self._lastGlobalUpdate > constants.PROTOCOL_TIMEOUT: - self._lastGlobalUpdate = None - self.ui.showErrorMessage(getMessage("server-timeout-error")) - self._protocol.drop() - return False - return True - - def _determinePlayerStateChange(self, paused, position): - pauseChange = self.getPlayerPaused() != paused and self.getGlobalPaused() != paused - _playerDiff = abs(self.getPlayerPosition() - position) - _globalDiff = abs(self.getGlobalPosition() - position) - seeked = _playerDiff > constants.SEEK_THRESHOLD and _globalDiff > constants.SEEK_THRESHOLD - return pauseChange, seeked - - def rewindFile(self): - self.setPosition(0) - self.establishRewindDoubleCheck() - - def establishRewindDoubleCheck(self): - if constants.DOUBLE_CHECK_REWIND: - reactor.callLater(0.5, self.doubleCheckRewindFile,) - reactor.callLater(1, self.doubleCheckRewindFile,) - reactor.callLater(1.5, self.doubleCheckRewindFile,) - return - - def doubleCheckRewindFile(self): - if self.getStoredPlayerPosition() > 5: - self.setPosition(0) - self.ui.showDebugMessage("Rewinded after double-check") - - def isPlayingMusic(self): - if self.userlist.currentUser.file: - for musicFormat in constants.MUSIC_FORMATS: - if self.userlist.currentUser.file['name'].lower().endswith(musicFormat): - return True - - def seamlessMusicOveride(self): - return self.isPlayingMusic() and self._recentlyAdvanced() - - def updatePlayerStatus(self, paused, position): - position -= self.getUserOffset() - pauseChange, seeked = self._determinePlayerStateChange(paused, position) - positionBeforeSeek = self._playerPosition - self._playerPosition = position - self._playerPaused = paused - currentLength = self.userlist.currentUser.file["duration"] if self.userlist.currentUser.file else 0 - if ( - pauseChange and paused and currentLength > constants.PLAYLIST_LOAD_NEXT_FILE_MINIMUM_LENGTH - and abs(position - currentLength) < constants.PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD - ): - self.playlist.advancePlaylistCheck() - elif pauseChange and "readiness" in self.serverFeatures and self.serverFeatures["readiness"]: - if ( - currentLength == 0 or currentLength == -1 or - not ( - not self.playlist.notJustChangedPlaylist() and - abs(position - currentLength) < constants.PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD - ) - ): - pauseChange = self._toggleReady(pauseChange, paused) - - if self._lastGlobalUpdate: - self._lastPlayerUpdate = time.time() - if (pauseChange or seeked) and self._protocol: - if self.recentlyRewound() or self._recentlyAdvanced(): - self._protocol.sendState(self._globalPosition, self.getPlayerPaused(), False, None, True) - return - if seeked: - self.playerPositionBeforeLastSeek = self.getGlobalPosition() - self._protocol.sendState(self.getPlayerPosition(), self.getPlayerPaused(), seeked, None, True) - - def prepareToChangeToNewPlaylistItemAndRewind(self): - self.ui.showDebugMessage("Preparing to change to new playlist index and rewind...") - self.fileOpenBeforeChangingPlaylistIndex = self.userlist.currentUser.file["path"] if self.userlist.currentUser.file else None - self.waitingToLoadNewfile = True - self.waitingToLoadNewfileSince = time.time() - - def prepareToAdvancePlaylist(self): - if self.playlist.canSwitchToNextPlaylistIndex(): - self.ui.showDebugMessage("Preparing to advance playlist...") - self.lastAdvanceTime = time.time() - else: - self.ui.showDebugMessage("Not preparing to advance playlist because the next file cannot be switched to") - - def _recentlyAdvanced(self): - lastAdvandedDiff = time.time() - self.lastAdvanceTime if self.lastAdvanceTime else None - if lastAdvandedDiff is not None and lastAdvandedDiff < constants.AUTOPLAY_DELAY + 5: - return True - - def recentlyConnected(self): - connectDiff = time.time() - self.lastConnectTime if self.lastConnectTime else None - if connectDiff is None or connectDiff < constants.LAST_PAUSED_DIFF_THRESHOLD: - return True - - def recentlyRewound(self, recentRewindThreshold = 5.0): - lastRewindTime = self.lastRewindTime - if lastRewindTime and self.lastUpdatedFileTime and self.lastUpdatedFileTime > lastRewindTime: - lastRewindTime = self.lastRewindTime - 4.5 - return lastRewindTime is not None and abs(time.time() - lastRewindTime) < recentRewindThreshold - - def _toggleReady(self, pauseChange, paused): - if not self.userlist.currentUser.canControl(): - self._player.setPaused(self._globalPaused) - if not self.recentlyRewound() and not ((self._globalPaused == True) and not self._recentlyAdvanced()): - self.toggleReady(manuallyInitiated=True) - self._playerPaused = self._globalPaused - pauseChange = False - if self.userlist.currentUser.isReady(): - self.ui.showMessage(getMessage("set-as-not-ready-notification")) - else: - self.ui.showMessage(getMessage("set-as-ready-notification")) - elif self.seamlessMusicOveride(): - self.ui.showDebugMessage("Readiness toggle ignored due to seamless music override") - self._player.setPaused(paused) - self._playerPaused = paused - elif (self.recentlyRewound() and (self._globalPaused == True) and not self._recentlyAdvanced()): - self._player.setPaused(self._globalPaused) - self._playerPaused = self._globalPaused - pauseChange = False - elif not paused and not self.instaplayConditionsMet(): - paused = True - self._player.setPaused(paused) - self._playerPaused = paused - self.changeReadyState(True, manuallyInitiated=True) - pauseChange = False - self.ui.showMessage(getMessage("ready-to-unpause-notification")) - else: - lastPausedDiff = time.time() - self.lastPausedOnLeaveTime if self.lastPausedOnLeaveTime else None - if lastPausedDiff is not None and lastPausedDiff < constants.LAST_PAUSED_DIFF_THRESHOLD: - self.lastPausedOnLeaveTime = None - else: - self.changeReadyState(not self.getPlayerPaused(), manuallyInitiated=False) - return pauseChange - - def getLocalState(self): - paused = self.getPlayerPaused() - if self._config['dontSlowDownWithMe']: - position = self.getGlobalPosition() - else: - position = self.getPlayerPosition() - pauseChange, _ = self._determinePlayerStateChange(paused, position) - if self._lastGlobalUpdate: - return position, paused, _, pauseChange - else: - return None, None, None, None - - def _initPlayerState(self, position, paused): - if self.userlist.currentUser.file: - self.setPosition(position) - self._player.setPaused(paused) - madeChangeOnPlayer = True - return madeChangeOnPlayer - - def _rewindPlayerDueToTimeDifference(self, position, setBy): - madeChangeOnPlayer = False - if self.getUsername() == setBy: - self.ui.showDebugMessage("Caught attempt to rewind due to time difference with self") - else: - hideFromOSD = not constants.SHOW_SAME_ROOM_OSD - self.setPosition(position) - self.ui.showMessage(getMessage("rewind-notification").format(setBy), hideFromOSD) - madeChangeOnPlayer = True - return madeChangeOnPlayer - - def _fastforwardPlayerDueToTimeDifference(self, position, setBy): - madeChangeOnPlayer = False - if self.getUsername() == setBy: - self.ui.showDebugMessage("Caught attempt to fastforward due to time difference with self") - else: - hideFromOSD = not constants.SHOW_SAME_ROOM_OSD - self.setPosition(position + constants.FASTFORWARD_EXTRA_TIME) - self.ui.showMessage(getMessage("fastforward-notification").format(setBy), hideFromOSD) - madeChangeOnPlayer = True - return madeChangeOnPlayer - - def _serverUnpaused(self, setBy): - hideFromOSD = not constants.SHOW_SAME_ROOM_OSD - self._player.setPaused(False) - madeChangeOnPlayer = True - self.ui.showMessage(getMessage("unpause-notification").format(setBy), hideFromOSD) - return madeChangeOnPlayer - - def _serverPaused(self, setBy): - hideFromOSD = not constants.SHOW_SAME_ROOM_OSD - if constants.SYNC_ON_PAUSE and self.getUsername() != setBy: - self.setPosition(self.getGlobalPosition()) - self._player.setPaused(True) - madeChangeOnPlayer = True - if (self.lastLeftTime < time.time() - constants.OSD_DURATION) or hideFromOSD == True: - self.ui.showMessage(getMessage("pause-notification").format(setBy, utils.formatTime(self.getGlobalPosition())), hideFromOSD) - else: - self.ui.showMessage(getMessage("left-paused-notification").format(self.lastLeftUser, setBy), hideFromOSD) - return madeChangeOnPlayer - - def _serverSeeked(self, position, setBy): - hideFromOSD = not constants.SHOW_SAME_ROOM_OSD - if self.getUsername() != setBy: - self.playerPositionBeforeLastSeek = self.getPlayerPosition() - self.setPosition(position) - madeChangeOnPlayer = True - else: - madeChangeOnPlayer = False - message = getMessage("seek-notification").format(setBy, utils.formatTime(self.playerPositionBeforeLastSeek), utils.formatTime(position)) - self.ui.showMessage(message, hideFromOSD) - return madeChangeOnPlayer - - def _slowDownToCoverTimeDifference(self, diff, setBy): - hideFromOSD = not constants.SHOW_SLOWDOWN_OSD - madeChangeOnPlayer = False - if self._config['slowdownThreshold'] < diff and not self._speedChanged: - if self.getUsername() == setBy: - self.ui.showDebugMessage("Caught attempt to slow down due to time difference with self") - else: - self._player.setSpeed(constants.SLOWDOWN_RATE) - self._speedChanged = True - self.ui.showMessage(getMessage("slowdown-notification").format(setBy), hideFromOSD) - madeChangeOnPlayer = True - elif self._speedChanged and diff < constants.SLOWDOWN_RESET_THRESHOLD: - self._player.setSpeed(1.00) - self._speedChanged = False - self.ui.showMessage(getMessage("revert-notification"), hideFromOSD) - madeChangeOnPlayer = True - return madeChangeOnPlayer - - def _changePlayerStateAccordingToGlobalState(self, position, paused, doSeek, setBy): - madeChangeOnPlayer = False - pauseChanged = paused != self.getGlobalPaused() or paused != self.getPlayerPaused() - diff = self.getPlayerPosition() - position - if self._lastGlobalUpdate is None: - madeChangeOnPlayer = self._initPlayerState(position, paused) - self._globalPaused = paused - self._globalPosition = position - self._lastGlobalUpdate = time.time() - if doSeek: - madeChangeOnPlayer = self._serverSeeked(position, setBy) - if diff > self._config['rewindThreshold'] and not doSeek and not self._config['rewindOnDesync'] == False: - madeChangeOnPlayer = self._rewindPlayerDueToTimeDifference(position, setBy) - if self._config['fastforwardOnDesync'] and (self.userlist.currentUser.canControl() == False or self._config['dontSlowDownWithMe'] == True): - if diff < (constants.FASTFORWARD_BEHIND_THRESHOLD * -1) and not doSeek: - if self.behindFirstDetected is None: - self.behindFirstDetected = time.time() - else: - durationBehind = time.time() - self.behindFirstDetected - if (durationBehind > (self._config['fastforwardThreshold']-constants.FASTFORWARD_BEHIND_THRESHOLD))\ - and (diff < (self._config['fastforwardThreshold'] * -1)): - madeChangeOnPlayer = self._fastforwardPlayerDueToTimeDifference(position, setBy) - self.behindFirstDetected = time.time() + constants.FASTFORWARD_RESET_THRESHOLD - else: - self.behindFirstDetected = None - if self._player.speedSupported and not doSeek and not paused and not self._config['slowOnDesync'] == False: - madeChangeOnPlayer = self._slowDownToCoverTimeDifference(diff, setBy) - if paused == False and pauseChanged: - madeChangeOnPlayer = self._serverUnpaused(setBy) - elif paused == True and pauseChanged: - madeChangeOnPlayer = self._serverPaused(setBy) - return madeChangeOnPlayer - - def _executePlaystateHooks(self, position, paused, doSeek, setBy, messageAge): - if self.userlist.hasRoomStateChanged() and not paused: - self._warnings.checkWarnings() - self.userlist.roomStateConfirmed() - - def updateGlobalState(self, position, paused, doSeek, setBy, messageAge): - if self.__getUserlistOnLogon: - self.__getUserlistOnLogon = False - self.getUserList() - madeChangeOnPlayer = False - if not paused: - position += messageAge - if self._player: - madeChangeOnPlayer = self._changePlayerStateAccordingToGlobalState(position, paused, doSeek, setBy) - if madeChangeOnPlayer: - self.askPlayer() - self._executePlaystateHooks(position, paused, doSeek, setBy, messageAge) - - def getUserOffset(self): - return self._userOffset - - def setUserOffset(self, time): - self._userOffset = time - self.setPosition(self.getGlobalPosition()) - self.ui.showMessage(getMessage("current-offset-notification").format(self._userOffset)) - - def onDisconnect(self): - if self._config['pauseOnLeave']: - self.setPaused(True) - self.lastPausedOnLeaveTime = time.time() - - def removeUser(self, username): - if self.userlist.isUserInYourRoom(username): - self.onDisconnect() - self.userlist.removeUser(username) - - def getPlayerPosition(self): - if not self._lastPlayerUpdate: - if self._lastGlobalUpdate: - return self.getGlobalPosition() - else: - return 0.0 - position = self._playerPosition - if not self._playerPaused: - diff = time.time() - self._lastPlayerUpdate - position += diff - return position - - def getStoredPlayerPosition(self): - return self._playerPosition if self._playerPosition is not None else None - - def getPlayerPaused(self): - if not self._lastPlayerUpdate: - if self._lastGlobalUpdate: - return self.getGlobalPaused() - else: - return True - return self._playerPaused - - def getGlobalPosition(self): - if not self._lastGlobalUpdate: - return 0.0 - position = self._globalPosition - if not self._globalPaused: - position += time.time() - self._lastGlobalUpdate - return position - - def getGlobalPaused(self): - if not self._lastGlobalUpdate: - return True - return self._globalPaused - - def eofReportedByPlayer(self): - if self.playlist.notJustChangedPlaylist() and self.userlist.currentUser.file: - self.ui.showDebugMessage("Fixing file duration to allow for playlist advancement") - self.userlist.currentUser.file["duration"] = self._playerPosition - - def updateFile(self, filename, duration, path): - self.lastUpdatedFileTime = time.time() - newPath = "" - if utils.isURL(path): - filename = path - if not path: - return - try: - size = os.path.getsize(path) - except: - try: - path = path.decode('utf-8') - size = os.path.getsize(path) - except: - size = 0 - if not utils.isURL(path) and os.path.exists(path): - self.fileSwitch.notifyUserIfFileNotInMediaDirectory(filename, path) - filename, size = self.__executePrivacySettings(filename, size) - self.userlist.currentUser.setFile(filename, duration, size, path) - self.sendFile() - self.playlist.changeToPlaylistIndexFromFilename(filename) - - def setTrustedDomains(self, newTrustedDomains): - from syncplay.ui.ConfigurationGetter import ConfigurationGetter - ConfigurationGetter().setConfigOption("trustedDomains", newTrustedDomains) - oldTrustedDomains = self._config['trustedDomains'] - if oldTrustedDomains != newTrustedDomains: - self._config['trustedDomains'] = newTrustedDomains - self.fileSwitchFoundFiles() - self.ui.showMessage("Trusted domains updated") - # TODO: Properly add message for setting trusted domains! - # TODO: Handle cases where users add www. to start of domain - - def setRoomList(self, newRoomList): - newRoomList = sorted(newRoomList) - from syncplay.ui.ConfigurationGetter import ConfigurationGetter - ConfigurationGetter().setConfigOption("roomList", newRoomList) - oldRoomList = self._config['roomList'] - if oldRoomList != newRoomList: - self._config['roomList'] = newRoomList - - def _isURITrustableAndTrusted(self, URIToTest): - """Returns a tuple of booleans: (trustable, trusted). - - A given URI is "trustable" if it uses HTTP or HTTPS (constants.TRUSTABLE_WEB_PROTOCOLS). - A given URI is "trusted" if it matches an entry in the trustedDomains config. - Such an entry is considered matching if the domain is the same and the path - is a prefix of the given URI's path. - A "trustable" URI is always "trusted" if the config onlySwitchToTrustedDomains is false. - """ - o = urlparse(URIToTest) - trustable = o.scheme in constants.TRUSTABLE_WEB_PROTOCOLS - if not trustable: - # untrustable URIs are never trusted, return early - return False, False - if not self._config['onlySwitchToTrustedDomains']: - # trust all trustable URIs in this case - return trustable, True - # check for matching trusted domains - if self._config['trustedDomains']: - for entry in self._config['trustedDomains']: - trustedDomain, _, path = entry.partition('/') - foundMatch = False - if o.hostname in (trustedDomain, "www." + trustedDomain): - foundMatch = True - elif "*" in trustedDomain: - wildcardRegex = "^("+re.escape(trustedDomain).replace("\\*","([^.]+)")+")$" - wildcardMatch = bool(re.fullmatch(wildcardRegex, o.hostname, re.IGNORECASE)) - if wildcardMatch: - foundMatch = True - if not foundMatch: - continue - if path and not o.path.startswith('/' + path): - # trusted domain has a path component and it does not match - continue - # match found, trust this domain - return trustable, True - # no matches found, do not trust this domain - return trustable, False - - def isUntrustedTrustableURI(self, URIToTest): - if utils.isURL(URIToTest): - trustable, trusted = self._isURITrustableAndTrusted(URIToTest) - return trustable and not trusted - return False - - def isURITrusted(self, URIToTest): - trustable, trusted = self._isURITrustableAndTrusted(URIToTest) - return trustable and trusted - - def openFile(self, filePath, resetPosition=False, fromUser=False): - if not (filePath.startswith("http://") or filePath.startswith("https://"))\ - and ((fromUser and filePath.endswith(".txt")) or filePath.endswith(".m3u") or filePath.endswith(".m3u8")): - self.playlist.loadPlaylistFromFile(filePath, resetPosition) - return - - self.playlist.openedFile() - self._player.openFile(filePath, resetPosition) - if resetPosition: - self.rewindFile() - self.establishRewindDoubleCheck() - self.lastRewindTime = time.time() - self.autoplayCheck() - - def fileSwitchFoundFiles(self): - self.ui.fileSwitchFoundFiles() - self.playlist.loadCurrentPlaylistIndex() - - def setPlaylistIndex(self, index): - self._protocol.setPlaylistIndex(index) - - def changeToPlaylistIndex(self, *args, **kwargs): - self.playlist.changeToPlaylistIndex(*args, **kwargs) - - def loopSingleFiles(self): - return self._config["loopSingleFiles"] or self.isPlayingMusic() - - def isPlaylistLoopingEnabled(self): - return self._config["loopAtEndOfPlaylist"] or self.isPlayingMusic() - - def __executePrivacySettings(self, filename, size): - if self._config['filenamePrivacyMode'] == PRIVACY_SENDHASHED_MODE: - filename = utils.hashFilename(filename) - elif self._config['filenamePrivacyMode'] == PRIVACY_DONTSEND_MODE: - filename = PRIVACY_HIDDENFILENAME - if self._config['filesizePrivacyMode'] == PRIVACY_SENDHASHED_MODE: - size = utils.hashFilesize(size) - elif self._config['filesizePrivacyMode'] == PRIVACY_DONTSEND_MODE: - size = 0 - return filename, size - - def setServerVersion(self, version, featureList): - self.serverVersion = version - self.checkForFeatureSupport(featureList) - - def sendFeaturesToPlayer(self): - self._player.setFeatures(self.serverFeatures) - - def checkForFeatureSupport(self, featureList): - self.serverFeatures = { - "featureList": utils.meetsMinVersion(self.serverVersion, constants.FEATURE_LIST_MIN_VERSION), - "sharedPlaylists": utils.meetsMinVersion(self.serverVersion, constants.SHARED_PLAYLIST_MIN_VERSION), - "chat": utils.meetsMinVersion(self.serverVersion, constants.CHAT_MIN_VERSION), - "readiness": utils.meetsMinVersion(self.serverVersion, constants.USER_READY_MIN_VERSION), - "managedRooms": utils.meetsMinVersion(self.serverVersion, constants.CONTROLLED_ROOMS_MIN_VERSION), - "persistentRooms": False, - "maxChatMessageLength": constants.FALLBACK_MAX_CHAT_MESSAGE_LENGTH, - "maxUsernameLength": constants.FALLBACK_MAX_USERNAME_LENGTH, - "maxRoomNameLength": constants.FALLBACK_MAX_ROOM_NAME_LENGTH, - "maxFilenameLength": constants.FALLBACK_MAX_FILENAME_LENGTH, - "setOthersReadiness": utils.meetsMinVersion(self.serverVersion, constants.SET_OTHERS_READINESS_MIN_VERSION) - } - if featureList: - self.serverFeatures.update(featureList) - if not utils.meetsMinVersion(self.serverVersion, constants.SHARED_PLAYLIST_MIN_VERSION): - self.ui.showErrorMessage(getMessage("shared-playlists-not-supported-by-server-error").format(constants.SHARED_PLAYLIST_MIN_VERSION, self.serverVersion)) - elif not self.serverFeatures["sharedPlaylists"]: - self.ui.showErrorMessage(getMessage("shared-playlists-disabled-by-server-error")) - # TODO: Have messages for all unsupported & disabled features - if self.serverFeatures["maxChatMessageLength"] is not None: - constants.MAX_CHAT_MESSAGE_LENGTH = self.serverFeatures["maxChatMessageLength"] - if self.serverFeatures["maxUsernameLength"] is not None: - constants.MAX_USERNAME_LENGTH = self.serverFeatures["maxUsernameLength"] - if self.serverFeatures["maxRoomNameLength"] is not None: - constants.MAX_ROOM_NAME_LENGTH = self.serverFeatures["maxRoomNameLength"] - if self.serverFeatures["maxFilenameLength"] is not None: - constants.MAX_FILENAME_LENGTH = self.serverFeatures["maxFilenameLength"] - constants.MPV_SYNCPLAYINTF_CONSTANTS_TO_SEND = [ - "MaxChatMessageLength={}".format(constants.MAX_CHAT_MESSAGE_LENGTH), - "inputPromptStartCharacter={}".format(constants.MPV_INPUT_PROMPT_START_CHARACTER), - "inputPromptEndCharacter={}".format(constants.MPV_INPUT_PROMPT_END_CHARACTER), - "backslashSubstituteCharacter={}".format(constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER)] - self.ui.setFeatures(self.serverFeatures) - if self._player: - self.sendFeaturesToPlayer() - else: - # Player might not have been loaded if connecting to localhost (#545) - self.addPlayerReadyCallback(lambda x: self.sendFeaturesToPlayer()) - - def getSanitizedCurrentUserFile(self): - if self.userlist.currentUser.file: - file_ = deepcopy(self.userlist.currentUser.file) - if constants.PRIVATE_FILE_FIELDS: - for PrivateField in constants.PRIVATE_FILE_FIELDS: - if PrivateField in file_: - file_.pop(PrivateField) - return file_ - else: - return None - - def sendFile(self): - file_ = self.getSanitizedCurrentUserFile() - if self._protocol and self._protocol.logged and file_: - self._protocol.sendFileSetting(file_) - - def setUsername(self, username): - if username and username != "": - self.userlist.currentUser.username = username - else: - random_number = random.randrange(1000, 9999) - self.userlist.currentUser.username = "Anonymous" + str(random_number) # Not localised as this would give away locale - - def getUsername(self): - return self.userlist.currentUser.username - - def chatIsEnabled(self): - return True - # TODO: Allow chat to be disabled - - def getFeatures(self): - features = dict() - - # Can change during runtime: - features["sharedPlaylists"] = self.sharedPlaylistIsEnabled() # Can change during runtime - features["chat"] = self.chatIsEnabled() # Can change during runtime - features["uiMode"] = self.ui.getUIMode() - - # Static for this version/release of Syncplay: - features["featureList"] = True - features["readiness"] = True - features["managedRooms"] = True - features["persistentRooms"] = True - features["setOthersReadiness"] = True - - return features - - def setRoom(self, roomName, resetAutoplay=False): - self.lastSetRoomTime = time.time() - roomSplit = roomName.split(":") - if roomName.startswith("+") and len(roomSplit) > 2: - roomName = roomSplit[0] + ":" + roomSplit[1] - password = roomSplit[2] - self.storeControlPassword(roomName, password) - self.ui.updateRoomName(roomName) - self.userlist.currentUser.room = roomName - if resetAutoplay: - self.resetAutoPlayState() - - def sendRoom(self): - room = self.userlist.currentUser.room - if self._protocol and self._protocol.logged and room: - self._protocol.sendRoomSetting(room) - self.getUserList() - self.reIdentifyAsController() - - def reIdentifyAsController(self): - self.setRoom(self.userlist.currentUser.room) - room = self.userlist.currentUser.room - if utils.RoomPasswordProvider.isControlledRoom(room): - storedRoomPassword = self.getControlledRoomPassword(room) - if storedRoomPassword: - self.identifyAsController(storedRoomPassword) - - def isConnectedAndInARoom(self): - return self._protocol and self._protocol.logged and self.userlist.currentUser.room - - def sharedPlaylistIsEnabled(self): - if "sharedPlaylists" in self.serverFeatures and not self.serverFeatures["sharedPlaylists"]: - sharedPlaylistEnabled = False - else: - sharedPlaylistEnabled = self._config['sharedPlaylistEnabled'] - return sharedPlaylistEnabled - - def connected(self): - self.lastConnectTime = time.time() - readyState = self._config['readyAtStart'] if self.userlist.currentUser.isReady() is None else self.userlist.currentUser.isReady() - self._protocol.setReady(readyState, manuallyInitiated=False) - self.reIdentifyAsController() - if self._config["loadPlaylistFromFile"]: - self.playlist.loadPlaylistFromFile(self._config["loadPlaylistFromFile"]) - self._config["loadPlaylistFromFile"] = None - - def getRoom(self): - return self.userlist.currentUser.room - - def getConfig(self): - return self._config - - def getUserList(self): - if self._protocol and self._protocol.logged: - self._protocol.sendList() - - def showUserList(self, altUI=None): - self.userlist.showUserList(altUI) - - def getPassword(self): - if self.thisIsPublicServer(): - return "" - else: - return self._serverPassword - - def thisIsPublicServer(self): - self._publicServers = [] - if self._publicServers and self._host in self._publicServers: - return True - i = 0 - for server in constants.FALLBACK_PUBLIC_SYNCPLAY_SERVERS: - if server[1] == self._host: - return True - i += 1 - - def setPosition(self, position): - if self._lastPlayerUpdate: - self._lastPlayerUpdate = time.time() - if self.lastRewindTime is not None and abs(time.time() - self.lastRewindTime) < 1.0 and position > 5: - self.ui.showDebugMessage("Ignored seek to {} after rewind".format(position)) - return - position += self.getUserOffset() - if self._player and self.userlist.currentUser.file: - if position < 0: - position = 0 - self._protocol.sendState(self.getPlayerPosition(), self.getPlayerPaused(), True, None, True) - self._player.setPosition(position) - - def setPaused(self, paused): - if self._player and self.userlist.currentUser.file: - if self._lastPlayerUpdate and not paused: - self._lastPlayerUpdate = time.time() - self._player.setPaused(paused) - - def start(self, host, port): - if self._running: - return - self._running = True - if self._playerClass: - perPlayerArguments = utils.getPlayerArgumentsByPathAsArray(self._config['perPlayerArguments'], self._config['playerPath']) - if perPlayerArguments: - self._config['playerArgs'].extend(perPlayerArguments) - filePath = self._config['file'] - if self._config['sharedPlaylistEnabled'] and filePath is not None: - self.delayedLoadPath = filePath - filePath = "" - reactor.callLater(0.1, self._playerClass.run, self, self._config['playerPath'], filePath, self._config['playerArgs'], ) - self._playerClass = None - self.protocolFactory = SyncClientFactory(self) - if '[' in host: - host = host.strip('[]') - port = int(port) - self._endpoint = HostnameEndpoint(reactor, host, port) - try: - certs = pem.parse_file(SSL_CERT_FILE) - trustRoot = trustRootFromCertificates([Certificate.loadPEM(str(cert)) for cert in certs]) - self.protocolFactory.options = optionsForClientTLS(hostname=host, trustRoot=trustRoot) - self._clientSupportsTLS = True - except Exception as e: - self.ui.showDebugMessage(str(e)) - self.protocolFactory.options = None - self._clientSupportsTLS = False - - def retry(retries): - # Use shared state reset method - self._performRetryStateReset() - if retries == 0: - self.onDisconnect() - if retries > constants.RECONNECT_RETRIES: - reactor.callLater(0.1, self.ui.showErrorMessage, getMessage("connection-failed-notification"), - True) - reactor.callLater(0.1, self.stop, True) - return None - - return(0.1 * (2 ** min(retries, 5))) - - self._reconnectingService = ClientService(self._endpoint, self.protocolFactory, retryPolicy=retry) - try: - waitForConnection = self._reconnectingService.whenConnected(failAfterFailures=1) - except TypeError: - waitForConnection = self._reconnectingService.whenConnected() - self._reconnectingService.startService() - - def connectedNow(f): - hostIP = connectionHandle.result.transport.addr[0] - self.ui.showMessage(getMessage("reachout-successful-notification").format(host, hostIP)) - return - - def failed(f): - reactor.callLater(0.1, self.ui.showErrorMessage, getMessage("connection-failed-notification"), True) - reactor.callLater(0.1, self.stop, True) - - connectionHandle = waitForConnection.addCallbacks(connectedNow, failed) - message = getMessage("connection-attempt-notification").format(host, port) - self.ui.showMessage(message) - reactor.run() - - def stop(self, promptForAction=False): - if not self._running: - return - self._running = False - self.destroyProtocol() - if self._player: - self._player.drop() - if self.ui: - self.ui.drop() - reactor.callLater(0.1, reactor.stop) - 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) - def wrapper(self, *args, **kwds): - if self.serverVersion == "0.0.0": - self.ui.showDebugMessage( - "Tried to check server version too soon (testing support for: {})".format(featureRequired)) - return None - if featureRequired not in self.serverFeatures or not self.serverFeatures[featureRequired]: - featureName = getMessage("feature-{}".format(featureRequired)) - self.ui.showErrorMessage(getMessage("not-supported-by-server-error").format(featureName)) - return - return f(self, *args, **kwds) - return wrapper - return requireServerFeatureDecorator - - @requireServerFeature("chat") - def sendChat(self, message): - if self._protocol and self._protocol.logged: - try: - message = message.replace("\n", "").replace("\r", "") - except: - pass - message = utils.truncateText(message, constants.MAX_CHAT_MESSAGE_LENGTH) - self._protocol.sendChatMessage(message) - - @requireServerFeature("setOthersReadiness") - def setOthersReadiness(self, username, newReadyStatus): - self._protocol.setReady(newReadyStatus, True, username) - - def sendFeaturesUpdate(self, features): - self._protocol.sendFeaturesUpdate(features) - - def changePlaylistEnabledState(self, newState): - oldState = self.sharedPlaylistIsEnabled() - from syncplay.ui.ConfigurationGetter import ConfigurationGetter - ConfigurationGetter().setConfigOption("sharedPlaylistEnabled", newState) - self._config["sharedPlaylistEnabled"] = newState - if oldState == False and newState == True: - self.playlist.loadCurrentPlaylistIndex() - - def changeAutoplayState(self, newState): - self.autoPlay = newState - self.autoplayCheck() - - def changeAutoPlayThrehsold(self, newThreshold): - oldAutoplayConditionsMet = self.autoplayConditionsMet() - self.autoPlayThreshold = newThreshold - newAutoplayConditionsMet = self.autoplayConditionsMet() - if oldAutoplayConditionsMet == False and newAutoplayConditionsMet == True: - self.autoplayCheck() - - def autoplayCheck(self): - if self.isPlayingMusic(): - return True - if self.autoplayConditionsMet(): - self.startAutoplayCountdown() - else: - self.stopAutoplayCountdown() - - def instaplayConditionsMet(self): - if self.isPlayingMusic(): - return True - if not self.userlist.currentUser.canControl(): - return False - - unpauseAction = self._config['unpauseAction'] - if self.userlist.currentUser.isReady() or unpauseAction == constants.UNPAUSE_ALWAYS_MODE: - return True - elif unpauseAction == constants.UNPAUSE_IFOTHERSREADY_MODE and self.userlist.areAllOtherUsersInRoomReady(): - return True - elif unpauseAction == constants.UNPAUSE_IFMINUSERSREADY_MODE and self.userlist.areAllOtherUsersInRoomReady()\ - and self.autoPlayThreshold and self.userlist.usersInRoomCount() >= self.autoPlayThreshold: - return True - else: - return False - - def autoplayConditionsMet(self): - if self.seamlessMusicOveride(): - self.setPaused(False) - recentlyAdvanced = self._recentlyAdvanced() - return ( - self._playerPaused and (self.autoPlay or recentlyAdvanced) and - self.userlist.currentUser.canControl() and self.userlist.isReadinessSupported() - and self.userlist.areAllUsersInRoomReady(requireSameFilenames=self._config["autoplayRequireSameFilenames"]) - and ((self.autoPlayThreshold and self.userlist.usersInRoomCount() >= self.autoPlayThreshold) or recentlyAdvanced) - ) - - def autoplayTimerIsRunning(self): - return self.autoplayTimer.running - - def startAutoplayCountdown(self): - if self.autoplayConditionsMet() and not self.autoplayTimer.running: - self.autoplayTimeLeft = constants.AUTOPLAY_DELAY - self.autoplayTimer.start(1) - - def stopAutoplayCountdown(self): - if self.autoplayTimer.running: - self.autoplayTimer.stop() - self.autoplayTimeLeft = constants.AUTOPLAY_DELAY - - def autoplayCountdown(self): - if not self.autoplayConditionsMet(): - self.stopAutoplayCountdown() - return - allReadyMessage = getMessage("all-users-ready").format(self.userlist.readyUserCount()) - autoplayingMessage = getMessage("autoplaying-notification").format(int(self.autoplayTimeLeft)) - countdownMessage = "{}{}{}".format(allReadyMessage, self._player.osdMessageSeparator, autoplayingMessage) - self.ui.showOSDMessage(countdownMessage, 1, OSDType=constants.OSD_ALERT, mood=constants.MESSAGE_GOODNEWS) - if self.autoplayTimeLeft <= 0: - self.setPaused(False) - self.stopAutoplayCountdown() - else: - self.autoplayTimeLeft -= 1 - - def resetAutoPlayState(self): - self.autoPlay = False - self.ui.updateAutoPlayState(False) - self.stopAutoplayCountdown() - - @requireServerFeature("readiness") - def toggleReady(self, manuallyInitiated=True): - self._protocol.setReady(not self.userlist.currentUser.isReady(), manuallyInitiated) - - @requireServerFeature("readiness") - def changeReadyState(self, newState, manuallyInitiated=True): - oldState = self.userlist.currentUser.isReady() - if newState != oldState: - self.toggleReady(manuallyInitiated) - - def setReady(self, username, isReady, manuallyInitiated=True, setBy=None): - oldReadyState = self.userlist.isReady(username) - if oldReadyState is None: - oldReadyState = False - self.userlist.setReady(username, isReady) - self.ui.userListChange() - if oldReadyState != isReady: - self._warnings.checkReadyStates() - if setBy: - if isReady: - self.ui.showMessage(getMessage("other-set-as-ready-notification").format(username, setBy)) - else: - self.ui.showMessage(getMessage("other-set-as-not-ready-notification").format(username, setBy)) - - @requireServerFeature("managedRooms") - def setUserFeatures(self, username, features): - self.userlist.setFeatures(username, features) - self.ui.userListChange() - - @requireServerFeature("managedRooms") - def createControlledRoom(self, roomName): - controlPassword = utils.RandomStringGenerator.generate_room_password() - self.lastControlPasswordAttempt = controlPassword - self._protocol.requestControlledRoom(roomName, controlPassword) - - def controlledRoomCreated(self, roomName, controlPassword): - self.ui.showMessage(getMessage("created-controlled-room-notification").format(roomName, controlPassword, roomName, roomName + ":" + controlPassword)) - self.setRoom(roomName, resetAutoplay=True) - self.sendRoom() - self._protocol.requestControlledRoom(roomName, controlPassword) - self.ui.updateRoomName(roomName) - - def stripControlPassword(self, controlPassword): - if controlPassword: - return re.sub(constants.CONTROL_PASSWORD_STRIP_REGEX, "", controlPassword).upper() - else: - return "" - - def identifyAsController(self, controlPassword): - controlPassword = self.stripControlPassword(controlPassword) - self.ui.showMessage(getMessage("identifying-as-controller-notification").format(controlPassword)) - self.lastControlPasswordAttempt = controlPassword - self._protocol.requestControlledRoom(self.getRoom(), controlPassword) - - def controllerIdentificationError(self, username, room): - if username == self.getUsername(): - self.ui.showErrorMessage(getMessage("failed-to-identify-as-controller-notification").format(username)) - - def controllerIdentificationSuccess(self, username, roomname): - self.userlist.setUserAsController(username) - if self.userlist.isRoomSame(roomname): - hideFromOSD = not constants.SHOW_SAME_ROOM_OSD - self.ui.showMessage(getMessage("authenticated-as-controller-notification").format(username), hideFromOSD) - if username == self.userlist.currentUser.username: - self.storeControlPassword(roomname, self.lastControlPasswordAttempt) - self.ui.userListChange() - - def storeControlPassword(self, room, password): - if password: - self.controlpasswords[room] = password - try: - if self._config['autosaveJoinsToList']: - self.ui.addRoomToList(room+":"+password) - except: - pass - - def getControlledRoomPassword(self, room): - if room in self.controlpasswords: - return self.controlpasswords[room] - - def checkForUpdate(self, userInitiated): - try: - import urllib.request, urllib.parse, urllib.error, syncplay, sys, json, platform - try: - architecture = platform.architecture()[0] - except: - architecture = "Unknown" - try: - machine = platform.machine() - except: - machine = "Unknown" - params = urllib.parse.urlencode({'version': syncplay.version, 'milestone': syncplay.milestone, 'release_number': syncplay.release_number, 'language': syncplay.messages.messages["CURRENT"], 'platform': sys.platform, 'architecture': architecture, 'machine': machine, 'userInitiated': userInitiated}) - if isMacOS(): - import requests - response = requests.get(constants.SYNCPLAY_UPDATE_URL.format(params)) - response = response.text - else: - f = urllib.request.urlopen(constants.SYNCPLAY_UPDATE_URL.format(params)) - response = f.read() - response = response.decode('utf-8') - response = response.replace("
", "").replace("
", "").replace("