diff --git a/syncplay/__init__.py b/syncplay/__init__.py index cb78f9c..c2a8ec7 100644 --- a/syncplay/__init__.py +++ b/syncplay/__init__.py @@ -1,3 +1,3 @@ -version = '1.2.7' -milestone = 'Biscuit' +version = '1.2.8' +milestone = 'Hammer' projectURL = 'http://syncplay.pl/' diff --git a/syncplay/client.py b/syncplay/client.py index 25ee5b7..89b8751 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -1,4 +1,3 @@ -#coding:utf8 import hashlib import os.path import time @@ -9,8 +8,9 @@ from syncplay.protocols import SyncClientProtocol from syncplay import utils, constants from syncplay.messages import getMessage import threading -from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE,\ +from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \ PRIVACY_HIDDENFILENAME, FILENAME_STRIP_REGEX +import collections # libMal = None '''try: @@ -19,13 +19,14 @@ except ImportError: libMal = None ''' # + class SyncClientFactory(ClientFactory): - def __init__(self, client, retry = constants.RECONNECT_RETRIES): + def __init__(self, client, retry=constants.RECONNECT_RETRIES): self._client = client self.retry = retry self._timesTried = 0 self.reconnecting = False - + def buildProtocol(self, addr): self._timesTried = 0 return SyncClientProtocol(self._client) @@ -42,7 +43,7 @@ class SyncClientFactory(ClientFactory): self._timesTried += 1 self._client.ui.showMessage(getMessage("en", "reconnection-attempt-notification")) self.reconnecting = True - reactor.callLater(0.1*(2**self._timesTried), connector.connect) + reactor.callLater(0.1 * (2 ** self._timesTried), connector.connect) else: message = getMessage("en", "disconnection-notification") self._client.ui.showErrorMessage(message) @@ -52,13 +53,13 @@ class SyncClientFactory(ClientFactory): self._client.ui.showErrorMessage(getMessage("en", "connection-failed-notification")) else: self.clientConnectionLost(connector, reason) - + def resetRetrying(self): self._timesTried = 0 def stopRetrying(self): self._timesTried = self.retry - + class SyncplayClient(object): def __init__(self, playerClass, ui, config): self.protocolFactory = SyncClientFactory(self) @@ -68,7 +69,7 @@ class SyncplayClient(object): self._player = None self.givenmalprivacywarning = False if(config['room'] == None or config['room'] == ''): - config['room'] = config['name'] # ticket #58 + config['room'] = config['name'] # ticket #58 self.defaultRoom = config['room'] self.playerPositionBeforeLastSeek = 0.0 self.setUsername(config['name']) @@ -82,39 +83,39 @@ class SyncplayClient(object): 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._warnings = self._WarningManager(self._player, self.userlist, self.ui) self._malUpdater = MalUpdater(config["malUsername"], config["malPassword"], self.ui) - + 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 self.scheduleAskPlayer() - + 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 @@ -135,7 +136,7 @@ class SyncplayClient(object): _playerDiff = abs(self.getPlayerPosition() - position) _globalDiff = abs(self.getGlobalPosition() - position) seeked = _playerDiff > constants.SEEK_THRESHOLD and _globalDiff > constants.SEEK_THRESHOLD - return pauseChange, seeked + return pauseChange, seeked def updatePlayerStatus(self, paused, position): position -= self.getUserOffset() @@ -147,7 +148,7 @@ class SyncplayClient(object): if((pauseChange or seeked) and self._protocol): if(seeked): self.playerPositionBeforeLastSeek = self.getGlobalPosition() - self._protocol.sendState(self.getPlayerPosition(), self.getPlayerPaused(), seeked, None, True) + self._protocol.sendState(self.getPlayerPosition(), self.getPlayerPaused(), seeked, None, True) def getLocalState(self): paused = self.getPlayerPaused() @@ -157,7 +158,7 @@ class SyncplayClient(object): return position, paused, _, pauseChange else: return None, None, None, None - + def _initPlayerState(self, position, paused): if(self.userlist.currentUser.file): self.setPosition(position) @@ -246,10 +247,10 @@ class SyncplayClient(object): 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()) @@ -273,7 +274,7 @@ class SyncplayClient(object): position = self._playerPosition if(not self._playerPaused): diff = time.time() - self._lastPlayerUpdate - position += diff + position += diff return position def getPlayerPaused(self): @@ -282,8 +283,8 @@ class SyncplayClient(object): return self.getGlobalPaused() else: return True - return self._playerPaused - + return self._playerPaused + def getGlobalPosition(self): if not self._lastGlobalUpdate: return 0.0 @@ -291,18 +292,18 @@ class SyncplayClient(object): if not self._globalPaused: position += time.time() - self._lastGlobalUpdate return position - + def getGlobalPaused(self): if(not self._lastGlobalUpdate): return True - return self._globalPaused - + return self._globalPaused + def updateFile(self, filename, duration, path): if not path: return try: size = os.path.getsize(path) - except OSError: #file not accessible (stream?) + except OSError: # file not accessible (stream?) size = 0 rawfilename = filename filename, size = self.__executePrivacySettings(filename, size) @@ -313,11 +314,11 @@ class SyncplayClient(object): message = getMessage("en", "mal-noprivacy-notification") self.ui.showErrorMessage(message) self.givenmalprivacywarning = True - - + + def __executePrivacySettings(self, filename, size): if (self._config['filenamePrivacyMode'] == PRIVACY_SENDHASHED_MODE): - filename = utils.hashFilename(filename) + filename = utils.hashFilename(filename) elif (self._config['filenamePrivacyMode'] == PRIVACY_DONTSEND_MODE): filename = PRIVACY_HIDDENFILENAME if (self._config['filesizePrivacyMode'] == PRIVACY_SENDHASHED_MODE): @@ -333,44 +334,44 @@ class SyncplayClient(object): def setUsername(self, username): self.userlist.currentUser.username = username - + def getUsername(self): return self.userlist.currentUser.username - + def setRoom(self, roomName): self.userlist.currentUser.room = roomName - + def sendRoom(self): room = self.userlist.currentUser.room if(self._protocol and self._protocol.logged and room): self._protocol.sendRoomSetting(room) self.getUserList() - + def getRoom(self): return self.userlist.currentUser.room - + def getUserList(self): if(self._protocol and self._protocol.logged): self._protocol.sendList() - + def showUserList(self): self.userlist.showUserList() - + def getPassword(self): return self._serverPassword - + def setPosition(self, position): - position += self.getUserOffset() + 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._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): self._player.setPaused(paused) - + def start(self, host, port): if self._running: return @@ -382,7 +383,7 @@ class SyncplayClient(object): reactor.connectTCP(host, port, self.protocolFactory) reactor.run() - def stop(self, promptForAction = False): + def stop(self, promptForAction=False): if not self._running: return self._running = False @@ -415,7 +416,7 @@ class SyncplayClient(object): def checkWarnings(self): self._checkIfYouReAloneInTheRoom() self._checkRoomForSameFiles() - + def _checkRoomForSameFiles(self): if (not self._userlist.areAllFilesInRoomSame()): self._ui.showMessage(getMessage("en", "room-files-not-same"), True) @@ -423,7 +424,7 @@ class SyncplayClient(object): self._warnings["room-files-not-same"]['timer'].start(constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, True) elif(self._warnings["room-files-not-same"]['timer'].running): self._warnings["room-files-not-same"]['timer'].stop() - + def _checkIfYouReAloneInTheRoom(self): if (self._userlist.areYouAloneInRoom()): self._ui.showMessage(getMessage("en", "alone-in-the-room"), True) @@ -431,24 +432,24 @@ class SyncplayClient(object): 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 __displayMessageOnOSD(self, warningName): if (constants.OSD_WARNING_MESSAGE_DURATION > self._warnings[warningName]["displayedFor"]): - self._ui.showOSDMessage(getMessage("en", warningName ), constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL) + self._ui.showOSDMessage(getMessage("en", warningName), constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL) self._warnings[warningName]["displayedFor"] += constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL else: self._warnings[warningName]["displayedFor"] = 0 self._warnings[warningName]["timer"].stop() - + class SyncplayUser(object): - def __init__(self, username = None, room = None, file_ = None, position = 0): + def __init__(self, username=None, room=None, file_=None, position=0): self.username = username self.room = room self.file = file_ self.lastPosition = position - + def setFile(self, filename, duration, size): file_ = { "name": filename, @@ -456,19 +457,24 @@ class SyncplayUser(object): "size":size } self.file = file_ - - def isFileSame(self, 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): - return self.username < other.username - - + return self.username.lower() < other.username.lower() + + 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) + class MalUpdater(object): def __init__(self, username, password, ui): self._filePlayingFor = 0.0 @@ -499,7 +505,7 @@ class MalUpdater(object): self._filename = filename self._filePlayingFor = 0.0 self._lastHookUpdate = None - + def _updateMal(self): try: self._fileDuration = 0 # Disable playingHook @@ -509,13 +515,13 @@ class MalUpdater(object): if(len(results) > 0): result = results[0] message = "Updating MAL with: \"{}\", episode: {}".format(result.mainTitle, result.episodeBeingWatched) - reactor.callFromThread(self._ui.showMessage,(message),) + reactor.callFromThread(self._ui.showMessage, (message),) options = {"tags": ["syncplay"]} manager.updateEntryOnMal(result, options) self._filename = "" # Make sure no updates will be performed until switch except: - reactor.callFromThread(self._ui.showMessage, ("MAL Update failure"),) - + reactor.callFromThread(self._ui.showMessage, ("MAL Update failure"),) + class SyncplayUserlist(object): def __init__(self, ui, client): self.currentUser = SyncplayUser() @@ -542,7 +548,7 @@ class SyncplayUserlist(object): differentSize = not utils.sameFilesize(self.currentUser.file['size'], file_['size']) differentDuration = not utils.sameFileduration(self.currentUser.file['duration'], file_['duration']) if(differentName): - differences.append("filename") + differences.append("filename") if(differentSize): differences.append("size") if(differentDuration): @@ -550,7 +556,7 @@ class SyncplayUserlist(object): message = getMessage("en", "file-differences-notification") + ", ".join(differences) self.ui.showMessage(message) - def addUser(self, username, room, file_, position = 0, noMessage = False): + def addUser(self, username, room, file_, position=0, noMessage=False): if(username == self.currentUser.username): self.currentUser.lastPosition = position return @@ -559,14 +565,14 @@ class SyncplayUserlist(object): if(not noMessage): self.__showUserChangeMessage(username, room, file_) self.userListChange() - + def removeUser(self, username): if(self._users.has_key(username)): self._users.pop(username) message = getMessage("en", "left-notification").format(username) self.ui.showMessage(message) self.userListChange() - + def __displayModUserMessage(self, username, room, file_, user): if (file_ and not user.isFileSame(file_)): self.__showUserChangeMessage(username, room, file_) @@ -585,13 +591,13 @@ class SyncplayUserlist(object): else: self.addUser(username, room, file_) self.userListChange() - + def areAllFilesInRoomSame(self): for user in self._users.itervalues(): if(user.room == self.currentUser.room and user.file and not self.currentUser.isFileSame(user.file)): return False return True - + def areYouAloneInRoom(self): for user in self._users.itervalues(): if(user.room == self.currentUser.room): @@ -607,13 +613,13 @@ class SyncplayUserlist(object): def userListChange(self): self._roomUsersChanged = True self.ui.userListChange() - + def roomStateConfirmed(self): self._roomUsersChanged = False - + def hasRoomStateChanged(self): return self._roomUsersChanged - + def showUserList(self): rooms = {} for user in self._users.itervalues(): @@ -623,41 +629,47 @@ class SyncplayUserlist(object): if(self.currentUser.room not in rooms): rooms[self.currentUser.room] = [] rooms[self.currentUser.room].append(self.currentUser) - + rooms = self.sortList(rooms) self.ui.showUserList(self.currentUser, rooms) - + def clearList(self): self._users = {} - + + def sortList(self, rooms): + for room in rooms: + rooms[room] = sorted(rooms[room]) + rooms = collections.OrderedDict(sorted(rooms.items(), key=lambda s: s[0].lower())) + return rooms + class UiManager(object): def __init__(self, client, ui): self._client = client self.__ui = ui - - def showMessage(self, message, noPlayer = False, noTimestamp = False): + + def showMessage(self, message, noPlayer=False, noTimestamp=False): if(not noPlayer): self.showOSDMessage(message) self.__ui.showMessage(message, noTimestamp) - + def showUserList(self, currentUser, rooms): self.__ui.showUserList(currentUser, rooms) - - def showOSDMessage(self, message, duration = constants.OSD_DURATION): + + def showOSDMessage(self, message, duration=constants.OSD_DURATION): if(self._client._player): self._client._player.displayMessage(message, duration * 1000) - - def showErrorMessage(self, message, criticalerror = False): + + def showErrorMessage(self, message, criticalerror=False): 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 drop(self): self.__ui.drop() - + diff --git a/syncplay/constants.py b/syncplay/constants.py index 2cacb9c..c684781 100644 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -8,6 +8,8 @@ UI_TIME_FORMAT = "[%X] " CONFIG_NAMES = [".syncplay", "syncplay.ini"] #Syncplay searches first to last DEFAULT_CONFIG_NAME_WINDOWS = "syncplay.ini" DEFAULT_CONFIG_NAME_LINUX = ".syncplay" +RECENT_CLIENT_THRESHOLD = "1.2.7" #This and higher considered 'recent' clients (no warnings) +WARN_OLD_CLIENTS = True #Use MOTD to inform old clients to upgrade #Changing these might be ok REWIND_THRESHOLD = 4 diff --git a/syncplay/messages.py b/syncplay/messages.py index 7bb4e22..7fb0058 100644 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -163,6 +163,9 @@ en = { "help-tooltip" : "Opens the Syncplay.pl user guide.", + # Server messages to client + "new-syncplay-available-motd-message" : " You are using Syncplay {} but a newer version is available from http://syncplay.pl ", #ClientVersion + # Server notifications "welcome-server-notification" : "Welcome to Syncplay server, ver. {0}", #version "client-connected-room-server-notification" : "{0}({2}) connected to room '{1}'", #username, host, room diff --git a/syncplay/protocols.py b/syncplay/protocols.py index c8efaf5..f83cfa0 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -291,9 +291,9 @@ class SyncServerProtocol(JSONCommandProtocol): return self._factory.addWatcher(self, username, roomName, roomPassword) self._logged = True - self.sendHello() + self.sendHello(version) - def sendHello(self): + def sendHello(self, clientVersion): hello = {} username = self._factory.watcherGetUsername(self) hello["username"] = username @@ -301,7 +301,7 @@ class SyncServerProtocol(JSONCommandProtocol): room = self._factory.watcherGetRoom(self) if(room): hello["room"] = {"name": room} hello["version"] = syncplay.version - hello["motd"] = self._factory.getMotd(userIp, username, room) + hello["motd"] = self._factory.getMotd(userIp, username, room, clientVersion) self.sendMessage({"Hello": hello}) @requireLogged diff --git a/syncplay/server.py b/syncplay/server.py index 8df7ba4..a136281 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -136,15 +136,24 @@ class SyncFactory(Factory): position += timePassedSinceSet return paused, position - def getMotd(self, userIp, username, room): + def getMotd(self, userIp, username, room, clientVersion): + oldClient = False + if constants.WARN_OLD_CLIENTS: + if int(clientVersion.replace(".","")) < int(constants.RECENT_CLIENT_THRESHOLD.replace(".","")): + oldClient = True if(self._motdFilePath and os.path.isfile(self._motdFilePath)): tmpl = codecs.open(self._motdFilePath, "r", "utf-8-sig").read() args = dict(version=syncplay.version, userIp=userIp, username=username, room=room) try: motd = Template(tmpl).substitute(args) + if oldClient: + motdwarning = getMessage("en","new-syncplay-available-motd-message").format(clientVersion) + motd = "{}\n{}".format(motdwarning, motd) return motd if len(motd) < constants.SERVER_MAX_TEMPLATE_LENGTH else getMessage("en", "server-messed-up-motd-too-long").format(constants.SERVER_MAX_TEMPLATE_LENGTH, len(motd)) except ValueError: return getMessage("en", "server-messed-up-motd-unescaped-placeholders") + elif oldClient: + return getMessage("en", "new-syncplay-available-motd-message").format(clientVersion) else: return "" diff --git a/syncplay/ui/__init__.py b/syncplay/ui/__init__.py index c88bb96..9156a56 100644 --- a/syncplay/ui/__init__.py +++ b/syncplay/ui/__init__.py @@ -1,4 +1,7 @@ -from syncplay.ui.gui import MainWindow as GraphicalUI +try: + from syncplay.ui.gui import MainWindow as GraphicalUI +except ImportError: + pass from syncplay.ui.consoleUI import ConsoleUI def getUi(graphical=True): diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index e04918f..7aea0ee 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -211,7 +211,7 @@ class MainWindow(QtGui.QMainWindow): t = self._syncplayClient.getUserOffset() + sign * t self._syncplayClient.setUserOffset(t) else: - self.showMessage("Invalid offset value", True) + self.showErrorMessage("Invalid offset value") def openUserGuide(self): if sys.platform.startswith('linux'):