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("
", "").replace("“", "\"").replace("”", "\"") # Fix Wordpress - response = json.loads(response) - publicServers = None - if response["public-servers"]: - publicServers = response["public-servers"].\ - replace("”", "'").replace(":’", "'").replace("’", "'").replace("′", "'").replace("\n", "").replace("\r", "") - publicServers = ast.literal_eval(publicServers) - return response["version-status"], response["version-message"] if "version-message" in response\ - else None, response["version-url"] if "version-url" in response else None, publicServers - except Exception as e: - return "failed", str(e)+"\n-----\n"+getMessage("update-check-failed-notification").format(syncplay.version), constants.SYNCPLAY_DOWNLOAD_URL, None - - class _WarningManager(object): - def __init__(self, player, userlist, ui, client): - self._client = client - self._player = player - self._userlist = userlist - self._ui = ui - self._warnings = { - "room-file-differences": { - "timer": task.LoopingCall(self.__displayMessageOnOSD, "room-file-differences", - lambda: self._checkRoomForSameFiles(OSDOnly=True),), - "displayedFor": 0, - }, - "alone-in-the-room": { - "timer": task.LoopingCall(self.__displayMessageOnOSD, "alone-in-the-room", - lambda: self._checkIfYouReAloneInTheRoom(OSDOnly=True)), - "displayedFor": 0, - }, - "not-all-ready": { - "timer": task.LoopingCall(self.__displayMessageOnOSD, "not-all-ready", - lambda: self.checkReadyStates(),), - "displayedFor": 0, - }, - } - self.pausedTimer = task.LoopingCall(self.__displayPausedMessagesOnOSD) - self.pausedTimer.start(constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, True) - - def checkWarnings(self): - if self._client.autoplayConditionsMet(): - return - self._checkIfYouReAloneInTheRoom(OSDOnly=False) - self._checkRoomForSameFiles(OSDOnly=False) - self.checkReadyStates() - - def _checkRoomForSameFiles(self, OSDOnly): - if not self._userlist.areAllFilesInRoomSame(): - self._displayReadySameWarning() - if not OSDOnly and constants.SHOW_OSD_WARNINGS and not self._warnings["room-file-differences"]['timer'].running: - self._warnings["room-file-differences"]['timer'].start(constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, True) - elif self._warnings["room-file-differences"]['timer'].running: - self._warnings["room-file-differences"]['timer'].stop() - - def _checkIfYouAreOnlyUserInRoomWhoSupportsReadiness(self): - self._userlist._onlyUserInRoomWhoSupportsReadiness() - - def _checkIfYouReAloneInTheRoom(self, OSDOnly): - if self._userlist.areYouAloneInRoom(): - self._ui.showOSDMessage(getMessage("alone-in-the-room"), constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, OSDType=constants.OSD_ALERT, mood=constants.MESSAGE_BADNEWS) - if not OSDOnly: - self._ui.showMessage(getMessage("alone-in-the-room"), True) - if constants.SHOW_OSD_WARNINGS and not self._warnings["alone-in-the-room"]['timer'].running: - self._warnings["alone-in-the-room"]['timer'].start(constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, True) - elif self._warnings["alone-in-the-room"]['timer'].running: - self._warnings["alone-in-the-room"]['timer'].stop() - - def checkReadyStates(self): - if not self._client: - return - if self._client.getPlayerPaused() or not self._userlist.currentUser.isReady() or not self._userlist.areAllRelevantUsersInRoomReady(): - self._warnings["not-all-ready"]["displayedFor"] = 0 - if self._userlist.areYouAloneInRoom(): - if self._warnings["not-all-ready"]['timer'].running: - self._warnings["not-all-ready"]['timer'].stop() - elif not self._userlist.areAllRelevantUsersInRoomReady(): - self._displayReadySameWarning() - if constants.SHOW_OSD_WARNINGS and not self._warnings["not-all-ready"]['timer'].running: - self._warnings["not-all-ready"]['timer'].start(constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, True) - elif self._warnings["not-all-ready"]['timer'].running: - self._warnings["not-all-ready"]['timer'].stop() - self._displayReadySameWarning() - elif self._client.getPlayerPaused() or not self._userlist.currentUser.isReady(): - self._displayReadySameWarning() - - def _displayReadySameWarning(self): - if not self._client._player or self._client.autoplayTimerIsRunning(): - return - osdMessage = None - messageMood = constants.MESSAGE_GOODNEWS - fileDifferencesForRoom = self._userlist.getFileDifferencesForRoom() - if not self._userlist.areAllFilesInRoomSame() and fileDifferencesForRoom is not None: - messageMood = constants.MESSAGE_BADNEWS - fileDifferencesMessage = getMessage("room-file-differences").format(fileDifferencesForRoom) - if self._userlist.currentUser.canControl() and self._userlist.isReadinessSupported(): - if self._userlist.areAllUsersInRoomReady(): - allReadyMessage = getMessage("all-users-ready").format(self._userlist.readyUserCount()) - osdMessage = "{}{}{}".format(fileDifferencesMessage, self._client._player.osdMessageSeparator, allReadyMessage) - else: - notAllReadyMessage = getMessage("not-all-ready").format(self._userlist.usersInRoomNotReady()) - osdMessage = "{}{}{}".format(fileDifferencesMessage, self._client._player.osdMessageSeparator, notAllReadyMessage) - else: - osdMessage = fileDifferencesMessage - elif self._userlist.isReadinessSupported(): - if self._userlist.areAllUsersInRoomReady(): - osdMessage = getMessage("all-users-ready").format(self._userlist.readyUserCount()) - else: - messageMood = constants.MESSAGE_BADNEWS - osdMessage = getMessage("not-all-ready").format(self._userlist.usersInRoomNotReady()) - if osdMessage: - self._ui.showOSDMessage(osdMessage, constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, OSDType=constants.OSD_ALERT, mood=messageMood) - - def __displayMessageOnOSD(self, warningName, warningFunction): - if constants.OSD_WARNING_MESSAGE_DURATION > self._warnings[warningName]["displayedFor"]: - warningFunction() - self._warnings[warningName]["displayedFor"] += constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL - else: - self._warnings[warningName]["displayedFor"] = 0 - try: - self._warnings[warningName]["timer"].stop() - except: - pass - - def __displayPausedMessagesOnOSD(self): - if self._client.autoplayConditionsMet(): - return - if self._client and self._client._player and self._client.getPlayerPaused(): - self._checkRoomForSameFiles(OSDOnly=True) - self.checkReadyStates() - elif not self._userlist.currentUser.isReady(): # CurrentUser should always be reminded they are set to not ready - self.checkReadyStates() - - -class SyncplayUser(object): - def __init__(self, username=None, room=None, file_=None): - self.ready = None - self.username = username - self.room = room - self.file = file_ - self._controller = False - self._features = {} - - def setFile(self, filename, duration, size, path=None): - file_ = { - "name": filename, - "duration": duration, - "size": size, - "path": path - } - self.file = file_ - - def isFileSame(self, file_): - if not self.file: - return False - sameName = utils.sameFilename(self.file['name'], file_['name']) - sameSize = utils.sameFilesize(self.file['size'], file_['size']) - sameDuration = utils.sameFileduration(self.file['duration'], file_['duration']) - return sameName and sameSize and sameDuration - - def __lt__(self, other): - if self.isController() == other.isController(): - return self.username.lower() < other.username.lower() - else: - return self.isController() > other.isController() - - def __repr__(self, *args, **kwargs): - if self.file: - return "{}: {} ({}, {})".format(self.username, self.file['name'], self.file['duration'], self.file['size']) - else: - return "{}".format(self.username) - - def setControllerStatus(self, isController): - self._controller = isController - - def isController(self): - return self._controller - - def canControl(self): - if self.isController() or not utils.RoomPasswordProvider.isControlledRoom(self.room): - return True - else: - return False - - def isReadyWithFile(self): - if self.file is None: - return None - return self.ready - - def isReady(self): - return self.ready - - def setReady(self, ready): - self.ready = ready - - def setFeatures(self, features): - self._features = features - - -class SyncplayUserlist(object): - def __init__(self, ui, client): - self.currentUser = SyncplayUser() - self._users = {} - self.ui = ui - self._client = client - self._roomUsersChanged = True - - def isReadinessSupported(self, requiresOtherUsers=True): - if not utils.meetsMinVersion(self._client.serverVersion, constants.USER_READY_MIN_VERSION): - return False - elif self.onlyUserInRoomWhoSupportsReadiness() and requiresOtherUsers: - return False - else: - return self._client.serverFeatures["readiness"] - - def isRoomSame(self, room): - if room and self.currentUser.room and self.currentUser.room == room: - return True - else: - return False - - def __showUserChangeMessage(self, username, room, file_, oldRoom=None): - if room: - if self.isRoomSame(room) or self.isRoomSame(oldRoom): - showOnOSD = constants.SHOW_OSD_WARNINGS - else: - showOnOSD = constants.SHOW_DIFFERENT_ROOM_OSD - if constants.SHOW_NONCONTROLLER_OSD == False and self.canControl(username) == False: - showOnOSD = False - hideFromOSD = not showOnOSD - if not file_: - message = getMessage("room-join-notification").format(username, room) - self.ui.showMessage(message, hideFromOSD) - else: - duration = utils.formatTime(file_['duration']) - message = getMessage("playing-notification").format(username, file_['name'], duration) - if self.currentUser.room != room or self.currentUser.username == username: - message += getMessage("playing-notification/room-addendum").format(room) - self.ui.showMessage(message, hideFromOSD) - if self.currentUser.file and not self.currentUser.isFileSame(file_) and self.currentUser.room == room: - fileDifferences = self.getFileDifferencesForUser(self.currentUser.file, file_) - if fileDifferences is not None: - message = getMessage("file-differences-notification").format(fileDifferences) - self.ui.showMessage(message, True) - - def getFileDifferencesForUser(self, currentUserFile, otherUserFile): - if not currentUserFile or not otherUserFile: - return None - differences = [] - differentName = not utils.sameFilename(currentUserFile['name'], otherUserFile['name']) - differentSize = not utils.sameFilesize(currentUserFile['size'], otherUserFile['size']) - differentDuration = not utils.sameFileduration(currentUserFile['duration'], otherUserFile['duration']) - if differentName: differences.append(getMessage("file-difference-filename")) - if differentSize: differences.append(getMessage("file-difference-filesize")) - if differentDuration: differences.append(getMessage("file-difference-duration")) - return ", ".join(differences) - - def getFileDifferencesForRoom(self): - if not self.currentUser.file: - return None - differences = [] - differentName = False - differentSize = False - differentDuration = False - for otherUser in self._users.values(): - if otherUser.room == self.currentUser.room and otherUser.file: - if not utils.sameFilename(self.currentUser.file['name'], otherUser.file['name']): - differentName = True - if not utils.sameFilesize(self.currentUser.file['size'], otherUser.file['size']): - differentSize = True - if not utils.sameFileduration(self.currentUser.file['duration'], otherUser.file['duration']): - differentDuration = True - if differentName: differences.append(getMessage("file-difference-filename")) - if differentSize: differences.append(getMessage("file-difference-filesize")) - if differentDuration: differences.append(getMessage("file-difference-duration")) - return ", ".join(differences) - - def addUser(self, username, room, file_, noMessage=False, isController=None, isReady=None, features={}): - if username == self.currentUser.username: - if isController is not None: - self.currentUser.setControllerStatus(isController) - self.currentUser.setReady(isReady) - return - user = SyncplayUser(username, room, file_) - if isController is not None: - user.setControllerStatus(isController) - self._users[username] = user - user.setReady(isReady) - user.setFeatures(features) - if not noMessage: - self.__showUserChangeMessage(username, room, file_) - self.userListChange(room) - - def removeUser(self, username): - hideFromOSD = not constants.SHOW_DIFFERENT_ROOM_OSD - if username in self._users: - user = self._users[username] - if user.room: - if self.isRoomSame(user.room): - hideFromOSD = not constants.SHOW_SAME_ROOM_OSD - if username in self._users: - self._users.pop(username) - message = getMessage("left-notification").format(username) - self.ui.showMessage(message, hideFromOSD) - self._client.lastLeftTime = time.time() - self._client.lastLeftUser = username - self.userListChange() - - def __displayModUserMessage(self, username, room, file_, user, oldRoom): - if file_ and not user.isFileSame(file_): - self.__showUserChangeMessage(username, room, file_, oldRoom) - elif room and room != user.room: - self.__showUserChangeMessage(username, room, None, oldRoom) - - def modUser(self, username, room, file_): - if username in self._users: - user = self._users[username] - oldRoom = user.room if user.room else None - if user.room != room: - user.setControllerStatus(isController=False) - self.__displayModUserMessage(username, room, file_, user, oldRoom) - user.room = room - if file_: - user.file = file_ - elif username == self.currentUser.username: - self.__showUserChangeMessage(username, room, file_) - else: - self.addUser(username, room, file_) - self.userListChange(room) - - def setUserAsController(self, username): - if self.currentUser.username == username: - self.currentUser.setControllerStatus(True) - elif username in self._users: - user = self._users[username] - user.setControllerStatus(True) - - def areAllRelevantUsersInRoomReady(self, requireSameFilenames=False): - if not self.currentUser.isReady(): - return False - if self.currentUser.canControl(): - return self.areAllUsersInRoomReady(requireSameFilenames) - else: - for user in self._users.values(): - if user.room == self.currentUser.room and user.canControl(): - if user.isReadyWithFile() == False: - return False - elif ( - requireSameFilenames and - ( - self.currentUser.file is None - or user.file is None - or not utils.sameFilename(self.currentUser.file['name'], user.file['name']) - ) - ): - return False - return True - - def areAllUsersInRoomReady(self, requireSameFilenames=False): - if not self.currentUser.isReady(): - return False - for user in self._users.values(): - if user.room == self.currentUser.room: - if user.isReadyWithFile() == False: - return False - elif ( - requireSameFilenames and - ( - self.currentUser.file is None - or user.file is None - or not utils.sameFilename(self.currentUser.file['name'], user.file['name']) - ) - ): - return False - return True - - def areAllOtherUsersInRoomReady(self): - for user in self._users.values(): - if user.room == self.currentUser.room and user.isReadyWithFile() == False: - return False - return True - - def readyUserCount(self): - readyCount = 0 - if self.currentUser.isReady(): - readyCount += 1 - for user in self._users.values(): - if user.room == self.currentUser.room and user.isReadyWithFile(): - readyCount += 1 - return readyCount - - def usersInRoomCount(self): - userCount = 1 - for user in self._users.values(): - if user.room == self.currentUser.room and user.isReadyWithFile(): - userCount += 1 - return userCount - - def usersInRoomNotReady(self): - notReady = [] - if not self.currentUser.isReady(): - notReady.append(self.currentUser.username) - for user in self._users.values(): - if user.room == self.currentUser.room and user.isReadyWithFile() == False: - notReady.append(user.username) - return ", ".join(notReady) - - def areAllFilesInRoomSame(self): - if self.currentUser.file: - for user in self._users.values(): - if user.room == self.currentUser.room and user.file and not self.currentUser.isFileSame(user.file): - if user.canControl(): - return False - return True - - def areYouAloneInRoom(self): - if self._client.recentlyConnected(): - return False - for user in self._users.values(): - if user.room == self.currentUser.room: - return False - return True - - def onlyUserInRoomWhoSupportsReadiness(self): - for user in self._users.values(): - if user.room == self.currentUser.room and user.isReadyWithFile() is not None: - return False - return True - - def isUserInYourRoom(self, username): - for user in self._users.values(): - if user.username == username and user.room == self.currentUser.room: - return True - return False - - def canControl(self, username): - if self.currentUser.username == username and self.currentUser.canControl(): - return True - - for user in self._users.values(): - if user.username == username and user.canControl(): - return True - return False - - def isReadyWithFile(self, username): - if self.currentUser.username == username: - return self.currentUser.isReadyWithFile() - - for user in self._users.values(): - if user.username == username: - return user.isReadyWithFile() - return None - - def isReady(self, username): - if self.currentUser.username == username: - return self.currentUser.isReady() - - for user in self._users.values(): - if user.username == username: - return user.isReady() - return None - - def setReady(self, username, isReady): - if self.currentUser.username == username: - self.currentUser.setReady(isReady) - elif username in self._users: - self._users[username].setReady(isReady) - self._client.autoplayCheck() - - def userListChange(self, room=None): - if room is not None and self.isRoomSame(room): - self._roomUsersChanged = True - self.ui.userListChange() - - def roomStateConfirmed(self): - self._roomUsersChanged = False - - def hasRoomStateChanged(self): - return self._roomUsersChanged - - def showUserList(self, altUI=None): - rooms = {} - for user in self._users.values(): - if user.room not in rooms: - rooms[user.room] = [] - rooms[user.room].append(user) - if self.currentUser.room not in rooms: - rooms[self.currentUser.room] = [] - rooms[self.currentUser.room].append(self.currentUser) - rooms = self.sortList(rooms) - if altUI: - altUI.showUserList(self.currentUser, rooms) - else: - self.ui.showUserList(self.currentUser, rooms) - self._client.autoplayCheck() - - def clearList(self): - self._users = {} - - def sortList(self, rooms): - for room in rooms: - rooms[room] = sorted(rooms[room]) - rooms = collections.OrderedDict(sorted(list(rooms.items()), key=lambda s: s[0].lower())) - return rooms - - -class UiManager(object): - def __init__(self, client, ui): - self._client = client - self.__ui = ui - self.lastNotificatinOSDMessage = None - self.lastNotificationOSDEndTime = None - self.lastAlertOSDMessage = None - self.lastAlertOSDEndTime = None - self.lastError = "" - - def getUIMode(self): - return self.__ui.uiMode - - def addFileToPlaylist(self, newPlaylistItem): - self.__ui.addFileToPlaylist(newPlaylistItem) - - def setPlaylist(self, newPlaylist, newIndexFilename=None): - self.__ui.setPlaylist(newPlaylist, newIndexFilename) - - def setPlaylistIndexFilename(self, filename): - self.__ui.setPlaylistIndexFilename(filename) - - def fileSwitchFoundFiles(self): - self.__ui.fileSwitchFoundFiles() - - def setFeatures(self, featureList): - self.__ui.setFeatures(featureList) - - def showDebugMessage(self, message): - if constants.DEBUG_MODE and message.rstrip(): - sys.stderr.write("{}{}\n".format(time.strftime(constants.UI_TIME_FORMAT, time.localtime()), message.rstrip())) - - def showChatMessage(self, username, userMessage): - messageString = "<{}> {}".format(username, userMessage) - if self._client._player.chatOSDSupported and self._client._config["chatOutputEnabled"]: - self._client._player.displayChatMessage(username, userMessage) - else: - self.showOSDMessage(messageString, duration=constants.OSD_DURATION) - self.__ui.showMessage(messageString) - - def setSSLMode(self, sslMode, sslInformation=""): - self.__ui.setSSLMode(sslMode, sslInformation) - - def showMessage(self, message, noPlayer=False, noTimestamp=False, OSDType=constants.OSD_NOTIFICATION, mood=constants.MESSAGE_NEUTRAL, isMotd=False): - if not noPlayer: - self.showOSDMessage(message, duration=constants.OSD_DURATION, OSDType=OSDType, mood=mood) - self.__ui.showMessage(message, noTimestamp=noTimestamp, isMotd=isMotd) - - def updateAutoPlayState(self, newState): - self.__ui.updateAutoPlayState(newState) - - def showUserList(self, currentUser, rooms): - self.__ui.showUserList(currentUser, rooms) - - def showOSDMessage(self, message, duration=constants.OSD_DURATION, OSDType=constants.OSD_NOTIFICATION, mood=constants.MESSAGE_NEUTRAL): - if(isNoOSDMessage(message)): - return - - autoplayConditionsMet = self._client.autoplayConditionsMet() - if OSDType == constants.OSD_ALERT and not constants.SHOW_OSD_WARNINGS and not self._client.autoplayTimerIsRunning(): - return - if not self._client._player: - return - if constants.SHOW_OSD and self._client and self._client._player: - if not self._client._player.alertOSDSupported: - if OSDType == constants.OSD_ALERT: - self.lastAlertOSDMessage = message - if autoplayConditionsMet: - self.lastAlertOSDEndTime = time.time() + 1.0 - else: - self.lastAlertOSDEndTime = time.time() + constants.NO_ALERT_OSD_WARNING_DURATION - if self.lastNotificationOSDEndTime and time.time() < self.lastNotificationOSDEndTime: - message = "{}{}{}".format(message, self._client._player.osdMessageSeparator, self.lastNotificatinOSDMessage) - else: - self.lastNotificatinOSDMessage = message - self.lastNotificationOSDEndTime = time.time() + constants.OSD_DURATION - if self.lastAlertOSDEndTime and time.time() < self.lastAlertOSDEndTime: - message = "{}{}{}".format(self.lastAlertOSDMessage, self._client._player.osdMessageSeparator, message) - self._client._player.displayMessage(message, int(duration * 1000), OSDType, mood) - - def setControllerStatus(self, username, isController): - self.__ui.setControllerStatus(username, isController) - - def showErrorMessage(self, message, criticalerror=False): - if message != self.lastError: # Avoid double call bug - self.lastError = message - self.__ui.showErrorMessage(message, criticalerror) - - def promptFor(self, prompt): - return self.__ui.promptFor(prompt) - - def userListChange(self): - self.__ui.userListChange() - - def markEndOfUserlist(self): - self.__ui.markEndOfUserlist() - - def updateRoomName(self, room=""): - self.__ui.updateRoomName(room) - - def addRoomToList(self, room): - self.__ui.addRoomToList(room) - - def executeCommand(self, command): - self.__ui.executeCommand(command) - - def drop(self): - self.__ui.drop() - - -class SyncplayPlaylist(): - def __init__(self, client): - self.queuedIndex = None - self._client = client - self._ui = self._client.ui - self._previousPlaylist = None - self._previousPlaylistRoom = None - self._playlist = [] - self._playlistIndex = None - self.addedChangeListCallback = False - self.switchToNewPlaylistItem = False - self._lastPlaylistIndexChange = time.time() - - def needsSharedPlaylistsEnabled(f): # @NoSelf - @wraps(f) - def wrapper(self, *args, **kwds): - if not self._client.sharedPlaylistIsEnabled(): - self._ui.showDebugMessage("Tried to use shared playlists when it was disabled!") - return - return f(self, *args, **kwds) - return wrapper - - def openedFile(self): - self._lastPlaylistIndexChange = time.time() - - def removeDirsFromPath(self, filePath): - if os.path.isfile(filePath): - return os.path.basename(filePath) - elif utils.isURL(filePath): - return filePath - self._ui.showDebugMessage("Could not find path: {}".format(filePath)) - - def getPlaylistIndexFromPath(self, filePath): - filePath = self.removeDirsFromPath(filePath) - try: - return self._playlist.index(filePath) - except ValueError: - return - - def changeToPlaylistIndexFromFilename(self, filename): - try: - index = self._playlist.index(filename) - if index != self._playlistIndex: - self.changeToPlaylistIndex(index, resetPosition=True) - else: - if filename == self.queuedIndexFilename: - return - self._client.rewindFile() - except ValueError: - pass - - def loadDelayedPath(self, changeToIndex): - # Implementing the behaviour set out at https://github.com/Syncplay/syncplay/issues/315 - - if not self._client: - return - - if self._client.playerIsNotReady(): - self._client.addPlayerReadyCallback(lambda x: self.loadDelayedPath(changeToIndex)) - return - - if self._client._protocol.hadFirstPlaylistIndex and self._client.delayedLoadPath: - delayedLoadPath = str(self._client.delayedLoadPath) - self._client.delayedLoadPath = None - if self._client.sharedPlaylistIsEnabled(): - pathWithoutDirs = self.removeDirsFromPath(delayedLoadPath) - if len(self._playlist) == 0: - self._client.openFile(delayedLoadPath, resetPosition=True, fromUser=True) - self._client.ui.addFileToPlaylist(delayedLoadPath) - else: - try: - currentPlaylistFilename = self._playlist[changeToIndex] - except TypeError: - currentPlaylistFilename = None - if currentPlaylistFilename != pathWithoutDirs: - if pathWithoutDirs not in self._playlist: - if utils.isURL(delayedLoadPath) or utils.isURL(currentPlaylistFilename): - self._client.ui.addFileToPlaylist(delayedLoadPath) - else: - foundFilePath = self._client.fileSwitch.findFilepath(currentPlaylistFilename, highPriority=True) - if foundFilePath is None: - self._client.openFile(delayedLoadPath, resetPosition=False) - else: - self._client.ui.showMessage("{}: {}...".format(getMessage("addfilestoplaylist-menu-label"), pathWithoutDirs)) - reactor.callLater(constants.DELAYED_LOAD_WAIT_TIME, self._client.ui.addFileToPlaylist, delayedLoadPath, ) # TODO: Avoid arbitary pause - else: - self._client.ui.showErrorMessage(getMessage("cannot-add-duplicate-error").format(pathWithoutDirs)) - - else: - self._client.openFile(delayedLoadPath) - - def changeToPlaylistIndex(self, index, username=None, resetPosition=False): - if self.loadDelayedPath(index): - return - if self._playlist is None or len(self._playlist) == 0: - return - if index is None: - return - if username is None and not self._client.sharedPlaylistIsEnabled(): - return - self._lastPlaylistIndexChange = time.time() - if self._client.playerIsNotReady(): - if not self.addedChangeListCallback: - self.addedChangeListCallback = True - self._client.addPlayerReadyCallback(lambda x: self.changeToPlaylistIndex(index, username, resetPosition)) - return - try: - filename = self._playlist[index] - self._ui.setPlaylistIndexFilename(filename) - if not self._client.sharedPlaylistIsEnabled(): - self._playlistIndex = index - if username is not None and self._client.userlist.currentUser.file and filename == self._client.userlist.currentUser.file['name']: - self._playlistIndex = index - return - except IndexError: - pass - - self._playlistIndex = index - if username is None: - if self._client.isConnectedAndInARoom() and self._client.sharedPlaylistIsEnabled(): - self._client.setPlaylistIndex(index) - filename = self._playlist[index] - self._ui.setPlaylistIndexFilename(filename) - if resetPosition: - self._ui.showDebugMessage("Pausing due to index change") - state = {} - state["playstate"] = {} - state["playstate"]["position"] = 0 - state["playstate"]["paused"] = True - self._client.lastAdvanceTime = time.time() - self._client._protocol.sendMessage({"State": state}) - self._playerPaused = True - self._client.autoplayCheck() - elif index is not None: - filename = self._playlist[index] - self._ui.setPlaylistIndexFilename(filename) - self._ui.showMessage(getMessage("playlist-selection-changed-notification").format(username)) - self.switchToNewPlaylistIndex(index, resetPosition=resetPosition) - - def canSwitchToNextPlaylistIndex(self): - if self._thereIsNextPlaylistIndex() and self._client.sharedPlaylistIsEnabled(): - try: - index = self._nextPlaylistIndex() - if index is None: - return False - filename = self._playlist[index] - if utils.isURL(filename): - return True if self._client.isURITrusted(filename) else False - else: - path = self._client.fileSwitch.findFilepath(filename, highPriority=True) - return True if path else False - except: - return False - return False - - @needsSharedPlaylistsEnabled - def switchToNewPlaylistIndex(self, index, resetPosition = False): - try: - self.queuedIndexFilename = self._playlist[index] - except: - self.queuedIndexFilename = None - self._ui.showDebugMessage("Failed to find index {} in playlist".format(index)) - if resetPosition and index is not None: - filename = self._playlist[index] - if (not utils.isURL(filename)) or self._client.isURITrusted(filename): - self._client.prepareToChangeToNewPlaylistItemAndRewind() - - self._lastPlaylistIndexChange = time.time() - if self._client.playerIsNotReady(): - self._client.addPlayerReadyCallback(lambda x: self.switchToNewPlaylistIndex(index, resetPosition)) - return - - try: - if index is None: - self._ui.showDebugMessage("Cannot switch to None index in playlist") - return - filename = self._playlist[index] - # TODO: Handle isse with index being None - if utils.isURL(filename): - if self._client.isURITrusted(filename): - self._client.openFile(filename, resetPosition=resetPosition) - else: - self._ui.showErrorMessage(getMessage("cannot-add-unsafe-path-error").format(filename)) - return - else: - path = self._client.fileSwitch.findFilepath(filename, highPriority=True) - if path: - self._client.openFile(path, resetPosition=resetPosition) - else: - self._ui.showErrorMessage(getMessage("cannot-find-file-for-playlist-switch-error").format(filename)) - return - except IndexError: - self._ui.showDebugMessage("Could not change playlist index due to IndexError") - - def _getValidIndexFromNewPlaylist(self, newPlaylist=None): - if self.switchToNewPlaylistItem: - self.switchToNewPlaylistItem = False - return len(self._playlist) - - if self._playlistIndex is None or not newPlaylist or len(newPlaylist) <= 1: - return 0 - - i = self._playlistIndex - while i <= len(self._playlist): - try: - filename = self._playlist[i] - validIndex = newPlaylist.index(filename) - return validIndex - except: - i += 1 - - i = self._playlistIndex - while i > 0: - try: - filename = self._playlist[i] - validIndex = newPlaylist.index(filename) - return validIndex+1 if validIndex < len(newPlaylist)-1 else validIndex - except: - i -= 1 - return 0 - - def _getFilenameFromIndexInGivenPlaylist(self, _playlist, _index): - if not _index or not _playlist: - return None - filename = _playlist[_index] if len(_playlist) > _index else None - return filename - - def loadPlaylistFromFile(self, path, shuffle=False): - if not os.path.isfile(path): - self._ui.showDebugMessage("Not loading {} as file could not be found".format(path)) - return - - with open(path) as f: - newPlaylist = f.read().splitlines() - if shuffle: - random.shuffle(newPlaylist) - if newPlaylist: - self.changePlaylist(newPlaylist, username=None, resetIndex=True) - - def savePlaylistToFile(self, path): - with open(path, 'w') as playlistFile: - playlistToSave = utils.getListAsMultilineString(self._playlist) - playlistFile.write(playlistToSave) - self._ui.showMessage("Playlist saved as {}".format(path)) # TODO: Move to messages_en - - def playlistNeedsRestoring(self, files, username): - if self._client.playlistMayNeedRestoring: - self._client.playlistMayNeedRestoring = False - return self._client.sharedPlaylistIsEnabled() and self._playlist != None and files == [] and username == None and not self._playlistBufferIsFromOldRoom(self._client.userlist.currentUser.room) - - def changePlaylist(self, files, username=None, resetIndex=False): - if self.playlistNeedsRestoring(files, username): - self._ui.showDebugMessage("Restoring playlist on reconnect...") - files = self._playlist.copy() - self._client._protocol.setPlaylist(files) - self._client._protocol.setPlaylistIndex(self._playlistIndex) - return - self.queuedIndexFilename = None - self._client.playlistMayNeedRestoring = False - if self._playlist == files: - if self._playlistIndex != 0 and resetIndex: - self.changeToPlaylistIndex(0) - return - - if resetIndex: - newIndex = 0 - filename = files[0] if files and len(files) > 0 else None - else: - newIndex = self._getValidIndexFromNewPlaylist(files) - filename = self._getFilenameFromIndexInGivenPlaylist(files, newIndex) - - self._updateUndoPlaylistBuffer(newPlaylist=files, newRoom=self._client.userlist.currentUser.room) - self._playlist = files - - if username is None: - if self._client.isConnectedAndInARoom() and self._client.sharedPlaylistIsEnabled(): - self._client._protocol.setPlaylist(files) - self.changeToPlaylistIndex(newIndex) - self._ui.setPlaylist(self._playlist, filename) - self._ui.showMessage(getMessage("playlist-contents-changed-notification").format(self._client.getUsername())) - else: - self._ui.setPlaylist(self._playlist) - self._ui.showMessage(getMessage("playlist-contents-changed-notification").format(username)) - - def addToPlaylist(self, file): - self.changePlaylist([*self._playlist, file]) - - def deleteAtIndex(self, index): - new_playlist = self._playlist.copy() - if index >= 0 and index < len(new_playlist): - del new_playlist[index] - self.changePlaylist(new_playlist) - else: - raise TypeError("Invalid index") - - - @needsSharedPlaylistsEnabled - def undoPlaylistChange(self): - if self.canUndoPlaylist(self._playlist): - newPlaylist = self._getPreviousPlaylist() - self.changePlaylist(newPlaylist, username=None) - - @needsSharedPlaylistsEnabled - def shuffleRemainingPlaylist(self): - if self._playlist and len(self._playlist) > 0: - shuffledPlaylist = deepcopy(self._playlist) - shufflePoint = self._playlistIndex + 1 - partToKeep = shuffledPlaylist[:shufflePoint] - partToShuffle = shuffledPlaylist[shufflePoint:] - random.shuffle(partToShuffle) - shuffledPlaylist = partToKeep + partToShuffle - self.changePlaylist(shuffledPlaylist, username=None, resetIndex=False) - - @needsSharedPlaylistsEnabled - def shuffleEntirePlaylist(self): - if self._playlist and len(self._playlist) > 0: - shuffledPlaylist = deepcopy(self._playlist) - random.shuffle(shuffledPlaylist) - self.changePlaylist(shuffledPlaylist, username=None, resetIndex=True) - self.switchToNewPlaylistIndex(0, resetPosition=True) - - def canUndoPlaylist(self, currentPlaylist): - return self._previousPlaylist is not None and currentPlaylist != self._previousPlaylist - - def loadCurrentPlaylistIndex(self): - if self._notPlayingCurrentIndex(): - self.switchToNewPlaylistIndex(self._playlistIndex) - - @needsSharedPlaylistsEnabled - def advancePlaylistCheck(self): - position = self._client.getStoredPlayerPosition() - currentLength = self._client.userlist.currentUser.file["duration"] if self._client.userlist.currentUser.file else 0 - if ( - currentLength > constants.PLAYLIST_LOAD_NEXT_FILE_MINIMUM_LENGTH and - abs(position - currentLength) < constants.PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD and - self.notJustChangedPlaylist() - ): - self.loadNextFileInPlaylist() - - def notJustChangedPlaylist(self): - secondsSinceLastChange = time.time() - self._lastPlaylistIndexChange - return secondsSinceLastChange > constants.PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD - - @needsSharedPlaylistsEnabled - def loadNextFileInPlaylist(self): - if self._notPlayingCurrentIndex(): - return - - if len(self._playlist) == 1 and self._client.loopSingleFiles(): - self._lastPlaylistIndexChange = time.time() - self._client.rewindFile() - self._client.setPaused(False) - reactor.callLater(0.5, self._client.setPaused, False,) - - elif self._thereIsNextPlaylistIndex(): - self._client.prepareToAdvancePlaylist() - self.switchToNewPlaylistIndex(self._nextPlaylistIndex(), resetPosition=True) - - def _updateUndoPlaylistBuffer(self, newPlaylist, newRoom): - if self._playlistBufferIsFromOldRoom(newRoom): - self._movePlaylistBufferToNewRoom(newRoom) - elif self._playlistBufferNeedsUpdating(newPlaylist): - self._previousPlaylist = self._playlist - - def _getPreviousPlaylist(self): - return self._previousPlaylist - - def _notPlayingCurrentIndex(self): - if self._playlistIndex is None or self._playlist is None or len(self._playlist) <= self._playlistIndex: - self._ui.showDebugMessage("Not playing current index - Index none or length issue") - return True - currentPlaylistFilename = self._playlist[self._playlistIndex] - if self._client.userlist.currentUser.file and currentPlaylistFilename == self._client.userlist.currentUser.file['name']: - return False - else: - self._ui.showDebugMessage("Not playing current index - Filename mismatch or no file") - return True - - def _thereIsNextPlaylistIndex(self): - if self._playlistIndex is None: - return False - elif len(self._playlist) == 1 and not self._client.loopSingleFiles(): - return False - elif self._playlistIsAtEnd(): - return self._client.isPlaylistLoopingEnabled() - else: - return True - - def _nextPlaylistIndex(self): - if self._playlistIsAtEnd(): - return 0 - else: - return self._playlistIndex+1 - - def _playlistIsAtEnd(self): - return len(self._playlist) <= self._playlistIndex+1 - - def _playlistBufferIsFromOldRoom(self, newRoom): - return self._previousPlaylistRoom != newRoom - - def _movePlaylistBufferToNewRoom(self, currentRoom): - self._previousPlaylist = None - self._previousPlaylistRoom = currentRoom - - def _playlistBufferNeedsUpdating(self, newPlaylist): - return self._previousPlaylist != self._playlist and self._playlist != newPlaylist - - -class FileSwitchManager(object): - def __init__(self, client): - self._client = client - self.mediaFilesCache = {} - self.filenameWatchlist = [] - self.currentDirectory = None - self.mediaDirectories = client.getConfig().get('mediaSearchDirectories') - self.lock = threading.Lock() - self.folderSearchEnabled = True - self.directorySearchError = None - self.newInfo = False - self.currentlyUpdating = False - self.newWatchlist = [] - self.fileSwitchTimer = task.LoopingCall(self.updateInfo) - self.fileSwitchTimer.start(constants.FOLDER_SEARCH_DOUBLE_CHECK_INTERVAL, True) - self.mediaDirectoriesNotFound = [] - - def setClient(self, newClient): - self._client = newClient - - def setCurrentDirectory(self, curDir): - self.currentDirectory = curDir - - def changeMediaDirectories(self, mediaDirs): - from syncplay.ui.ConfigurationGetter import ConfigurationGetter - ConfigurationGetter().setConfigOption("mediaSearchDirectories", mediaDirs) - self._client._config["mediaSearchDirectories"] = mediaDirs - self._client.ui.showMessage(getMessage("media-directory-list-updated-notification")) - self.mediaDirectoriesNotFound = [] - self.folderSearchEnabled = True - self.setMediaDirectories(mediaDirs) - if mediaDirs == "": - self._client.ui.showErrorMessage(getMessage("no-media-directories-error")) - self.mediaFilesCache = {} - self.newInfo = True - self.checkForFileSwitchUpdate() - - def setMediaDirectories(self, mediaDirs): - self.mediaDirectories = mediaDirs - self.updateInfo() - - def checkForFileSwitchUpdate(self): - if self.newInfo: - self.newInfo = False - self.infoUpdated() - if self.directorySearchError: - self._client.ui.showErrorMessage(self.directorySearchError) - self.directorySearchError = None - - def updateInfo(self): - if not self.currentlyUpdating and self.mediaDirectories: - threads.deferToThread(self._updateInfoThread).addCallback(lambda x: self.checkForFileSwitchUpdate()) - - def setFilenameWatchlist(self, unfoundFilenames): - self.filenameWatchlist = unfoundFilenames - - def _updateInfoThread(self): - with self.lock: - try: - self.currentlyUpdating = True - dirsToSearch = self.mediaDirectories - - if not self.folderSearchEnabled: - return - - if dirsToSearch: - # Spin up hard drives to prevent premature timeout - randomFilename = "RandomFile"+str(random.randrange(10000, 99999))+".txt" - for directory in dirsToSearch: - if not os.path.isdir(directory): - self.directorySearchError = getMessage("cannot-find-directory-error").format(directory) - - startTime = time.time() - if os.path.isfile(os.path.join(directory, randomFilename)): - randomFilename = "RandomFile"+str(random.randrange(10000, 99999))+".txt" - print("Found random file (?)") - if time.time() - startTime > constants.FOLDER_SEARCH_FIRST_FILE_TIMEOUT: - self.folderSearchEnabled = False - self.directorySearchError = getMessage("folder-search-first-file-timeout-error").format(directory) - return - - # Actual directory search - newMediaFilesCache = {} - startTime = time.time() - fileCount = 0 - lastWarningTime = None - for directory in dirsToSearch: - for root, dirs, files in os.walk(directory): - fileCount += 1 - newMediaFilesCache[root] = files - timeTakenSoFar = time.time() - startTime - if timeTakenSoFar > constants.FOLDER_SEARCH_TIMEOUT: - reactor.callLater(0.1, self._client.ui.showErrorMessage, getMessage("folder-search-timeout-error").format(directory, fileCount),False) - self.folderSearchEnabled = False - return - if timeTakenSoFar > constants.FOLDER_SEARCH_WARNING_THRESHOLD: - if not lastWarningTime or timeTakenSoFar - lastWarningTime >= 1: - reactor.callLater(0.1, self._client.ui.showErrorMessage, getMessage("folder-search-timeout-warning").format(int(timeTakenSoFar), fileCount, directory),False) - lastWarningTime = timeTakenSoFar - - if self.mediaFilesCache != newMediaFilesCache: - self.mediaFilesCache = newMediaFilesCache - self.newInfo = True - except Exception as e: - self._client.ui.showDebugMessage(str(e)) - finally: - self.currentlyUpdating = False - - def infoUpdated(self): - self._client.fileSwitchFoundFiles() - - def findFilepath(self, filename, highPriority=False): - if filename is None: - return - - if self._client.userlist.currentUser.file and utils.sameFilename(filename, self._client.userlist.currentUser.file['name']): - return self._client.userlist.currentUser.file['path'] - - if self.mediaFilesCache is not None: - for directory in self.mediaFilesCache: - files = self.mediaFilesCache[directory] - if len(files) > 0 and filename in files: - filepath = os.path.join(directory, filename) - if os.path.isfile(filepath): - return filepath - - if self.folderSearchEnabled and self.mediaDirectories is not None: - directoryList = self.mediaDirectories - for directory in directoryList: - filepath = os.path.join(directory, filename) - if os.path.isfile(filepath): - return filepath - - def areWatchedFilenamesInCache(self): - if self.filenameWatchlist is not None: - for filename in self.filenameWatchlist: - if self.isFilenameInCache(filename): - return True - - def isFilenameInCache(self, filename): - if filename is not None and self.mediaFilesCache is not None: - for directory in self.mediaFilesCache: - files = self.mediaFilesCache[directory] - if filename in files: - return True - - def getDirectoryOfFilenameInCache(self, filename): - if filename is not None and self.mediaFilesCache is not None: - for directory in self.mediaFilesCache: - files = self.mediaFilesCache[directory] - if filename in files: - return directory - return None - - def isDirectoryInList(self, directoryToFind, folderList): - if directoryToFind and folderList: - normedDirectoryToFind = os.path.normcase(os.path.normpath(directoryToFind)) - for listedFolder in folderList: - normedListedFolder = os.path.normcase(os.path.normpath(listedFolder)) - if normedDirectoryToFind.startswith(normedListedFolder): - return True - return False - - def notifyUserIfFileNotInMediaDirectory(self, filenameToFind, path): - directoryToFind = os.path.dirname(path) - if directoryToFind in self.mediaDirectoriesNotFound: - return - if self.mediaDirectories is not None and self.mediaFilesCache is not None: - if directoryToFind in self.mediaFilesCache: - return - for directory in self.mediaFilesCache: - files = self.mediaFilesCache[directory] - if filenameToFind in files: - return - if directoryToFind in self.mediaFilesCache: - return - if self.isDirectoryInList(directoryToFind, self.mediaDirectories): - return - directoryToFind = str(directoryToFind) - self._client.ui.showErrorMessage(getMessage("added-file-not-in-media-directory-error").format(directoryToFind)) - self.mediaDirectoriesNotFound.append(directoryToFind) +(full file content here) \ No newline at end of file