diff --git a/buildPy2exe.py b/buildPy2exe.py index 6155944..5985bfc 100644 --- a/buildPy2exe.py +++ b/buildPy2exe.py @@ -616,7 +616,9 @@ guiIcons = ['resources/accept.png', 'resources/arrow_undo.png', 'resources/clock 'resources/timeline_marker.png','resources/control_play_blue.png', 'resources/mpc-hc.png','resources/mpc-hc64.png','resources/mplayer.png', 'resources/mpv.png','resources/vlc.png', 'resources/house.png', 'resources/film_link.png', - 'resources/eye.png', 'resources/comments.png', 'resources/cog_delete.png', 'resources/bullet_black.png' + 'resources/eye.png', 'resources/comments.png', 'resources/cog_delete.png', 'resources/chevrons_right.png', + 'resources/user_key.png', 'resources/lock.png', 'resources/key_go.png', 'resources/page_white_key.png', + 'resources/tick.png', 'resources/lock_open.png' ] resources = ["resources/icon.ico", "resources/syncplay.png"] resources.extend(guiIcons) diff --git a/resources/bullet_black.png b/resources/bullet_black.png deleted file mode 100644 index 5761970..0000000 Binary files a/resources/bullet_black.png and /dev/null differ diff --git a/resources/chevrons_right.png b/resources/chevrons_right.png new file mode 100644 index 0000000..356a05a Binary files /dev/null and b/resources/chevrons_right.png differ diff --git a/resources/key_go.png b/resources/key_go.png new file mode 100644 index 0000000..30b0dc3 Binary files /dev/null and b/resources/key_go.png differ diff --git a/resources/lock.png b/resources/lock.png new file mode 100644 index 0000000..2d06336 Binary files /dev/null and b/resources/lock.png differ diff --git a/resources/lock_open.png b/resources/lock_open.png new file mode 100644 index 0000000..3493dcf Binary files /dev/null and b/resources/lock_open.png differ diff --git a/resources/page_white_key.png b/resources/page_white_key.png new file mode 100644 index 0000000..d616484 Binary files /dev/null and b/resources/page_white_key.png differ diff --git a/resources/tick.png b/resources/tick.png new file mode 100644 index 0000000..a9925a0 Binary files /dev/null and b/resources/tick.png differ diff --git a/resources/user_key.png b/resources/user_key.png new file mode 100644 index 0000000..c60e6f9 Binary files /dev/null and b/resources/user_key.png differ diff --git a/syncplay/__init__.py b/syncplay/__init__.py index ef963d5..a1fa3eb 100644 --- a/syncplay/__init__.py +++ b/syncplay/__init__.py @@ -1,3 +1,3 @@ -version = '1.2.9' -milestone = 'Pineapple, Pulverize and Destroy!' +version = '1.3.0' +milestone = 'Akki' projectURL = 'http://syncplay.pl/' diff --git a/syncplay/client.py b/syncplay/client.py index f7618dd..1ee7968 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -4,12 +4,12 @@ import time import re from twisted.internet.protocol import ClientFactory from twisted.internet import reactor, task +from functools import wraps from syncplay.protocols import SyncClientProtocol from syncplay import utils, constants -from syncplay.messages import getMessage, getMissingStrings -import threading +from syncplay.messages import getMissingStrings, getMessage from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \ - PRIVACY_HIDDENFILENAME, FILENAME_STRIP_REGEX + PRIVACY_HIDDENFILENAME import collections class SyncClientFactory(ClientFactory): @@ -62,14 +62,20 @@ class SyncplayClient(object): constants.SHOW_SAME_ROOM_OSD = config['showSameRoomOSD'] constants.SHOW_DURATION_NOTIFICATION = config['showDurationNotification'] constants.DEBUG_MODE = config['debug'] + + self.controlpasswords = {} + self.lastControlPasswordAttempt = None + self.serverVersion = "0.0.0" + self.lastLeftTime = 0 self.lastLeftUser = u"" 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'] == None or config['room'] == '': + if config['room'] is None or config['room'] == '': config['room'] = config['name'] # ticket #58 self.defaultRoom = config['room'] self.playerPositionBeforeLastSeek = 0.0 @@ -100,7 +106,8 @@ class SyncplayClient(object): self._warnings = self._WarningManager(self._player, self.userlist, self.ui) if constants.LIST_RELATIVE_CONFIGS and self._config.has_key('loadedRelativePaths') and self._config['loadedRelativePaths']: - self.ui.showMessage(getMessage("relative-config-notification").format("; ".join(self._config['loadedRelativePaths'])), noPlayer=True, noTimestamp=True) + 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() @@ -229,7 +236,7 @@ class SyncplayClient(object): def _changePlayerStateAccordingToGlobalState(self, position, paused, doSeek, setBy): madeChangeOnPlayer = False - pauseChanged = paused != self.getGlobalPaused() + pauseChanged = paused != self.getGlobalPaused() or paused != self.getPlayerPaused() diff = self.getPlayerPosition() - position if self._lastGlobalUpdate is None: madeChangeOnPlayer = self._initPlayerState(position, paused) @@ -240,7 +247,7 @@ class SyncplayClient(object): 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._player.speedSupported and not doSeek and not paused and not self._config['slowOnDesync'] == False): + 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) @@ -323,7 +330,6 @@ class SyncplayClient(object): size = os.path.getsize(path) except OSError: # file not accessible (stream?) size = 0 - rawfilename = filename filename, size = self.__executePrivacySettings(filename, size) self.userlist.currentUser.setFile(filename, duration, size) self.sendFile() @@ -339,6 +345,9 @@ class SyncplayClient(object): size = 0 return filename, size + def setServerVersion(self, version): + self.serverVersion = version + def sendFile(self): file_ = self.userlist.currentUser.file if self._protocol and self._protocol.logged and file_: @@ -364,6 +373,17 @@ class SyncplayClient(object): if self._protocol and self._protocol.logged and room: self._protocol.sendRoomSetting(room) self.getUserList() + self.reIdentifyAsController() + + def reIdentifyAsController(self): + room = self.userlist.currentUser.room + if utils.RoomPasswordProvider.isControlledRoom(room): + storedRoomPassword = self.getControlledRoomPassword(room) + if storedRoomPassword: + self.identifyAsController(storedRoomPassword) + + def connected(self): + self.reIdentifyAsController() def getRoom(self): return self.userlist.currentUser.room @@ -420,6 +440,64 @@ class SyncplayClient(object): if promptForAction: self.ui.promptFor(getMessage("enter-to-exit-prompt")) + def requireMinServerVersion(minVersion): + def requireMinVersionDecorator(f): + @wraps(f) + def wrapper(self, *args, **kwds): + if not utils.meetsMinVersion(self.serverVersion,minVersion): + self.ui.showErrorMessage(u"This feature is not supported by the server. The feature requires a server running Syncplay {}+, but the server is running Syncplay {}.".format(minVersion, self.serverVersion)) + return + return f(self, *args, **kwds) + return wrapper + return requireMinVersionDecorator + + @requireMinServerVersion(constants.CONTROLLED_ROOMS_MIN_VERSION) + def createControlledRoom(self, roomName): + controlPassword = utils.RandomStringGenerator.generate_room_password() + self.ui.showMessage(u"Attempting to create controlled room '{}' with password '{}'...".format(roomName, controlPassword)) + self.lastControlPasswordAttempt = controlPassword + self._protocol.requestControlledRoom(roomName, controlPassword) + + def controlledRoomCreated(self, roomName, controlPassword): + self.ui.showMessage(u"Created controlled room '{}' with password '{}'. Please save this information for future reference!".format(roomName, controlPassword)) + self.setRoom(roomName) + 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 "" + + @requireMinServerVersion(constants.CONTROLLED_ROOMS_MIN_VERSION) + 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): + 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 + + def getControlledRoomPassword(self, room): + if self.controlpasswords.has_key(room): + return self.controlpasswords[room] + class _WarningManager(object): def __init__(self, player, userlist, ui): self._player = player @@ -472,6 +550,7 @@ class SyncplayUser(object): self.username = username self.room = room self.file = file_ + self._controller = False def setFile(self, filename, duration, size): file_ = { @@ -490,7 +569,10 @@ class SyncplayUser(object): return sameName and sameSize and sameDuration def __lt__(self, other): - return self.username.lower() < other.username.lower() + 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: @@ -498,6 +580,12 @@ class SyncplayUser(object): else: return "{}".format(self.username) + def setControllerStatus(self, isController): + self._controller = isController + + def isController(self): + return self._controller + class SyncplayUserlist(object): def __init__(self, ui, client): self.currentUser = SyncplayUser() @@ -544,10 +632,14 @@ class SyncplayUserlist(object): message = getMessage("file-differences-notification") + ", ".join(differences) self.ui.showMessage(message, not constants.SHOW_OSD_WARNINGS) - def addUser(self, username, room, file_, noMessage=False): + def addUser(self, username, room, file_, noMessage=False, isController=None): if username == self.currentUser.username: + if isController is not None: + self.currentUser.setControllerStatus(isController) return user = SyncplayUser(username, room, file_) + if isController is not None: + user.setControllerStatus(isController) self._users[username] = user if not noMessage: self.__showUserChangeMessage(username, room, file_) @@ -578,6 +670,8 @@ class SyncplayUserlist(object): if self._users.has_key(username): 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_: @@ -588,6 +682,13 @@ class SyncplayUserlist(object): self.addUser(username, room, file_) self.userListChange() + def setUserAsController(self, username): + if self.currentUser.username == username: + self.currentUser.setControllerStatus(True) + elif self._users.has_key(username): + user = self._users[username] + user.setControllerStatus(True) + 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): @@ -658,6 +759,9 @@ class UiManager(object): if constants.SHOW_OSD and self._client._player: self._client._player.displayMessage(message, duration * 1000) + 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 @@ -672,5 +776,10 @@ class UiManager(object): def markEndOfUserlist(self): self.__ui.markEndOfUserlist() + def updateRoomName(self, room=""): + self.__ui.updateRoomName(room) + def drop(self): self.__ui.drop() + + diff --git a/syncplay/constants.py b/syncplay/constants.py index 59a5adb..e55afd3 100644 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -42,14 +42,19 @@ MERGE_PLAYPAUSE_BUTTONS = False SYNC_ON_PAUSE = True # Client seek to global position - subtitles may disappear on some media players #Usually there's no need to adjust these FILENAME_STRIP_REGEX = u"[-~_\.\[\](): ]" +CONTROL_PASSWORD_STRIP_REGEX = u"[^a-zA-Z0-9\-]" +ROOM_NAME_STRIP_REGEX = u"^(\+)(?P.*)(:)(\w{12})$" COMMANDS_UNDO = ["u", "undo", "revert"] COMMANDS_LIST = ["l", "list", "users"] COMMANDS_PAUSE = ["p", "play", "pause"] COMMANDS_ROOM = ["r", "room"] COMMANDS_HELP = ['help', 'h', '?', '/?', r'\?'] +COMMANDS_CREATE = ['c','create'] +COMMANDS_AUTH = ['a','auth'] MPC_MIN_VER = "1.6.4" VLC_MIN_VERSION = "2.0.0" VLC_INTERFACE_MIN_VERSION = "0.2.1" +CONTROLLED_ROOMS_MIN_VERSION = "1.3.0" MPC_PATHS = [ r"C:\Program Files (x86)\MPC-HC\mpc-hc.exe", r"C:\Program Files\MPC-HC\mpc-hc.exe", @@ -99,8 +104,8 @@ VLC_MAX_PORT = 55000 #These are not changes you're looking for STYLE_TABLIST = "QListWidget::item { color: black; border-style: solid; border-width: 1px; border-radius: 2px; } QListWidget::item:selected { background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(242, 248, 255, 255), stop:1 rgba(208, 229, 255, 255)); border-color: #84ACDD; } QListWidget::item:!selected { border-color: transparent; } QListWidget::item:!selected:hover { background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(248, 248, 248, 255), stop:1 rgba(229, 229, 229, 255)); border-color: silver; }" -STYLE_SUBCHECKBOX = "QCheckBox, QLabel {{ margin-left: 8px; padding-left: 18px; background:url('{}') left no-repeat }}" #Graphic path -STYLE_SUBLABEL = "QCheckBox, QLabel {{ margin-left: 8px; padding-left: 14px; background:url('{}') left no-repeat }}" #Graphic path +STYLE_SUBCHECKBOX = "QCheckBox, QLabel {{ margin-left: 6px; padding-left: 21px; background:url('{}') left no-repeat }}" #Graphic path +STYLE_SUBLABEL = "QCheckBox, QLabel {{ margin-left: 6px; padding-left: 16px; background:url('{}') left no-repeat }}" #Graphic path STYLE_ERRORLABEL = "QLabel { color : black; border-style: outset; border-width: 2px; border-radius: 7px; border-color: red; padding: 2px; background: #FFAAAA; }" STYLE_SUCCESSLABEL = "QLabel { color : black; border-style: outset; border-width: 2px; border-radius: 7px; border-color: green; padding: 2px; background: #AAFFAA; }" STYLE_NOTIFICATIONBOX = "Username { color: #367AA9; font-weight:bold; }" @@ -108,6 +113,9 @@ STYLE_USERNAME = "color: #367AA9; font-weight:bold;" STYLE_ERRORNOTIFICATION = "color: red;" STYLE_DIFFERENTITEM_COLOR = 'red' STYLE_NOFILEITEM_COLOR = 'blue' +STYLE_NOTCONTROLLER_COLOR = 'grey' + +USERLIST_GUI_USERNAME_OFFSET = 21 # Pixels MPLAYER_SLAVE_ARGS = ['-slave', '--hr-seek=always', '-nomsgcolor', '-msglevel', 'all=1:global=4:cplayer=4', '-af', 'scaletempo'] # --quiet works with both mpv 0.2 and 0.3 @@ -135,3 +143,5 @@ ERROR_MESSAGE_MARKER = "*" LOAD_SAVE_MANUALLY_MARKER = "!" CONFIG_NAME_MARKER = ":" CONFIG_VALUE_MARKER = "=" +USERITEM_CONTROLLER_ROLE = 0 +USERITEM_READY_ROLE = 1 diff --git a/syncplay/messages.py b/syncplay/messages.py index c51f40b..6bf191a 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -31,8 +31,12 @@ en = { "playing-notification" : "<{}> is playing '{}' ({})", # User, file, duration "playing-notification/room-addendum" : " in room: '{}'", # Room + "identifying-as-controller-notification" : u"Identifying as room controller with password '{}'...", + "failed-to-identify-as-controller-notification" : u"<{}> failed to identify as a room controller.", + "authenticated-as-controller-notification" : u"<{}> authenticated as a room controller", + "file-different-notification" : "File you are playing appears to be different from <{}>'s", # User - "file-differences-notification" : "Your file differs in the following way(s): ", + "file-differences-notification" : "Your file differs in the following way(s): ", # controlPassword "room-files-not-same" : "Not all files played in the room are the same", "alone-in-the-room": "You're alone in the room", @@ -53,6 +57,8 @@ en = { "commandlist-notification/pause" : "\tp - toggle pause", "commandlist-notification/seek" : "\t[s][+-]time - seek to the given value of time, if + or - is not specified it's absolute time in seconds or min:sec", "commandlist-notification/help" : "\th - this help", + "commandlist-notification/create" : "\tc [name] - create controlled room using name of current room", + "commandlist-notification/auth" : "\ta [password] - authenticate as room controller with controller password", "syncplay-version-notification" : "Syncplay version: {}", # syncplay.version "more-info-notification" : "More info available at: {}", # projectURL @@ -175,7 +181,9 @@ en = { "pause-guibuttonlabel" : "Pause", "roomuser-heading-label" : "Room / User", - "fileplayed-heading-label" : "File being played", + "size-heading-label" : "Size", + "duration-heading-label" : "Length", + "filename-heading-label" : "Filename", "notifications-heading-label" : "Notifications", "userlist-heading-label" : "List of who is playing what", "othercommands-heading-label" : "Other commands", @@ -189,12 +197,24 @@ en = { "exit-menu-label" : "E&xit", "advanced-menu-label" : "&Advanced", "setoffset-menu-label" : "Set &offset", + "createcontrolledroom-menu-label" : "&Create controlled room", + "identifyascontroller-menu-label" : "&Identify as room controller", + "help-menu-label" : "&Help", "userguide-menu-label" : "Open user &guide", "setoffset-msgbox-label" : "Set offset", "offsetinfo-msgbox-label" : "Offset (see http://syncplay.pl/guide/ for usage instructions):", + + "createcontrolledroom-msgbox-label" : "Create controlled room", + "controlledroominfo-msgbox-label" : "Enter name of controlled room\r\n(see http://syncplay.pl/guide/ for usage instructions):", + + "identifyascontroller-msgbox-label" : "Identify as Room Controller", + "identifyinfo-msgbox-label" : "Enter controller password for this room\r\n(see http://syncplay.pl/guide/ for usage instructions):", + + "megabyte-suffix" : " MB", + # Tooltips "host-tooltip" : "Hostname or IP to connect to, optionally including port (e.g. syncplay.pl:8999). Only synchronised with people on same server/port.", @@ -253,6 +273,7 @@ en = { "welcome-server-notification" : "Welcome to Syncplay server, ver. {0}", # version "client-connected-room-server-notification" : "{0}({2}) connected to room '{1}'", # username, host, room "client-left-server-notification" : "{0} left server", # name + "no-salt-notification" : "PLEASE NOTE: To allow room control passwords generated by this server instance to still work when the server is restarted, please add the following command line argument when running the Syncplay server in the future: --salt {}", #Salt # Server arguments @@ -261,11 +282,10 @@ en = { "server-port-argument" : 'server TCP port', "server-password-argument" : 'server password', "server-isolate-room-argument" : 'should rooms be isolated?', + "server-salt-argument" : "random string used to generate controlled room passwords", "server-motd-argument": "path to file from which motd will be fetched", "server-messed-up-motd-unescaped-placeholders": "Message of the Day has unescaped placeholders. All $ signs should be doubled ($$).", "server-messed-up-motd-too-long": "Message of the Day is too long - maximum of {} chars, {} given.", - "server-irc-verbose": "Should server actively report changes in rooms", - "server-irc-config": "Path to irc bot config files", # Server errors "unknown-command-server-error" : "Unknown command {}", # message @@ -275,9 +295,6 @@ en = { "password-required-server-error" : "Password required", "wrong-password-server-error" : "Wrong password supplied", "hello-server-error" : "Not enough Hello arguments", - "version-mismatch-server-error" : "Mismatch between versions of client and server" - - } pl = { diff --git a/syncplay/players/mplayer.py b/syncplay/players/mplayer.py index 03b1ea2..c01ac5f 100644 --- a/syncplay/players/mplayer.py +++ b/syncplay/players/mplayer.py @@ -145,6 +145,14 @@ class MplayerPlayer(BasePlayer): def lineReceived(self, line): if line: self._client.ui.showDebugMessage("player << {}".format(line)) + if "Failed to get value of property" in line: + if "filename" in line: + self._getFilename() + elif "length" in line: + self._getLength() + elif "path" in line: + self._getFilepath() + return match = self.RE_ANSWER.match(line) if not match: self._handleUnknownLine(line) diff --git a/syncplay/protocols.py b/syncplay/protocols.py index fc4db62..8271a2b 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -76,6 +76,7 @@ class SyncClientProtocol(JSONCommandProtocol): username = hello["username"] if hello.has_key("username") else None roomName = hello["room"]["name"] if hello.has_key("room") else None version = hello["version"] if hello.has_key("version") else None + version = hello["realversion"] if hello.has_key("realversion") else version # Used for 1.2.X compatibility motd = hello["motd"] if hello.has_key("motd") else None return username, roomName, version, motd @@ -83,8 +84,6 @@ class SyncClientProtocol(JSONCommandProtocol): username, roomName, version, motd = self._extractHelloArguments(hello) if not username or not roomName or not version: self.dropWithError(getMessage("hello-server-error").format(hello)) - elif version.split(".")[0:2] != syncplay.version.split(".")[0:2]: - self.dropWithError(getMessage("version-mismatch-server-error".format(hello))) else: self._client.setUsername(username) self._client.setRoom(roomName) @@ -92,7 +91,9 @@ class SyncClientProtocol(JSONCommandProtocol): if motd: self._client.ui.showMessage(motd, True, True) self._client.ui.showMessage(getMessage("connected-successful-notification")) + self._client.connected() self._client.sendFile() + self._client.setServerVersion(version) def sendHello(self): hello = {} @@ -101,7 +102,8 @@ class SyncClientProtocol(JSONCommandProtocol): if password: hello["password"] = password room = self._client.getRoom() if room: hello["room"] = {"name" :room} - hello["version"] = syncplay.version + hello["version"] = "1.2.255" # Used so newer clients work on 1.2.X server + hello["realversion"] = syncplay.version self.sendMessage({"Hello": hello}) def _SetUser(self, users): @@ -119,13 +121,21 @@ class SyncClientProtocol(JSONCommandProtocol): self._client.userlist.modUser(username, room, file_) def handleSet(self, settings): - for set_ in settings.iteritems(): - command = set_[0] + for (command, values) in settings.iteritems(): if command == "room": - roomName = set_[1]["name"] if set_[1].has_key("name") else None + roomName = values["name"] if values.has_key("name") else None self._client.setRoom(roomName) elif command == "user": - self._SetUser(set_[1]) + self._SetUser(values) + elif command == "controllerAuth": + if values['success']: + self._client.controllerIdentificationSuccess(values["user"], values["room"]) + else: + self._client.controllerIdentificationError(values["user"], values["room"]) + elif command == "newControlledRoom": + controlPassword = values['password'] + roomName = values['roomName'] + self._client.controlledRoomCreated(roomName, controlPassword) def sendSet(self, setting): self.sendMessage({"Set": setting}) @@ -147,7 +157,8 @@ class SyncClientProtocol(JSONCommandProtocol): for user in room[1].iteritems(): userName = user[0] file_ = user[1]['file'] if user[1]['file'] <> {} else None - self._client.userlist.addUser(userName, roomName, file_, noMessage=True) + isController = user[1]['controller'] if 'controller' in user[1] else False + self._client.userlist.addUser(userName, roomName, file_, noMessage=True, isController=isController) self._client.userlist.showUserList() def sendList(self): @@ -215,8 +226,16 @@ class SyncClientProtocol(JSONCommandProtocol): state["ignoringOnTheFly"]["client"] = self.clientIgnoringOnTheFly self.sendMessage({"State": state}) + def requestControlledRoom(self, room, password): + self.sendSet({ + "controllerAuth": { + "room": room, + "password": password + } + }) + def handleError(self, error): - self.dropWithError(error["message"]) # TODO: more processing and fallbacking + self.dropWithError(error["message"]) def sendError(self, message): self.sendMessage({"Error": {"message": message}}) @@ -261,7 +280,7 @@ class SyncServerProtocol(JSONCommandProtocol): return self._logged def _extractHelloArguments(self, hello): - roomName, roomPassword = None, None + roomName = None username = hello["username"] if hello.has_key("username") else None username = username.strip() serverPassword = hello["password"] if hello.has_key("password") else None @@ -269,9 +288,9 @@ class SyncServerProtocol(JSONCommandProtocol): if room: roomName = room["name"] if room.has_key("name") else None roomName = roomName.strip() - roomPassword = room["password"] if room.has_key("password") else None version = hello["version"] if hello.has_key("version") else None - return username, serverPassword, roomName, roomPassword, version + version = hello["realversion"] if hello.has_key("realversion") else version + return username, serverPassword, roomName, version def _checkPassword(self, serverPassword): if self._factory.password: @@ -284,15 +303,14 @@ class SyncServerProtocol(JSONCommandProtocol): return True def handleHello(self, hello): - username, serverPassword, roomName, roomPassword, version = self._extractHelloArguments(hello) + username, serverPassword, roomName, version = self._extractHelloArguments(hello) if not username or not roomName or not version: self.dropWithError(getMessage("hello-server-error")) - elif version.split(".")[0:2] != syncplay.version.split(".")[0:2]: - self.dropWithError(getMessage("version-mismatch-server-error")) + return else: if not self._checkPassword(serverPassword): return - self._factory.addWatcher(self, username, roomName, roomPassword) + self._factory.addWatcher(self, username, roomName) self._logged = True self.sendHello(version) @@ -306,7 +324,8 @@ class SyncServerProtocol(JSONCommandProtocol): userIp = self.transport.getPeer().host room = self._watcher.getRoom() if room: hello["room"] = {"name": room.getName()} - hello["version"] = syncplay.version + hello["version"] = clientVersion # Used so 1.2.X client works on newer server + hello["realversion"] = syncplay.version hello["motd"] = self._factory.getMotd(userIp, username, room, clientVersion) self.sendMessage({"Hello": hello}) @@ -319,14 +338,34 @@ class SyncServerProtocol(JSONCommandProtocol): self._factory.setWatcherRoom(self._watcher, roomName) elif command == "file": self._watcher.setFile(set_[1]) + elif command == "controllerAuth": + password = set_[1]["password"] if set_[1].has_key("password") else None + room = set_[1]["room"] if set_[1].has_key("room") else None + self._factory.authRoomController(self._watcher, password, room) def sendSet(self, setting): self.sendMessage({"Set": setting}) + def sendNewControlledRoom(self, roomName, password): + self.sendSet({ + "newControlledRoom": { + "password": password, + "roomName": roomName + } + }) + + def sendControlledRoomAuthStatus(self, success, username, roomname): + self.sendSet({ + "controllerAuth": { + "user": username, + "room": roomname, + "success": success + } + }) + def sendUserSetting(self, username, room, file_, event): room = {"name": room.getName()} - user = {} - user[username] = {} + user = {username: {}} user[username]["room"] = room if file_: user[username]["file"] = file_ @@ -339,7 +378,11 @@ class SyncServerProtocol(JSONCommandProtocol): if room: if room.getName() not in userlist: userlist[room.getName()] = {} - userFile = { "position": 0, "file": watcher.getFile() if watcher.getFile() else {} } + userFile = { + "position": 0, + "file": watcher.getFile() if watcher.getFile() else {}, + "controller": watcher.isController() + } userlist[room.getName()][watcher.getName()] = userFile def sendList(self): @@ -362,8 +405,8 @@ class SyncServerProtocol(JSONCommandProtocol): "position": position if position else 0, "paused": paused, "doSeek": doSeek, - "setBy": setBy.getName() - } + "setBy": setBy.getName() if setBy else None + } ping = { "latencyCalculation": self._pingService.newTimestamp(), "serverRtt": self._pingService.getRtt() diff --git a/syncplay/server.py b/syncplay/server.py index b349003..221b262 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -1,4 +1,5 @@ import hashlib +import random from twisted.internet import task, reactor from twisted.internet.protocol import Factory import syncplay @@ -10,13 +11,18 @@ import codecs import os from string import Template import argparse +from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomStringGenerator, meetsMinVersion class SyncFactory(Factory): - def __init__(self, password='', motdFilePath=None, isolateRooms=False): + def __init__(self, password='', motdFilePath=None, isolateRooms=False, salt=None): print getMessage("welcome-server-notification").format(syncplay.version) if password: password = hashlib.md5(password).hexdigest() self.password = password + if salt is None: + salt = RandomStringGenerator.generate_server_salt() + print getMessage("no-salt-notification").format(salt) + self._salt = salt self._motdFilePath = motdFilePath if not isolateRooms: self._roomManager = RoomManager() @@ -36,7 +42,7 @@ class SyncFactory(Factory): def getMotd(self, userIp, username, room, clientVersion): oldClient = False if constants.WARN_OLD_CLIENTS: - if int(clientVersion.replace(".", "")) < int(constants.RECENT_CLIENT_THRESHOLD.replace(".", "")): + if not meetsMinVersion(clientVersion, constants.RECENT_CLIENT_THRESHOLD): oldClient = True if self._motdFilePath and os.path.isfile(self._motdFilePath): tmpl = codecs.open(self._motdFilePath, "r", "utf-8-sig").read() @@ -54,7 +60,7 @@ class SyncFactory(Factory): else: return "" - def addWatcher(self, watcherProtocol, username, roomName, roomPassword): + def addWatcher(self, watcherProtocol, username, roomName): username = self._roomManager.findFreeUsername(username) watcher = Watcher(self, watcherProtocol, username) self.setWatcherRoom(watcher, roomName, asJoin=True) @@ -65,13 +71,16 @@ class SyncFactory(Factory): self.sendJoinMessage(watcher) else: self.sendRoomSwitchMessage(watcher) + if RoomPasswordProvider.isControlledRoom(roomName): + for controller in watcher.getRoom().getControllers(): + watcher.sendControlledRoomAuthStatus(True, controller, roomName) def sendRoomSwitchMessage(self, watcher): l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None) self._roomManager.broadcast(watcher, l) def removeWatcher(self, watcher): - if watcher.getRoom(): + if watcher and watcher.getRoom(): self.sendLeftMessage(watcher) self._roomManager.removeWatcher(watcher) @@ -83,21 +92,40 @@ class SyncFactory(Factory): l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"joined": True}) if w != watcher else None self._roomManager.broadcast(watcher, l) - def sendFileUpdate(self, watcher, file_): + def sendFileUpdate(self, watcher): l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), watcher.getFile(), None) self._roomManager.broadcast(watcher, l) - def forcePositionUpdate(self, room, watcher, doSeek): + def forcePositionUpdate(self, watcher, doSeek, watcherPauseState): room = watcher.getRoom() - paused, position = room.isPaused(), watcher.getPosition() - setBy = watcher - room.setPosition(watcher.getPosition(), setBy) - l = lambda w: w.sendState(position, paused, doSeek, setBy, True) - self._roomManager.broadcastRoom(watcher, l) + if room.canControl(watcher): + paused, position = room.isPaused(), watcher.getPosition() + setBy = watcher + l = lambda w: w.sendState(position, paused, doSeek, setBy, True) + room.setPosition(watcher.getPosition(), setBy) + self._roomManager.broadcastRoom(watcher, l) + else: + watcher.sendState(room.getPosition(), watcherPauseState, False, watcher, True) # Fixes BC break with 1.2.x + watcher.sendState(room.getPosition(), room.isPaused(), True, room.getSetBy(), True) def getAllWatchersForUser(self, forUser): return self._roomManager.getAllWatchersForUser(forUser) + def authRoomController(self, watcher, password, roomBaseName=None): + room = watcher.getRoom() + roomName = roomBaseName if roomBaseName else room.getName() + try: + success = RoomPasswordProvider.check(roomName, password, self._salt) + if success: + watcher.getRoom().addController(watcher) + self._roomManager.broadcast(watcher, lambda w: w.sendControlledRoomAuthStatus(success, watcher.getName(), room._name)) + except NotControlledRoom: + newName = RoomPasswordProvider.getControlledRoomName(roomName, password, self._salt) + watcher.sendNewControlledRoom(newName, password) + except ValueError: + self._roomManager.broadcastRoom(watcher, lambda w: w.sendControlledRoomAuthStatus(False, watcher.getName(), room._name)) + + class RoomManager(object): def __init__(self): self._rooms = {} @@ -135,7 +163,10 @@ class RoomManager(object): if roomName in self._rooms: return self._rooms[roomName] else: - room = Room(roomName) + if RoomPasswordProvider.isControlledRoom(roomName): + room = ControlledRoom(roomName) + else: + room = Room(roomName) self._rooms[roomName] = room return room @@ -228,6 +259,44 @@ class Room(object): def getSetBy(self): return self._setBy + def canControl(self, watcher): + return True + +class ControlledRoom(Room): + def __init__(self, name): + Room.__init__(self, name) + self._controllers = {} + + def getPosition(self): + if self._controllers: + watcher = min(self._controllers.values()) + self._setBy = watcher + return watcher.getPosition() + else: + return 0 + + def addController(self, watcher): + self._controllers[watcher.getName()] = watcher + + def removeWatcher(self, watcher): + Room.removeWatcher(self, watcher) + if watcher.getName() in self._controllers: + del self._controllers[watcher.getName()] + + def setPaused(self, paused=Room.STATE_PAUSED, setBy=None): + if self.canControl(setBy): + Room.setPaused(self, paused, setBy) + + def setPosition(self, position, setBy=None): + if self.canControl(setBy): + Room.setPosition(self, position, setBy) + + def canControl(self, watcher): + return watcher.getName() in self._controllers + + def getControllers(self): + return self._controllers + class Watcher(object): def __init__(self, server, connector, name): self._server = server @@ -243,7 +312,7 @@ class Watcher(object): def setFile(self, file_): self._file = file_ - self._server.sendFileUpdate(self, file_) + self._server.sendFileUpdate(self) def setRoom(self, room): self._room = room @@ -277,16 +346,22 @@ class Watcher(object): def sendSetting(self, user, room, file_, event): self._connector.sendUserSetting(user, room, file_, event) + def sendNewControlledRoom(self, roomBaseName, password): + self._connector.sendNewControlledRoom(roomBaseName, password) + + def sendControlledRoomAuthStatus(self, success, username, room): + self._connector.sendControlledRoomAuthStatus(success, username, room) + def __lt__(self, b): if self.getPosition() is None or self._file is None: return False - if b.getPosition is None or b._file is None: + if b.getPosition() is None or b.getFile() is None: return True return self.getPosition() < b.getPosition() def _scheduleSendState(self): self._sendStateTimer = task.LoopingCall(self._askForStateUpdate) - self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL, True) + self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL) def _askForStateUpdate(self, doSeek=False, forcedUpdate=False): self._server.sendState(self, doSeek, forcedUpdate) @@ -313,18 +388,25 @@ class Watcher(object): return False return self._room.isPaused() and not paused or not self._room.isPaused() and paused + def _updatePositionByAge(self, messageAge, paused, position): + if not paused: + position += messageAge + return position + def updateState(self, position, paused, doSeek, messageAge): pauseChanged = self.__hasPauseChanged(paused) self._lastUpdatedOn = time.time() if pauseChanged: self.getRoom().setPaused(Room.STATE_PAUSED if paused else Room.STATE_PLAYING, self) if position is not None: - if not paused: - position += messageAge + position = self._updatePositionByAge(messageAge, paused, position) self.setPosition(position) if doSeek or pauseChanged: - self._server.forcePositionUpdate(self._room, self, doSeek) + self._server.forcePositionUpdate(self, doSeek, paused) + def isController(self): + return RoomPasswordProvider.isControlledRoom(self._room.getName()) \ + and self._room.canControl(self) class ConfigurationGetter(object): def getConfiguration(self): @@ -340,4 +422,5 @@ class ConfigurationGetter(object): self._argparser.add_argument('--port', metavar='port', type=str, nargs='?', help=getMessage("server-port-argument")) self._argparser.add_argument('--password', metavar='password', type=str, nargs='?', help=getMessage("server-password-argument")) self._argparser.add_argument('--isolate-rooms', action='store_true', help=getMessage("server-isolate-room-argument")) - self._argparser.add_argument('--motd-file', metavar='file', type=str, nargs='?', help=getMessage("server-motd-argument")) + self._argparser.add_argument('--salt', metavar='salt', type=str, nargs='?', help=getMessage("server-salt-argument")) + self._argparser.add_argument('--motd-file', metavar='file', type=str, nargs='?', help=getMessage("server-motd-argument")) \ No newline at end of file diff --git a/syncplay/ui/GuiConfiguration.py b/syncplay/ui/GuiConfiguration.py index d858b1f..2bbda7a 100644 --- a/syncplay/ui/GuiConfiguration.py +++ b/syncplay/ui/GuiConfiguration.py @@ -426,7 +426,7 @@ class ConfigDialog(QtGui.QDialog): self.desyncFrame.setMidLineWidth(0) self.slowdownThresholdLabel = QLabel(getMessage("slowdown-threshold-label"), self) - self.slowdownThresholdLabel.setStyleSheet(constants.STYLE_SUBLABEL.format(self.posixresourcespath + "bullet_black.png")) + self.slowdownThresholdLabel.setStyleSheet(constants.STYLE_SUBLABEL.format(self.posixresourcespath + "chevrons_right.png")) self.slowdownThresholdSpinbox = QDoubleSpinBox() try: @@ -443,7 +443,7 @@ class ConfigDialog(QtGui.QDialog): self.slowdownThresholdSpinbox.adjustSize() self.rewindThresholdLabel = QLabel(getMessage("rewind-threshold-label"), self) - self.rewindThresholdLabel.setStyleSheet(constants.STYLE_SUBLABEL.format(self.posixresourcespath + "bullet_black.png")) + self.rewindThresholdLabel.setStyleSheet(constants.STYLE_SUBLABEL.format(self.posixresourcespath + "chevrons_right.png")) self.rewindThresholdSpinbox = QDoubleSpinBox() try: rewindThreshold = float(config['rewindThreshold']) @@ -515,22 +515,22 @@ class ConfigDialog(QtGui.QDialog): self.showSameRoomOSDCheckbox = QCheckBox(getMessage("showsameroomosd-label")) self.showSameRoomOSDCheckbox.setObjectName("showSameRoomOSD") - self.showSameRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "bullet_black.png")) + self.showSameRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png")) self.osdSettingsLayout.addWidget(self.showSameRoomOSDCheckbox) self.showDifferentRoomOSDCheckbox = QCheckBox(getMessage("showdifferentroomosd-label")) self.showDifferentRoomOSDCheckbox.setObjectName("showDifferentRoomOSD") - self.showDifferentRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "bullet_black.png")) + self.showDifferentRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png")) self.osdSettingsLayout.addWidget(self.showDifferentRoomOSDCheckbox) self.slowdownOSDCheckbox = QCheckBox(getMessage("showslowdownosd-label")) self.slowdownOSDCheckbox.setObjectName("showSlowdownOSD") - self.slowdownOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "bullet_black.png")) + self.slowdownOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png")) self.osdSettingsLayout.addWidget(self.slowdownOSDCheckbox) self.showOSDWarningsCheckbox = QCheckBox(getMessage("showosdwarnings-label")) self.showOSDWarningsCheckbox.setObjectName("showOSDWarnings") - self.showOSDWarningsCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "bullet_black.png")) + self.showOSDWarningsCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png")) self.osdSettingsLayout.addWidget(self.showOSDWarningsCheckbox) self.subitems['showOSD'] = ["showSameRoomOSD", "showDifferentRoomOSD", "showSlowdownOSD", "showOSDWarnings"] diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index dd2bd84..1b060ef 100644 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -35,7 +35,10 @@ class ConsoleUI(threading.Thread): self._executeCommand(data) except EOFError: pass - + + def updateRoomName(self, room=""): + pass + def promptFor(self, prompt=">", message=""): if message <> "": print(message) @@ -49,14 +52,17 @@ class ConsoleUI(threading.Thread): message = u"In room '{}':".format(room) self.showMessage(message, True) for user in rooms[room]: - username = "*<{}>*".format(user.username) if user == currentUser else "<{}>".format(user.username) + userflags = u"" + if user.isController(): + userflags = userflags + u"(Controller) " + username = userflags + u"*<{}>*".format(user.username) if user == currentUser else userflags + u"<{}>".format(user.username) if user.file: message = u"{} is playing:".format(username) self.showMessage(message, True) message = u" File: '{}' ({})".format(user.file['name'], formatTime(user.file['duration'])) if currentUser.file: if user.file['name'] == currentUser.file['name'] and user.file['size'] != currentUser.file['size']: - message += " (their file size is different from yours!)" + message += u" (their file size is different from yours!)" self.showMessage(message, True) else: message = u"{} is not playing a file".format(username) @@ -74,7 +80,7 @@ class ConsoleUI(threading.Thread): def showDebugMessage(self, message): print(message) - + def showErrorMessage(self, message, criticalerror = False): print("ERROR:\t" + message) @@ -134,6 +140,15 @@ class ConsoleUI(threading.Thread): self._syncplayClient.setRoom(room) self._syncplayClient.sendRoom() + elif command.group('command') in constants.COMMANDS_CREATE: + roombasename = command.group('parameter') + if roombasename == None: + roombasename = self._syncplayClient.getRoom() + roombasename = utils.stripRoomName(roombasename) + self._syncplayClient.createControlledRoom(roombasename) + elif command.group('command') in constants.COMMANDS_AUTH: + controlpassword = command.group('parameter') + self._syncplayClient.identifyAsController(controlpassword) else: if self._tryAdvancedCommands(data): return @@ -146,6 +161,8 @@ class ConsoleUI(threading.Thread): self.showMessage(getMessage("commandlist-notification/pause"), True) self.showMessage(getMessage("commandlist-notification/seek"), True) self.showMessage(getMessage("commandlist-notification/help"), True) + self.showMessage(getMessage("commandlist-notification/create"), True) + self.showMessage(getMessage("commandlist-notification/auth"), True) self.showMessage(getMessage("syncplay-version-notification").format(syncplay.version), True) self.showMessage(getMessage("more-info-notification").format(syncplay.projectURL), True) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index ad57eb9..bca3a43 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -1,45 +1,90 @@ -from PySide import QtGui #@UnresolvedImport -from PySide.QtCore import Qt, QSettings, QSize, QPoint #@UnresolvedImport +from PySide import QtGui +from PySide.QtCore import Qt, QSettings, QSize, QPoint from syncplay import utils, constants, version from syncplay.messages import getMessage import sys import time import re import os -import threading -from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration +from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration, RoomPasswordProvider, formatSize + +class UserlistItemDelegate(QtGui.QStyledItemDelegate): + def __init__(self): + QtGui.QStyledItemDelegate.__init__(self) + + def sizeHint(self, option, index): + size = QtGui.QStyledItemDelegate.sizeHint(self, option, index) + if (index.column() == 0): + size.setWidth(size.width() + constants.USERLIST_GUI_USERNAME_OFFSET) + return size + + def paint(self, itemQPainter, optionQStyleOptionViewItem, indexQModelIndex): + column = indexQModelIndex.column() + if column == 0: + currentQAbstractItemModel = indexQModelIndex.model() + itemQModelIndex = currentQAbstractItemModel.index(indexQModelIndex.row(), 0, indexQModelIndex.parent()) + if sys.platform.startswith('win'): + resourcespath = utils.findWorkingDir() + "\\resources\\" + else: + resourcespath = utils.findWorkingDir() + "/resources/" + controlIconQPixmap = QtGui.QPixmap(resourcespath + "user_key.png") + tickIconQPixmap = QtGui.QPixmap(resourcespath + "tick.png") + crossIconQPixmap = QtGui.QPixmap(resourcespath + "cross.png") + roomController = currentQAbstractItemModel.data(itemQModelIndex, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE) + userReady = currentQAbstractItemModel.data(itemQModelIndex, Qt.UserRole + constants.USERITEM_READY_ROLE) + + if roomController and not controlIconQPixmap.isNull(): + itemQPainter.drawPixmap ( + optionQStyleOptionViewItem.rect.x()+6, + optionQStyleOptionViewItem.rect.y(), + controlIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio)) + + if userReady and not tickIconQPixmap.isNull(): + itemQPainter.drawPixmap ( + (optionQStyleOptionViewItem.rect.x()-10), + optionQStyleOptionViewItem.rect.y(), + tickIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio)) + + elif userReady == False and not crossIconQPixmap.isNull(): + itemQPainter.drawPixmap ( + (optionQStyleOptionViewItem.rect.x()-10), + optionQStyleOptionViewItem.rect.y(), + crossIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio)) + isUserRow = indexQModelIndex.parent() != indexQModelIndex.parent().parent() + if isUserRow: + optionQStyleOptionViewItem.rect.setX(optionQStyleOptionViewItem.rect.x()+constants.USERLIST_GUI_USERNAME_OFFSET) + QtGui.QStyledItemDelegate.paint(self, itemQPainter, optionQStyleOptionViewItem, indexQModelIndex) class MainWindow(QtGui.QMainWindow): + class topSplitter(QtGui.QSplitter): + def createHandle(self): + return self.topSplitterHandle(self.orientation(), self) + + class topSplitterHandle(QtGui.QSplitterHandle): + def mouseReleaseEvent(self, event): + QtGui.QSplitterHandle.mouseReleaseEvent(self, event) + self.parent().parent().parent().updateListGeometry() + + def mouseMoveEvent(self, event): + QtGui.QSplitterHandle.mouseMoveEvent(self, event) + self.parent().parent().parent().updateListGeometry() + def addClient(self, client): self._syncplayClient = client self.roomInput.setText(self._syncplayClient.getRoom()) self.config = self._syncplayClient.getConfig() + constants.SHOW_BUTTON_LABELS = self.config['showButtonLabels'] try: if self.contactLabel and not self.config['showContactInfo']: self.contactLabel.hide() - if not self.config['showButtonLabels']: - if constants.MERGE_PLAYPAUSE_BUTTONS: - self.playpauseButton.setText("") - else: - self.playButton.setText("") - self.playButton.setFixedWidth(self.playButton.minimumSizeHint().width()) - self.pauseButton.setText("") - self.pauseButton.setFixedWidth(self.pauseButton.minimumSizeHint().width()) - self.roomButton.setText("") - self.roomButton.setFixedWidth(self.roomButton.minimumSizeHint().width()) - self.seekButton.setText("") - self.seekButton.setFixedWidth(self.seekButton.minimumSizeHint().width()) - self.unseekButton.setText("") - self.unseekButton.setFixedWidth(self.unseekButton.minimumSizeHint().width()) - self.roomGroup.setFixedWidth(self.roomGroup.sizeHint().width()) - self.seekGroup.setFixedWidth(self.seekGroup.minimumSizeHint().width()) - self.miscGroup.setFixedWidth(self.miscGroup.minimumSizeHint().width()) + if not constants.SHOW_BUTTON_LABELS: + self.hideRoomSeekLabels() + self.hideMiscLabels() except (): pass - def promptFor(self, prompt=">", message=""): - #TODO: Prompt user + # TODO: Prompt user return None def showMessage(self, message, noTimestamp=False): @@ -52,86 +97,133 @@ class MainWindow(QtGui.QMainWindow): self.newMessage(message + "
") else: self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "
") - + def showUserList(self, currentUser, rooms): self._usertreebuffer = QtGui.QStandardItemModel() - self._usertreebuffer.setColumnCount(2) - self._usertreebuffer.setHorizontalHeaderLabels((getMessage("roomuser-heading-label"),getMessage("fileplayed-heading-label"))) + self._usertreebuffer.setHorizontalHeaderLabels( + (getMessage("roomuser-heading-label"), getMessage("size-heading-label"), getMessage("duration-heading-label"), getMessage("filename-heading-label") )) usertreeRoot = self._usertreebuffer.invisibleRootItem() - + for room in rooms: roomitem = QtGui.QStandardItem(room) + font = QtGui.QFont() + font.setItalic(True) if room == currentUser.room: - font = QtGui.QFont() font.setWeight(QtGui.QFont.Bold) - roomitem.setFont(font) - blankitem = QtGui.QStandardItem("") - roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable) - blankitem.setFlags(blankitem.flags() & ~Qt.ItemIsEditable) - usertreeRoot.appendRow((roomitem, blankitem)) + roomitem.setFont(font) + roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable) + usertreeRoot.appendRow(roomitem) + isControlledRoom = RoomPasswordProvider.isControlledRoom(room) + + if isControlledRoom: + if room == currentUser.room and currentUser.isController(): + roomitem.setIcon(QtGui.QIcon(self.resourcespath + 'lock_open.png')) + else: + roomitem.setIcon(QtGui.QIcon(self.resourcespath + 'lock.png')) + else: + roomitem.setIcon(QtGui.QIcon(self.resourcespath + 'chevrons_right.png')) + for user in rooms[room]: useritem = QtGui.QStandardItem(user.username) - fileitem = QtGui.QStandardItem("") + isController = user.isController() + useritem.setData(isController, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE) if user.file: - fileitem = QtGui.QStandardItem(u"{} ({})".format(user.file['name'], formatTime(user.file['duration']))) + filesizeitem = QtGui.QStandardItem(formatSize(user.file['size'])) + filedurationitem = QtGui.QStandardItem("({})".format(formatTime(user.file['duration']))) + filenameitem = QtGui.QStandardItem((user.file['name'])) if currentUser.file: sameName = sameFilename(user.file['name'], currentUser.file['name']) sameSize = sameFilesize(user.file['size'], currentUser.file['size']) sameDuration = sameFileduration(user.file['duration'], currentUser.file['duration']) sameRoom = room == currentUser.room - differentName = not sameName - differentSize = not sameSize - differentDuration = not sameDuration - if sameName or sameRoom: - if differentSize and sameDuration: - fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentsize-note"))) - elif differentSize and differentDuration: - fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentsizeandduration-note"))) - elif differentDuration: - fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentduration-note"))) - if sameRoom and (differentName or differentSize or differentDuration): - fileitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR))) + underlinefont = QtGui.QFont() + underlinefont.setUnderline(True) + if sameRoom: + if not sameName: + filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR))) + filenameitem.setFont(underlinefont) + if not sameSize: + if currentUser.file is not None and formatSize(user.file['size']) == formatSize(currentUser.file['size']): + filesizeitem = QtGui.QStandardItem(formatSize(user.file['size'],precise=True)) + filesizeitem.setFont(underlinefont) + filesizeitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR))) + if not sameDuration: + filedurationitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR))) + filedurationitem.setFont(underlinefont) else: - fileitem = QtGui.QStandardItem(getMessage("nofile-note")) + filenameitem = QtGui.QStandardItem(getMessage("nofile-note")) + filedurationitem = QtGui.QStandardItem("") + filesizeitem = QtGui.QStandardItem("") if room == currentUser.room: - fileitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOFILEITEM_COLOR))) + filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOFILEITEM_COLOR))) + font = QtGui.QFont() if currentUser.username == user.username: - font = QtGui.QFont() font.setWeight(QtGui.QFont.Bold) - useritem.setFont(font) - useritem.setFlags(useritem.flags() & ~Qt.ItemIsEditable) - fileitem.setFlags(fileitem.flags() & ~Qt.ItemIsEditable) - roomitem.appendRow((useritem, fileitem)) - + if isControlledRoom and not isController: + if currentUser.username == user.username: + useritem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOTCONTROLLER_COLOR))) + useritem.setFont(font) + useritem.setFlags(useritem.flags() & ~Qt.ItemIsEditable) + filenameitem.setFlags(filenameitem.flags() & ~Qt.ItemIsEditable) + filesizeitem.setFlags(filesizeitem.flags() & ~Qt.ItemIsEditable) + filedurationitem.setFlags(filedurationitem.flags() & ~Qt.ItemIsEditable) + roomitem.appendRow((useritem, filesizeitem, filedurationitem, filenameitem)) self.listTreeModel = self._usertreebuffer self.listTreeView.setModel(self.listTreeModel) + self.listTreeView.setItemDelegate(UserlistItemDelegate()) self.listTreeView.setItemsExpandable(False) + self.listTreeView.setRootIsDecorated(False) self.listTreeView.expandAll() - self.listTreeView.resizeColumnToContents(0) - self.listTreeView.resizeColumnToContents(1) - + self.updateListGeometry() + + def updateListGeometry(self): + try: + roomtocheck = 0 + while self.listTreeModel.item(roomtocheck): + self.listTreeView.setFirstColumnSpanned(roomtocheck, self.listTreeView.rootIndex(), True) + roomtocheck += 1 + self.listTreeView.header().setStretchLastSection(False) + self.listTreeView.header().setResizeMode(0, QtGui.QHeaderView.ResizeToContents) + self.listTreeView.header().setResizeMode(1, QtGui.QHeaderView.ResizeToContents) + self.listTreeView.header().setResizeMode(2, QtGui.QHeaderView.ResizeToContents) + self.listTreeView.header().setResizeMode(3, QtGui.QHeaderView.ResizeToContents) + NarrowTabsWidth = self.listTreeView.header().sectionSize(0)+self.listTreeView.header().sectionSize(1)+self.listTreeView.header().sectionSize(2) + if self.listTreeView.header().width() < (NarrowTabsWidth+self.listTreeView.header().sectionSize(3)): + self.listTreeView.header().resizeSection(3,self.listTreeView.header().width()-NarrowTabsWidth) + else: + self.listTreeView.header().setResizeMode(3, QtGui.QHeaderView.Stretch) + self.listTreeView.expandAll() + except: + pass + def roomClicked(self, item): while item.parent().row() != -1: item = item.parent() self.joinRoom(item.sibling(item.row(), 0).data()) - + def userListChange(self): self._syncplayClient.showUserList() - - def showErrorMessage(self, message, criticalerror = False): + + def updateRoomName(self, room=""): + self.roomInput.setText(room) + + def showDebugMessage(self, message): + print(message) + + def showErrorMessage(self, message, criticalerror=False): message = unicode(message) if criticalerror: - QtGui.QMessageBox.critical(self,"Syncplay", message) + QtGui.QMessageBox.critical(self, "Syncplay", message) message = message.replace("&", "&").replace('"', """).replace("<", "<").replace(">", ">") message = message.replace("\n", "
") message = "".format(constants.STYLE_ERRORNOTIFICATION) + message + "" self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "
") - def joinRoom(self, room = None): + def joinRoom(self, room=None): if room == None: room = self.roomInput.text() if room == "": - if self._syncplayClient.userlist.currentUser.file: + if self._syncplayClient.userlist.currentUser.file: room = self._syncplayClient.userlist.currentUser.file["name"] else: room = self._syncplayClient.defaultRoom @@ -148,45 +240,108 @@ class MainWindow(QtGui.QMainWindow): if t is None: return if sign: - t = self._syncplayClient.getGlobalPosition() + sign * t + t = self._syncplayClient.getGlobalPosition() + sign * t self._syncplayClient.setPosition(t) else: self.showErrorMessage("Invalid seek value") - + def undoSeek(self): tmp_pos = self._syncplayClient.getPlayerPosition() self._syncplayClient.setPosition(self._syncplayClient.playerPositionBeforeLastSeek) self._syncplayClient.playerPositionBeforeLastSeek = tmp_pos - + def togglePause(self): self._syncplayClient.setPaused(not self._syncplayClient.getPlayerPaused()) - + def play(self): self._syncplayClient.setPaused(False) - + def pause(self): self._syncplayClient.setPaused(True) - + def exitSyncplay(self): self._syncplayClient.stop() - + def closeEvent(self, event): self.exitSyncplay() self.saveSettings() - + + def setupSizes(self): + self.hideRoomSeekLabels() + self.miscThreshold = self.seekGroup.sizeHint().width()+self.roomGroup.sizeHint().width()+self.miscGroup.sizeHint().width()+30 + self.hideMiscLabels() + self.setMinimumWidth(self.seekGroup.sizeHint().width()+self.roomGroup.sizeHint().width()+self.miscGroup.sizeHint().width()+30) + self.seekGroup.setMinimumWidth(self.seekGroup.sizeHint().width()) + self.roomGroup.setMinimumWidth(self.roomGroup.sizeHint().width()) + self.miscGroup.setMinimumWidth(self.miscGroup.sizeHint().width()) + self.showRoomSeekLabels() + self.showMiscLabels() + windowMaximumWidth = self.maximumWidth() + self.seekGroup.setMaximumWidth(self.seekGroup.sizeHint().width()) + self.roomGroup.setMaximumWidth(self.roomGroup.sizeHint().width()) + self.miscGroup.setMaximumWidth(self.miscGroup.sizeHint().width()) + self.setMaximumWidth(windowMaximumWidth) + self.roomSeekThreshold = self.mainLayout.sizeHint().width() + + def hideRoomSeekLabels(self): + self.roomButton.setText("") + self.seekButton.setText("") + + def hideMiscLabels(self): + self.unseekButton.setText("") + if constants.MERGE_PLAYPAUSE_BUTTONS: + self.playpauseButton.setText("") + else: + self.playButton.setText("") + self.pauseButton.setText("") + + def showRoomSeekLabels(self): + if not constants.SHOW_BUTTON_LABELS: + return + self.roomButton.setText(getMessage("joinroom-guibuttonlabel")) + self.seekButton.setText(getMessage("seektime-guibuttonlabel")) + + def showMiscLabels(self): + self.unseekButton.setText(getMessage("undoseek-guibuttonlabel")) + if not constants.SHOW_BUTTON_LABELS: + return + if constants.MERGE_PLAYPAUSE_BUTTONS: + self.playpauseButton.setText(getMessage("togglepause-guibuttonlabel")) + else: + self.playButton.setText(getMessage("play-guibuttonlabel")) + self.pauseButton.setText(getMessage("pause-guibuttonlabel")) + + def resizeEvent(self,resizeEvent): + self.updateListGeometry() + if self.roomGroup and self.miscThreshold: + currentWidth = self.mainFrame.size().width() + if currentWidth < self.miscThreshold: + if self.unseekButton.text() != "": + self.hideMiscLabels() + else: + if self.unseekButton.text() == "": + self.showMiscLabels() + + if currentWidth < self.roomSeekThreshold: + if self.roomButton.text() != "": + self.hideRoomSeekLabels() + else: + if self.roomButton.text() == "": + self.showRoomSeekLabels() + def loadMediaBrowseSettings(self): settings = QSettings("Syncplay", "MediaBrowseDialog") settings.beginGroup("MediaBrowseDialog") self.mediadirectory = settings.value("mediadir", "") settings.endGroup() - + def saveMediaBrowseSettings(self): settings = QSettings("Syncplay", "MediaBrowseDialog") settings.beginGroup("MediaBrowseDialog") settings.setValue("mediadir", self.mediadirectory) settings.endGroup() - + def browseMediapath(self): self.loadMediaBrowseSettings() options = QtGui.QFileDialog.Options() @@ -198,16 +353,30 @@ class MainWindow(QtGui.QMainWindow): defaultdirectory = QtGui.QDesktopServices.storageLocation(QtGui.QDesktopServices.HomeLocation) else: defaultdirectory = "" - browserfilter = "All files (*)" - fileName, filtr = QtGui.QFileDialog.getOpenFileName(self,getMessage("browseformedia-label"),defaultdirectory, - browserfilter, "", options) + browserfilter = "All files (*)" + fileName, filtr = QtGui.QFileDialog.getOpenFileName(self, getMessage("browseformedia-label"), defaultdirectory, + browserfilter, "", options) if fileName: if sys.platform.startswith('win'): - fileName = fileName.replace("/","\\") + fileName = fileName.replace("/", "\\") self.mediadirectory = os.path.dirname(fileName) self.saveMediaBrowseSettings() self._syncplayClient._player.openFile(fileName) - + + def createControlledRoom(self): + controlroom, ok = QtGui.QInputDialog.getText(self, getMessage("createcontrolledroom-msgbox-label"), + getMessage("controlledroominfo-msgbox-label"), QtGui.QLineEdit.Normal, + utils.stripRoomName(self._syncplayClient.getRoom())) + if ok and controlroom != '': + self._syncplayClient.createControlledRoom(controlroom) + + def identifyAsController(self): + msgboxtitle = getMessage("identifyascontroller-msgbox-label") + msgboxtext = getMessage("identifyinfo-msgbox-label") + controlpassword, ok = QtGui.QInputDialog.getText(self, msgboxtitle, msgboxtext, QtGui.QLineEdit.Normal, "") + if ok and controlpassword != '': + self._syncplayClient.identifyAsController(controlpassword) + def _extractSign(self, m): if m: if m == "-": @@ -216,11 +385,11 @@ class MainWindow(QtGui.QMainWindow): return 1 else: return None - + def setOffset(self): - newoffset, ok = QtGui.QInputDialog.getText(self,getMessage("setoffset-msgbox-label"), - getMessage("offsetinfo-msgbox-label"), QtGui.QLineEdit.Normal, - "") + newoffset, ok = QtGui.QInputDialog.getText(self, getMessage("setoffset-msgbox-label"), + getMessage("offsetinfo-msgbox-label"), QtGui.QLineEdit.Normal, + "") if ok and newoffset != '': o = re.match(constants.UI_OFFSET_REGEX, "o " + newoffset) if o: @@ -229,13 +398,13 @@ class MainWindow(QtGui.QMainWindow): if t is None: return if o.group('sign') == "/": - t = self._syncplayClient.getPlayerPosition() - t + t = self._syncplayClient.getPlayerPosition() - t elif sign: - t = self._syncplayClient.getUserOffset() + sign * t + t = self._syncplayClient.getUserOffset() + sign * t self._syncplayClient.setUserOffset(t) else: self.showErrorMessage("Invalid offset value") - + def openUserGuide(self): if sys.platform.startswith('linux'): self.QtGui.QDesktopServices.openUrl("http://syncplay.pl/guide/linux/") @@ -246,9 +415,9 @@ class MainWindow(QtGui.QMainWindow): def drop(self): self.close() - - def addTopLayout(self, window): - window.topSplit = QtGui.QSplitter(Qt.Horizontal) + + def addTopLayout(self, window): + window.topSplit = self.topSplitter(Qt.Horizontal, self) window.outputLayout = QtGui.QVBoxLayout() window.outputbox = QtGui.QTextEdit() @@ -257,21 +426,22 @@ class MainWindow(QtGui.QMainWindow): window.outputFrame = QtGui.QFrame() window.outputFrame.setLineWidth(0) window.outputFrame.setMidLineWidth(0) - window.outputLayout.setContentsMargins(0,0,0,0) + window.outputLayout.setContentsMargins(0, 0, 0, 0) window.outputLayout.addWidget(window.outputlabel) window.outputLayout.addWidget(window.outputbox) window.outputFrame.setLayout(window.outputLayout) - + window.listLayout = QtGui.QVBoxLayout() window.listTreeModel = QtGui.QStandardItemModel() window.listTreeView = QtGui.QTreeView() window.listTreeView.setModel(window.listTreeModel) + window.listTreeView.setIndentation(21) window.listTreeView.doubleClicked.connect(self.roomClicked) window.listlabel = QtGui.QLabel(getMessage("userlist-heading-label")) window.listFrame = QtGui.QFrame() window.listFrame.setLineWidth(0) window.listFrame.setMidLineWidth(0) - window.listLayout.setContentsMargins(0,0,0,0) + window.listLayout.setContentsMargins(0, 0, 0, 0) window.listLayout.addWidget(window.listlabel) window.listLayout.addWidget(window.listTreeView) window.contactLabel = QtGui.QLabel() @@ -285,13 +455,13 @@ class MainWindow(QtGui.QMainWindow): window.contactLabel.setOpenExternalLinks(True) window.listLayout.addWidget(window.contactLabel) window.listFrame.setLayout(window.listLayout) - + window.topSplit.addWidget(window.outputFrame) window.topSplit.addWidget(window.listFrame) window.topSplit.setStretchFactor(0,4) window.topSplit.setStretchFactor(1,5) window.mainLayout.addWidget(window.topSplit) - window.topSplit.setSizePolicy(QtGui.QSizePolicy.Preferred,QtGui.QSizePolicy.Expanding) + window.topSplit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) def addBottomLayout(self, window): window.bottomLayout = QtGui.QHBoxLayout() @@ -300,134 +470,152 @@ class MainWindow(QtGui.QMainWindow): window.addSeekBox(MainWindow) window.addMiscBox(MainWindow) - window.bottomLayout.addWidget(window.roomGroup, Qt.AlignLeft) - window.bottomLayout.addWidget(window.seekGroup, Qt.AlignLeft) - window.bottomLayout.addWidget(window.miscGroup, Qt.AlignLeft) + window.bottomLayout.addWidget(window.roomGroup) + window.bottomLayout.addWidget(window.seekGroup) + window.bottomLayout.addWidget(window.miscGroup) window.mainLayout.addLayout(window.bottomLayout, Qt.AlignLeft) def addRoomBox(self, window): window.roomGroup = QtGui.QGroupBox(getMessage("room-heading-label")) - + window.roomInput = QtGui.QLineEdit() window.roomInput.returnPressed.connect(self.joinRoom) - window.roomButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'door_in.png'), getMessage("joinroom-guibuttonlabel")) + window.roomButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'door_in.png'), + getMessage("joinroom-guibuttonlabel")) window.roomButton.pressed.connect(self.joinRoom) window.roomLayout = QtGui.QHBoxLayout() window.roomInput.setFixedWidth(150) self.roomButton.setToolTip(getMessage("joinroom-tooltip")) - + window.roomLayout.addWidget(window.roomInput) window.roomLayout.addWidget(window.roomButton) - + window.roomGroup.setLayout(window.roomLayout) - window.roomGroup.setFixedSize(window.roomGroup.sizeHint()) - + window.roomGroup.setFixedHeight(window.roomGroup.sizeHint().height()) + def addSeekBox(self, window): window.seekGroup = QtGui.QGroupBox(getMessage("seek-heading-label")) - + window.seekInput = QtGui.QLineEdit() window.seekInput.returnPressed.connect(self.seekPosition) - window.seekButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'clock_go.png'),getMessage("seektime-guibuttonlabel")) + window.seekButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'clock_go.png'), + getMessage("seektime-guibuttonlabel")) window.seekButton.pressed.connect(self.seekPosition) self.seekButton.setToolTip(getMessage("seektime-tooltip")) - + window.seekLayout = QtGui.QHBoxLayout() window.seekInput.setText("0:00") window.seekInput.setFixedWidth(60) - + window.seekLayout.addWidget(window.seekInput) window.seekLayout.addWidget(window.seekButton) - + window.seekGroup.setLayout(window.seekLayout) - window.seekGroup.setFixedSize(window.seekGroup.sizeHint()) - + window.seekGroup.setFixedHeight(window.seekGroup.sizeHint().height()) + def addMiscBox(self, window): window.miscGroup = QtGui.QGroupBox(getMessage("othercommands-heading-label")) - - window.unseekButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'arrow_undo.png'),getMessage("undoseek-guibuttonlabel")) + + window.unseekButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'arrow_undo.png'), + getMessage("undoseek-guibuttonlabel")) window.unseekButton.pressed.connect(self.undoSeek) self.unseekButton.setToolTip(getMessage("undoseek-tooltip")) window.miscLayout = QtGui.QHBoxLayout() window.miscLayout.addWidget(window.unseekButton) if constants.MERGE_PLAYPAUSE_BUTTONS == True: - window.playpauseButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'control_pause_blue.png'),getMessage("togglepause-guibuttonlabel")) + window.playpauseButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'control_pause_blue.png'), + getMessage("togglepause-guibuttonlabel")) window.playpauseButton.pressed.connect(self.togglePause) window.miscLayout.addWidget(window.playpauseButton) self.playpauseButton.setToolTip(getMessage("togglepause-tooltip")) else: - window.playButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'control_play_blue.png'),getMessage("play-guibuttonlabel")) + window.playButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'control_play_blue.png'), + getMessage("play-guibuttonlabel")) window.playButton.pressed.connect(self.play) window.playButton.setMaximumWidth(60) window.miscLayout.addWidget(window.playButton) - window.pauseButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'control_pause_blue.png'),getMessage("pause-guibuttonlabel")) + window.pauseButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'control_pause_blue.png'), + getMessage("pause-guibuttonlabel")) window.pauseButton.pressed.connect(self.pause) window.pauseButton.setMaximumWidth(60) window.miscLayout.addWidget(window.pauseButton) self.playButton.setToolTip(getMessage("play-tooltip")) self.pauseButton.setToolTip(getMessage("pause-tooltip")) - + window.miscGroup.setLayout(window.miscLayout) - window.miscGroup.setFixedSize(window.miscGroup.sizeHint()) - + window.miscGroup.setFixedHeight(window.miscGroup.sizeHint().height()) + def addMenubar(self, window): window.menuBar = QtGui.QMenuBar() window.fileMenu = QtGui.QMenu(getMessage("file-menu-label"), self) - window.openAction = window.fileMenu.addAction(QtGui.QIcon(self.resourcespath + 'folder_explore.png'), getMessage("openmedia-menu-label")) + window.openAction = window.fileMenu.addAction(QtGui.QIcon(self.resourcespath + 'folder_explore.png'), + getMessage("openmedia-menu-label")) window.openAction.triggered.connect(self.browseMediapath) - window.exitAction = window.fileMenu.addAction(QtGui.QIcon(self.resourcespath + 'cross.png'), getMessage("exit-menu-label")) + + window.exitAction = window.fileMenu.addAction(QtGui.QIcon(self.resourcespath + 'cross.png'), + getMessage("file-menu-label")) window.exitAction.triggered.connect(self.exitSyncplay) window.menuBar.addMenu(window.fileMenu) - + window.advancedMenu = QtGui.QMenu(getMessage("advanced-menu-label"), self) - window.setoffsetAction = window.advancedMenu.addAction(QtGui.QIcon(self.resourcespath + 'timeline_marker.png'),getMessage("setoffset-menu-label")) + window.setoffsetAction = window.advancedMenu.addAction(QtGui.QIcon(self.resourcespath + 'timeline_marker.png'), + getMessage("setoffset-menu-label")) window.setoffsetAction.triggered.connect(self.setOffset) + + window.createcontrolledroomAction = window.advancedMenu.addAction( + QtGui.QIcon(self.resourcespath + 'page_white_key.png'), getMessage("createcontrolledroom-menu-label")) + window.createcontrolledroomAction.triggered.connect(self.createControlledRoom) + window.identifyascontroller = window.advancedMenu.addAction(QtGui.QIcon(self.resourcespath + 'key_go.png'), + getMessage("identifyascontroller-menu-label")) + window.identifyascontroller.triggered.connect(self.identifyAsController) window.menuBar.addMenu(window.advancedMenu) - + window.helpMenu = QtGui.QMenu(getMessage("help-menu-label"), self) - window.userguideAction = window.helpMenu.addAction(QtGui.QIcon(self.resourcespath + 'help.png'), getMessage("userguide-menu-label")) + window.userguideAction = window.helpMenu.addAction(QtGui.QIcon(self.resourcespath + 'help.png'), + getMessage("userguide-menu-label")) window.userguideAction.triggered.connect(self.openUserGuide) - + window.menuBar.addMenu(window.helpMenu) window.mainLayout.setMenuBar(window.menuBar) - + def addMainFrame(self, window): window.mainFrame = QtGui.QFrame() window.mainFrame.setLineWidth(0) window.mainFrame.setMidLineWidth(0) - window.mainFrame.setContentsMargins(0,0,0,0) + window.mainFrame.setContentsMargins(0, 0, 0, 0) window.mainFrame.setLayout(window.mainLayout) - + window.setCentralWidget(window.mainFrame) - + def newMessage(self, message): self.outputbox.moveCursor(QtGui.QTextCursor.End) self.outputbox.insertHtml(message) self.outputbox.moveCursor(QtGui.QTextCursor.End) - + def resetList(self): self.listbox.setText("") - + def newListItem(self, item): self.listbox.moveCursor(QtGui.QTextCursor.End) self.listbox.insertHtml(item) self.listbox.moveCursor(QtGui.QTextCursor.End) - + def dragEnterEvent(self, event): data = event.mimeData() urls = data.urls() if urls and urls[0].scheme() == 'file': event.acceptProposedAction() - + def dropEvent(self, event): rewindFile = False if QtGui.QDropEvent.proposedAction(event) == Qt.MoveAction: - QtGui.QDropEvent.setDropAction(event, Qt.CopyAction) # Avoids file being deleted + QtGui.QDropEvent.setDropAction(event, Qt.CopyAction) # Avoids file being deleted rewindFile = True data = event.mimeData() urls = data.urls() @@ -439,14 +627,14 @@ class MainWindow(QtGui.QMainWindow): self._syncplayClient.setPosition(0) self._syncplayClient._player.openFile(dropfilepath, resetPosition=True) self._syncplayClient.setPosition(0) - + def saveSettings(self): settings = QSettings("Syncplay", "MainWindow") settings.beginGroup("MainWindow") settings.setValue("size", self.size()) settings.setValue("pos", self.pos()) settings.endGroup() - + def loadSettings(self): settings = QSettings("Syncplay", "MainWindow") settings.beginGroup("MainWindow") @@ -468,8 +656,9 @@ class MainWindow(QtGui.QMainWindow): self.addBottomLayout(self) self.addMenubar(self) self.addMainFrame(self) + self.setupSizes() self.loadSettings() self.setWindowIcon(QtGui.QIcon(self.resourcespath + "syncplay.png")) self.setWindowFlags(self.windowFlags() & Qt.WindowCloseButtonHint & Qt.AA_DontUseNativeMenuBar & Qt.WindowMinimizeButtonHint & ~Qt.WindowContextHelpButtonHint) self.show() - self.setAcceptDrops(True) \ No newline at end of file + self.setAcceptDrops(True) diff --git a/syncplay/utils.py b/syncplay/utils.py index 40bf3d2..d3ab921 100644 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -7,6 +7,8 @@ import sys import os import itertools import hashlib +import random +import string def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None): """Retry calling the decorated function using an exponential backoff. @@ -93,6 +95,19 @@ def formatTime(timeInSeconds, weeksAsTitles=True): formattedTime = "{0:} (Title {1:.0f})".format(formattedTime, title) return formattedTime +def formatSize (bytes, precise=False): + if bytes == 0: # E.g. when file size privacy is enabled + return "???" + try: + megabytes = int(bytes) / 1000000.0 + if precise: + megabytes = round(megabytes, 1) + else: + megabytes = int(megabytes) + return str(megabytes) + getMessage("megabyte-suffix") + except: # E.g. when filesize is hashed + return "???" + def findWorkingDir(): frozen = getattr(sys, 'frozen', '') if not frozen: @@ -141,7 +156,19 @@ def blackholeStdoutForFrozenWindow(): # Relate to file hashing / difference checking: def stripfilename(filename): - return re.sub(constants.FILENAME_STRIP_REGEX, "", filename) + if filename: + return re.sub(constants.FILENAME_STRIP_REGEX, "", filename) + else: + return "" + +def stripRoomName(RoomName): + if RoomName: + try: + return re.sub(constants.ROOM_NAME_STRIP_REGEX, "\g", RoomName) + except IndexError: + return RoomName + else: + return "" def hashFilename(filename): return hashlib.sha256(stripfilename(filename).encode('utf-8')).hexdigest()[:12] @@ -182,3 +209,69 @@ def sameFileduration (duration1, duration2): return True else: return False + +def meetsMinVersion(version, minVersion): + def versiontotuple(ver): + return tuple(map(int, ver.split("."))) + return versiontotuple(version) >= versiontotuple(minVersion) + +class RoomPasswordProvider(object): + CONTROLLED_ROOM_REGEX = re.compile("^\+(.*):(\w{12})$") + PASSWORD_REGEX = re.compile("[A-Z]{2}-\d{3}-\d{3}") + + @staticmethod + def isControlledRoom(roomName): + return bool(re.match(RoomPasswordProvider.CONTROLLED_ROOM_REGEX, roomName)) + + @staticmethod + def check(roomName, password, salt): + if not password or not re.match(RoomPasswordProvider.PASSWORD_REGEX, password): + raise ValueError() + + if not roomName: + raise NotControlledRoom() + match = re.match(RoomPasswordProvider.CONTROLLED_ROOM_REGEX, roomName) + if not match: + raise NotControlledRoom() + roomHash = match.group(2) + computedHash = RoomPasswordProvider._computeRoomHash(match.group(1), password, salt) + return roomHash == computedHash + + @staticmethod + def getControlledRoomName(roomName, password, salt): + return "+" + roomName + ":" + RoomPasswordProvider._computeRoomHash(roomName, password, salt) + + @staticmethod + def _computeRoomHash(roomName, password, salt): + roomName = roomName.encode('utf8') + salt = hashlib.sha256(salt).hexdigest() + provisionalHash = hashlib.sha256(roomName + salt).hexdigest() + return hashlib.sha1(provisionalHash + salt + password).hexdigest()[:12].upper() + +class RandomStringGenerator(object): + @staticmethod + def generate_room_password(): + parts = ( + RandomStringGenerator._get_random_letters(2), + RandomStringGenerator._get_random_numbers(3), + RandomStringGenerator._get_random_numbers(3) + ) + return "{}-{}-{}".format(*parts) + + @staticmethod + def generate_server_salt(): + parts = ( + RandomStringGenerator._get_random_letters(10), + ) + return "{}".format(*parts) + + @staticmethod + def _get_random_letters(quantity): + return ''.join(random.choice(string.ascii_uppercase) for _ in xrange(quantity)) + + @staticmethod + def _get_random_numbers(quantity): + return ''.join(random.choice(string.digits) for _ in xrange(quantity)) + +class NotControlledRoom(Exception): + pass \ No newline at end of file diff --git a/syncplayServer.py b/syncplayServer.py index 7b1faa1..c127fbf 100755 --- a/syncplayServer.py +++ b/syncplayServer.py @@ -20,5 +20,5 @@ if __name__ == '__main__': argsGetter = ConfigurationGetter() args = argsGetter.getConfiguration() - reactor.listenTCP(int(args.port), SyncFactory(args.password, args.motd_file, args.isolate_rooms)) + reactor.listenTCP(int(args.port), SyncFactory(args.password, args.motd_file, args.isolate_rooms, args.salt)) reactor.run()