From 11c53368cbfdf688dfcc6b377d5f8e2f0a99b377 Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sun, 7 Sep 2014 11:07:18 +0200 Subject: [PATCH 01/58] Updated GUI for controlled rooms --- buildPy2exe.py | 3 ++- resources/controller.png | Bin 0 -> 666 bytes resources/key_go.png | Bin 0 -> 744 bytes resources/page_white_key.png | Bin 0 -> 616 bytes syncplay/ui/gui.py | 18 ++++++++++++++++-- 5 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 resources/controller.png create mode 100644 resources/key_go.png create mode 100644 resources/page_white_key.png diff --git a/buildPy2exe.py b/buildPy2exe.py index 7f91acc..164146b 100644 --- a/buildPy2exe.py +++ b/buildPy2exe.py @@ -582,7 +582,8 @@ 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/bullet_black.png', + 'resources/controller.png', 'resources/key_go.png', 'resources/page_white_key.png' ] resources = ["resources/icon.ico", "resources/syncplay.png"] resources.extend(guiIcons) diff --git a/resources/controller.png b/resources/controller.png new file mode 100644 index 0000000000000000000000000000000000000000..5cf76ed029a4468cfcd05c1a50089a98e718418f GIT binary patch literal 666 zcmV;L0%iS)P) zlg*1#VHC#iD%uAHty;DTg0}q?ty;8bDFzCDreJ80{BB+R< zJENR4PZMQi-K$n$6{M+e{`y zCzHvi{{^55g~BV5NOa+FIKC8%#WFEpk_S+;0uA4flMsu=wvk9g@eiQYYGoUZ#;;nf zwk?;-FdB`3VHmiWEa5y`gl|tCa~huHAQ%j?r0MVf0GiFFt=sK#C%yTk;Ec=V5(eW1 zy!^<*iw^+Bx5qH>dSE^ra{Kc6JlJfuBMv}5)@Yi~=Nw?K*=zV6|Ft zLOAX<8jU;B^k)=l4vT=l&3kn^9UxVL)9GYU)9I9}vfXZjQmOn(B@&4pQul^EU^1CN zHk*ZXIt^qGkTG9cEEbJkuV?WZo6QC!5(!Z8c%13?`&?OQ#B#^uF;uHnNTpKPc_2n0 zqyCI{R;g4puH&IXp%AEOGKB zFaZe_Htgq(0OW7HPl9o7Hk+@=e6O@xZTY%@-!Rz$1Rlkwh5!Hn07*qoM6N<$f($t; AN&o-= literal 0 HcmV?d00001 diff --git a/resources/key_go.png b/resources/key_go.png new file mode 100644 index 0000000000000000000000000000000000000000..30b0dc316e52dba388d88112d4c1cc32672fffbb GIT binary patch literal 744 zcmVP)jGlUfiHCke$)P}4*HwX@?mb` zzgiE#M5fLDxtj=lZ6q=+Lkt2$!wyVqxC~@X03De&Gn$t$kdWJig()R`(|L#ldNHNi zi?tK@-;xA1zab1r3XkO&T;m5wdrx2~XU8>fpl4v~7ZF1h+!NXGy*rQ6jw}?mrNKGI zL&zzIrIL-VTRiLE`+jy5wt-SCISiy4pO}x6>V>$`&V{7&3{GoOF)87oTao_ek0H|L zXxL5q?8f4p7$RLZL=Q4>?LH3$EqhS@^b{VAG@wL(0y*{DquI)BECvw!(ty8jr^s8DqzY3Mx|xw6AM%RhNVG>V(j48H=@2*+n87;k63kFsH&hc^Czx zU)p@TON5%2#gM-!LRIG_NS|MUrcZ`*_YPuLB^9Iro+W2D85SRofmAHM&xik`9B1#a z@o-oLow*L$!CJHqC_n){?E(&Zwhf|^V!qqZ#X+&c)jMMwsg3*W31W<{FoWOGV1 zuOTTStWS(&DYr&0v}HowTZPN*IY_RcCU%rj3Cs*;4T3Shy&sFSmGFOV!%!{P*`>;C zSiN43jAg&56(U(ojS}-tZUVHjYHp;RjQ0M0pRlXN=mLv{hk9Ebp9&~+Wj-T9IkpzWPWd#fZ)d=zV^~S`;LE*! Date: Sun, 7 Sep 2014 11:08:00 +0200 Subject: [PATCH 02/58] Reformated GUI code --- syncplay/ui/gui.py | 210 +++++++++++++++++++++++++-------------------- 1 file changed, 116 insertions(+), 94 deletions(-) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index a041a3c..9dab263 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -1,5 +1,5 @@ -from PySide import QtGui #@UnresolvedImport -from PySide.QtCore import Qt, QSettings, QSize, QPoint #@UnresolvedImport +from PySide import QtGui # @UnresolvedImport +from PySide.QtCore import Qt, QSettings, QSize, QPoint # @UnresolvedImport from syncplay import utils, constants, version from syncplay.messages import getMessage import sys @@ -8,6 +8,7 @@ import re import os from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration + class MainWindow(QtGui.QMainWindow): def addClient(self, client): self._syncplayClient = client @@ -36,9 +37,9 @@ class MainWindow(QtGui.QMainWindow): except (): pass - + def promptFor(self, prompt=">", message=""): - #TODO: Prompt user + # TODO: Prompt user return None def showMessage(self, message, noTimestamp=False): @@ -51,13 +52,14 @@ 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("fileplayed-heading-label"))) usertreeRoot = self._usertreebuffer.invisibleRootItem() - + for room in rooms: roomitem = QtGui.QStandardItem(room) if room == currentUser.room: @@ -65,14 +67,15 @@ class MainWindow(QtGui.QMainWindow): font.setWeight(QtGui.QFont.Bold) roomitem.setFont(font) blankitem = QtGui.QStandardItem("") - roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable) + roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable) blankitem.setFlags(blankitem.flags() & ~Qt.ItemIsEditable) usertreeRoot.appendRow((roomitem, blankitem)) for user in rooms[room]: useritem = QtGui.QStandardItem(user.username) fileitem = QtGui.QStandardItem("") if user.file: - fileitem = QtGui.QStandardItem(u"{} ({})".format(user.file['name'], formatTime(user.file['duration']))) + fileitem = QtGui.QStandardItem( + u"{} ({})".format(user.file['name'], formatTime(user.file['duration']))) if currentUser.file: sameName = sameFilename(user.file['name'], currentUser.file['name']) sameSize = sameFilesize(user.file['size'], currentUser.file['size']) @@ -83,11 +86,17 @@ class MainWindow(QtGui.QMainWindow): 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"))) + 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"))) + 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"))) + 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))) else: @@ -98,42 +107,42 @@ class MainWindow(QtGui.QMainWindow): font = QtGui.QFont() font.setWeight(QtGui.QFont.Bold) useritem.setFont(font) - useritem.setFlags(useritem.flags() & ~Qt.ItemIsEditable) - fileitem.setFlags(fileitem.flags() & ~Qt.ItemIsEditable) + useritem.setFlags(useritem.flags() & ~Qt.ItemIsEditable) + fileitem.setFlags(fileitem.flags() & ~Qt.ItemIsEditable) roomitem.appendRow((useritem, fileitem)) - + self.listTreeModel = self._usertreebuffer self.listTreeView.setModel(self.listTreeModel) self.listTreeView.setItemsExpandable(False) self.listTreeView.expandAll() self.listTreeView.resizeColumnToContents(0) self.listTreeView.resizeColumnToContents(1) - + 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 showDebugMessage(self, message): print(message) - - def showErrorMessage(self, message, criticalerror = False): + + 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 @@ -150,45 +159,45 @@ 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 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() @@ -200,12 +209,12 @@ 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) @@ -215,7 +224,7 @@ class MainWindow(QtGui.QMainWindow): def identifyAsController(self): tooltip = "Enter controller password for this room\r\n(see http://syncplay.pl/guide/ for usage instructions):" - name = "Identify as Room Controller" + name = "Identify as Room Controller" controlpassword, ok = QtGui.QInputDialog.getText(self, name, tooltip, QtGui.QLineEdit.Normal, "") if ok and controlpassword != '': self._syncplayClient.identifyAsController(controlpassword) @@ -228,11 +237,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: @@ -241,13 +250,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/") @@ -258,8 +267,8 @@ class MainWindow(QtGui.QMainWindow): def drop(self): self.close() - - def addTopLayout(self, window): + + def addTopLayout(self, window): window.topSplit = QtGui.QSplitter(Qt.Horizontal) window.outputLayout = QtGui.QVBoxLayout() @@ -269,11 +278,11 @@ 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() @@ -283,7 +292,7 @@ class MainWindow(QtGui.QMainWindow): 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() @@ -297,13 +306,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.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() @@ -320,137 +329,149 @@ class MainWindow(QtGui.QMainWindow): 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()) - + 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()) - + 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()) - + 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("file-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'), "&Create controlled room suffix") + window.createcontrolledroomAction = window.advancedMenu.addAction( + QtGui.QIcon(self.resourcespath + 'page_white_key.png'), "&Create controlled room suffix") window.createcontrolledroomAction.triggered.connect(self.createControlledRoom) - window.identifyascontroller = window.advancedMenu.addAction(QtGui.QIcon(self.resourcespath + 'key_go.png'), "&Identify as room controller") + window.identifyascontroller = window.advancedMenu.addAction(QtGui.QIcon(self.resourcespath + 'key_go.png'), + "&Identify as room controller") 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() if urls and urls[0].scheme() == 'file': if sys.platform.startswith('win'): - dropfilepath = unicode(urls[0].path().replace("/", "\\"))[1:] # Removes starting slash + dropfilepath = unicode(urls[0].path().replace("/", "\\"))[1:] # Removes starting slash else: dropfilepath = unicode(urls[0].path()) if rewindFile == False: @@ -459,14 +480,14 @@ class MainWindow(QtGui.QMainWindow): self._syncplayClient.setPosition(0) self._syncplayClient._player.openFile(dropfilepath) 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") @@ -489,6 +510,7 @@ class MainWindow(QtGui.QMainWindow): self.addMainFrame(self) self.loadSettings() self.setWindowIcon(QtGui.QIcon(self.resourcespath + "syncplay.png")) - self.setWindowFlags(self.windowFlags() & Qt.WindowCloseButtonHint & Qt.WindowMinimizeButtonHint & ~Qt.WindowContextHelpButtonHint) + self.setWindowFlags( + self.windowFlags() & Qt.WindowCloseButtonHint & Qt.WindowMinimizeButtonHint & ~Qt.WindowContextHelpButtonHint) self.show() self.setAcceptDrops(True) \ No newline at end of file From 358109ad21e189081e70d6fd825e7fa388f728e8 Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sun, 7 Sep 2014 16:01:06 +0200 Subject: [PATCH 03/58] Made most of client-side changes regarding controlled rooms --- syncplay/client.py | 71 ++++++++++++++++++++++++++++++++++++---- syncplay/ui/consoleUI.py | 5 ++- syncplay/ui/gui.py | 3 ++ 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index 026c4b7..26c7c49 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -1,15 +1,15 @@ import hashlib import os.path +import random +import string import time -import re from twisted.internet.protocol import ClientFactory from twisted.internet import reactor, task 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, \ - PRIVACY_HIDDENFILENAME, FILENAME_STRIP_REGEX + PRIVACY_HIDDENFILENAME import collections class SyncClientFactory(ClientFactory): @@ -67,8 +67,9 @@ class SyncplayClient(object): 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 @@ -99,7 +100,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) def initProtocol(self, protocol): self._protocol = protocol @@ -234,7 +236,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) @@ -317,7 +319,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() @@ -414,6 +415,41 @@ class SyncplayClient(object): if promptForAction: self.ui.promptFor(getMessage("enter-to-exit-prompt")) + def createControlledRoom(self): + controlPassword = RoomPasswordGenerator.generate_password() + # TODO (Client): Send request to server; handle success and failure + # TODO (Server): Process request, send response + self.ui.showMessage("Attempting to create controlled room suffix with password '{}'...".format(controlPassword)) + self._protocol.requestControlledRoom(controlPassword) + + def controlledRoomCreated(self, controlPassword, roomName): + # NOTE (Client): Triggered by protocol to handle createControlledRoom when room is created + self.ui.showMessage("Created controlled room suffix '{}' with password '{}'. Please save this information for future reference!".format(roomName, controlPassword)) + self.setRoom(roomName) + self.sendRoom() + self.ui.updateRoomName(roomName) + + def controlledRoomCreationError(self, errormsg): + # NOTE (Client): Triggered by protocol to handle createControlledRoom if controlled rooms are not supported by server or if password is malformed + # NOTE (Server): Triggered by protocol to handle createControlledRoom if password is malformed + self.ui.showErrorMessage("Failed to create the controlled room suffix for the following reason: {}.".format(errormsg)) + + def identifyAsController(self, controlPassword): + # TODO (Client): Send identification to server; handle success and failure + # TODO (Server): Process request, send response + self.ui.showMessage("Identifying as room controller with password '{}'...".format(controlPassword)) + self._protocol.requestControlledRoom(controlPassword) + + def controllerIdentificationError(self, errormsg): + # NOTE (Client): Triggered by protocol handling identiedAsController, e.g. on server response or not supported error + # NOTE (Server): Relevant error given in response to identifyAsController if password is wrong + self.ui.showErrorMessage("Failed to identify as a room controller for the following reason: {}.".format(errormsg)) + + def notControllerError(self, errormsg): + # NOTE (Client): Trigger when client gets a "not controller" error from server (e.g. due to illegal pauses, unpauses and seeks) + # NOTE (Server): Give "not controller" error when users try to perform illegal pause, unpause or seek + self.ui.showErrorMessage("There are currently people with 'room controller' status in this room. As such, only they can pause, unpause and seek. If you want to perform these actions then you must either identify as a controller or join a different room. See http://syncplay.pl/guide/ for more details.") + class _WarningManager(object): def __init__(self, player, userlist, ui): self._player = player @@ -662,5 +698,26 @@ class UiManager(object): def markEndOfUserlist(self): self.__ui.markEndOfUserlist() + def updateRoomName(self, room=""): + self.__ui.updateRoomName(room) + def drop(self): self.__ui.drop() + +class RoomPasswordGenerator(object): + @staticmethod + def generate_password(): + parts = ( + RoomPasswordGenerator._get_random_letters(2), + RoomPasswordGenerator._get_random_numbers(3), + RoomPasswordGenerator._get_random_numbers(3) + ) + 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)) diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index dd2bd84..57a5981 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) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 9dab263..f0c7aa5 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -126,6 +126,9 @@ class MainWindow(QtGui.QMainWindow): def userListChange(self): self._syncplayClient.showUserList() + def updateRoomName(self, room=""): + self.roomInput.setText(room) + def showDebugMessage(self, message): print(message) From 8ba51ba1116aa5523c04273ea707d0dcb3c9c78f Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 13 Sep 2014 01:19:44 +0100 Subject: [PATCH 04/58] Fix new mpv bug by re-requesting info that was requested prematurely --- syncplay/players/mplayer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/syncplay/players/mplayer.py b/syncplay/players/mplayer.py index 36b7271..343a201 100644 --- a/syncplay/players/mplayer.py +++ b/syncplay/players/mplayer.py @@ -128,6 +128,15 @@ class MplayerPlayer(BasePlayer): if "Error parsing option" in line: self.quitReason = getMessage("mpv-version-error") + 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: return From 0b87e087ba297c21f82c777d009266731aabadd8 Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 4 Oct 2014 00:43:48 +0100 Subject: [PATCH 05/58] Initial controller + ready icon code --- resources/tick.png | Bin 0 -> 537 bytes syncplay/constants.py | 4 +++- syncplay/ui/gui.py | 50 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 resources/tick.png diff --git a/resources/tick.png b/resources/tick.png new file mode 100644 index 0000000000000000000000000000000000000000..a9925a06ab02db30c1e7ead9c701c15bc63145cb GIT binary patch literal 537 zcmV+!0_OdRP)Hs{AQG2a)rMyf zFQK~pm1x3+7!nu%-M`k}``c>^00{o_1pjWJUTfl8mg=3qGEl8H@}^@w`VUx0_$uy4 z2FhRqKX}xI*?Tv1DJd8z#F#0c%*~rM30HE1@2o5m~}ZyoWhqv>ql{V z1ZGE0lgcoK^lx+eqc*rAX1Ky;Xx3U%u#zG!m-;eD1Qsn@kf3|F9qz~|95=&g3(7!X zB}JAT>RU;a%vaNOGnJ%e1=K6eAh43c(QN8RQ6~GP%O}Jju$~Ld*%`mO1p Date: Sat, 4 Oct 2014 01:04:34 +0100 Subject: [PATCH 06/58] Add tick graphic to installer --- buildPy2exe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildPy2exe.py b/buildPy2exe.py index 164146b..1692e95 100644 --- a/buildPy2exe.py +++ b/buildPy2exe.py @@ -583,7 +583,7 @@ guiIcons = ['resources/accept.png', 'resources/arrow_undo.png', 'resources/clock '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/controller.png', 'resources/key_go.png', 'resources/page_white_key.png' + 'resources/controller.png', 'resources/key_go.png', 'resources/page_white_key.png', 'resources/tick.png' ] resources = ["resources/icon.ico", "resources/syncplay.png"] resources.extend(guiIcons) From cdce17b10b343fe2c1723bc85eb8fa39fd8e8eb3 Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sun, 5 Oct 2014 11:08:28 +0200 Subject: [PATCH 07/58] Created protocol exchanges for creating controlled rooms --- syncplay/client.py | 28 ++++++++------------- syncplay/protocols.py | 43 ++++++++++++++++++++++++++++---- syncplay/server.py | 57 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 23 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index 26c7c49..bcb504b 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -417,38 +417,30 @@ class SyncplayClient(object): def createControlledRoom(self): controlPassword = RoomPasswordGenerator.generate_password() - # TODO (Client): Send request to server; handle success and failure - # TODO (Server): Process request, send response self.ui.showMessage("Attempting to create controlled room suffix with password '{}'...".format(controlPassword)) self._protocol.requestControlledRoom(controlPassword) def controlledRoomCreated(self, controlPassword, roomName): - # NOTE (Client): Triggered by protocol to handle createControlledRoom when room is created self.ui.showMessage("Created controlled room suffix '{}' with password '{}'. Please save this information for future reference!".format(roomName, controlPassword)) self.setRoom(roomName) self.sendRoom() + self._protocol.requestControlledRoom(controlPassword) self.ui.updateRoomName(roomName) - def controlledRoomCreationError(self, errormsg): - # NOTE (Client): Triggered by protocol to handle createControlledRoom if controlled rooms are not supported by server or if password is malformed - # NOTE (Server): Triggered by protocol to handle createControlledRoom if password is malformed - self.ui.showErrorMessage("Failed to create the controlled room suffix for the following reason: {}.".format(errormsg)) - def identifyAsController(self, controlPassword): - # TODO (Client): Send identification to server; handle success and failure - # TODO (Server): Process request, send response self.ui.showMessage("Identifying as room controller with password '{}'...".format(controlPassword)) self._protocol.requestControlledRoom(controlPassword) - def controllerIdentificationError(self, errormsg): - # NOTE (Client): Triggered by protocol handling identiedAsController, e.g. on server response or not supported error - # NOTE (Server): Relevant error given in response to identifyAsController if password is wrong - self.ui.showErrorMessage("Failed to identify as a room controller for the following reason: {}.".format(errormsg)) + def controllerIdentificationError(self): + self.ui.showErrorMessage("Failed to identify as a room controller.") - def notControllerError(self, errormsg): - # NOTE (Client): Trigger when client gets a "not controller" error from server (e.g. due to illegal pauses, unpauses and seeks) - # NOTE (Server): Give "not controller" error when users try to perform illegal pause, unpause or seek - self.ui.showErrorMessage("There are currently people with 'room controller' status in this room. As such, only they can pause, unpause and seek. If you want to perform these actions then you must either identify as a controller or join a different room. See http://syncplay.pl/guide/ for more details.") + def controllerIdentificationSuccess(self): + # TODO: More UI stuff + self.ui.showErrorMessage("Authenticated as a room controller") + + # TODO: A person authenticated as a room controller + # TODO: Disable UI's "Create new Controlled Room when in Controlled Room" + # TODO: Disable authenticate when authenticated class _WarningManager(object): def __init__(self, player, userlist, ui): diff --git a/syncplay/protocols.py b/syncplay/protocols.py index 8e4e14a..a45b4be 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -114,13 +114,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() + else: + self._client.controllerIdentificationError() + elif command == "newControlledRoom": + controlPassword = values['password'] + roomName = values['roomName'] + self._client.controlledRoomCreated(controlPassword, roomName) def sendSet(self, setting): self.sendMessage({"Set": setting}) @@ -210,8 +218,15 @@ class SyncClientProtocol(JSONCommandProtocol): state["ignoringOnTheFly"]["client"] = self.clientIgnoringOnTheFly self.sendMessage({"State": state}) + def requestControlledRoom(self, password): + self.sendSet({ + "controllerAuth": { + "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}}) @@ -311,10 +326,28 @@ 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 + self._factory.authRoomController(self._watcher, password) def sendSet(self, setting): self.sendMessage({"Set": setting}) + def sendNewControlledRoom(self, roomName, password): + self.sendSet({ + "newControlledRoom": { + "password": password, + "roomName": roomName + } + }) + + def sendControlledRoomAuthStatus(self, success): + self.sendSet({ + "controllerAuth": { + "success": success + } + }) + def sendUserSetting(self, username, room, file_, event): room = {"name": room.getName()} user = {} diff --git a/syncplay/server.py b/syncplay/server.py index b349003..1776f7c 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -1,4 +1,6 @@ import hashlib +import random +import re from twisted.internet import task, reactor from twisted.internet.protocol import Factory import syncplay @@ -11,12 +13,18 @@ import os from string import Template import argparse +# TODO: Check if room should have status of "Controlled room" +# TODO: Make only controllers able to control a room +# TODO: Send list of controllers +# TODO: Broadcast information about controller auth class SyncFactory(Factory): def __init__(self, password='', motdFilePath=None, isolateRooms=False): print getMessage("welcome-server-notification").format(syncplay.version) if password: password = hashlib.md5(password).hexdigest() self.password = password + # TODO: Make salt come from more reasonable place + self._salt = str(random.random()) self._motdFilePath = motdFilePath if not isolateRooms: self._roomManager = RoomManager() @@ -98,6 +106,19 @@ class SyncFactory(Factory): def getAllWatchersForUser(self, forUser): return self._roomManager.getAllWatchersForUser(forUser) + def authRoomController(self, watcher, password): + room = watcher.getRoom() + try: + success = RoomPasswordProvider.check(room.getName(), password, self._salt) + # TODO: Authenticate watcher to make changes in the room + watcher.sendControlledRoomAuthStatus(success) + except NotControlledRoom: + newName = RoomPasswordProvider.getControlledRoomName(room.getName(), password, self._salt) + watcher.sendNewControlledRoom(newName, password) + except ValueError: + watcher.sendControlledRoomAuthStatus(False) + + class RoomManager(object): def __init__(self): self._rooms = {} @@ -277,6 +298,12 @@ class Watcher(object): def sendSetting(self, user, room, file_, event): self._connector.sendUserSetting(user, room, file_, event) + def sendNewControlledRoom(self, roomName, password): + self._connector.sendNewControlledRoom(roomName, password) + + def sendControlledRoomAuthStatus(self, success): + self._connector.sendControlledRoomAuthStatus(success) + def __lt__(self, b): if self.getPosition() is None or self._file is None: return False @@ -341,3 +368,33 @@ class ConfigurationGetter(object): 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")) + + +class RoomPasswordProvider(object): + CONTROLLED_ROOM_REGEX = re.compile("^\+(.*):(\w{12})$") + PASSWORD_REGEX = re.compile("[A-Z]{2}-\d{3}-\d{3}") + + @staticmethod + def check(roomName, password, salt): + if not re.match(RoomPasswordProvider.PASSWORD_REGEX, password): + raise ValueError() + + 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): + salt = hashlib.sha256(salt).hexdigest() + provisionalHash = hashlib.sha256(roomName + salt).hexdigest() + return hashlib.sha1(provisionalHash + salt + password).hexdigest()[:12].upper() + +class NotControlledRoom(Exception): + pass \ No newline at end of file From 9506a80b9bb869e0f7209a116884b9eb2b3c460d Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sun, 5 Oct 2014 20:05:06 +0200 Subject: [PATCH 08/58] Updated the controlled room protocol to fit more needs --- syncplay/client.py | 10 +++++----- syncplay/protocols.py | 12 ++++++------ syncplay/server.py | 9 ++++----- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index bcb504b..ba30bba 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -431,14 +431,14 @@ class SyncplayClient(object): self.ui.showMessage("Identifying as room controller with password '{}'...".format(controlPassword)) self._protocol.requestControlledRoom(controlPassword) - def controllerIdentificationError(self): - self.ui.showErrorMessage("Failed to identify as a room controller.") + def controllerIdentificationError(self, username): + self.ui.showErrorMessage("<{}> failed to identify as a room controller.".format(username)) - def controllerIdentificationSuccess(self): + def controllerIdentificationSuccess(self, username): # TODO: More UI stuff - self.ui.showErrorMessage("Authenticated as a room controller") + self.ui.showErrorMessage("<{}> authenticated as a room controller".format(username)) - # TODO: A person authenticated as a room controller + # TODO: Mark person as a room controller # TODO: Disable UI's "Create new Controlled Room when in Controlled Room" # TODO: Disable authenticate when authenticated diff --git a/syncplay/protocols.py b/syncplay/protocols.py index a45b4be..f025c16 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -122,9 +122,9 @@ class SyncClientProtocol(JSONCommandProtocol): self._SetUser(values) elif command == "controllerAuth": if values['success']: - self._client.controllerIdentificationSuccess() + self._client.controllerIdentificationSuccess(values["user"]) else: - self._client.controllerIdentificationError() + self._client.controllerIdentificationError(values["user"]) elif command == "newControlledRoom": controlPassword = values['password'] roomName = values['roomName'] @@ -341,17 +341,17 @@ class SyncServerProtocol(JSONCommandProtocol): } }) - def sendControlledRoomAuthStatus(self, success): + def sendControlledRoomAuthStatus(self, success, username): self.sendSet({ "controllerAuth": { + "user": username, "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_ @@ -388,7 +388,7 @@ class SyncServerProtocol(JSONCommandProtocol): "paused": paused, "doSeek": doSeek, "setBy": setBy.getName() - } + } ping = { "latencyCalculation": self._pingService.newTimestamp(), "serverRtt": self._pingService.getRtt() diff --git a/syncplay/server.py b/syncplay/server.py index 1776f7c..173d06f 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -110,13 +110,12 @@ class SyncFactory(Factory): room = watcher.getRoom() try: success = RoomPasswordProvider.check(room.getName(), password, self._salt) - # TODO: Authenticate watcher to make changes in the room - watcher.sendControlledRoomAuthStatus(success) + self._roomManager.broadcastRoom(watcher, lambda w: w.sendControlledRoomAuthStatus(success, watcher.getName())) except NotControlledRoom: newName = RoomPasswordProvider.getControlledRoomName(room.getName(), password, self._salt) watcher.sendNewControlledRoom(newName, password) except ValueError: - watcher.sendControlledRoomAuthStatus(False) + self._roomManager.broadcastRoom(watcher, lambda w: w.sendControlledRoomAuthStatus(False, watcher.getName())) class RoomManager(object): @@ -301,8 +300,8 @@ class Watcher(object): def sendNewControlledRoom(self, roomName, password): self._connector.sendNewControlledRoom(roomName, password) - def sendControlledRoomAuthStatus(self, success): - self._connector.sendControlledRoomAuthStatus(success) + def sendControlledRoomAuthStatus(self, success, username): + self._connector.sendControlledRoomAuthStatus(success, username) def __lt__(self, b): if self.getPosition() is None or self._file is None: From 8072fca28f3e1a05a6db3ccc402448f0a9bb73e8 Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sun, 5 Oct 2014 21:01:34 +0200 Subject: [PATCH 09/58] Fixed comparing watchers in server with a rare case --- syncplay/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncplay/server.py b/syncplay/server.py index 173d06f..e2ceea3 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -306,7 +306,7 @@ class Watcher(object): 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() From 241f5e75bbd33ad319e11c5839bc846a3dec4478 Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sun, 5 Oct 2014 21:04:22 +0200 Subject: [PATCH 10/58] Removed unused password argument --- syncplay/protocols.py | 9 ++++----- syncplay/server.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/syncplay/protocols.py b/syncplay/protocols.py index f025c16..0ccca71 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -268,7 +268,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 @@ -276,9 +276,8 @@ 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 + return username, serverPassword, roomName, version def _checkPassword(self, serverPassword): if self._factory.password: @@ -291,7 +290,7 @@ 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]: @@ -299,7 +298,7 @@ class SyncServerProtocol(JSONCommandProtocol): 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) diff --git a/syncplay/server.py b/syncplay/server.py index e2ceea3..91a066c 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -62,7 +62,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) From ec74daf9633f1b0a943da99b0fc286e945bb21ab Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sun, 5 Oct 2014 21:05:53 +0200 Subject: [PATCH 11/58] Fixed more unused arguments --- syncplay/server.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/syncplay/server.py b/syncplay/server.py index 91a066c..c88c9b9 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -91,11 +91,11 @@ 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): room = watcher.getRoom() paused, position = room.isPaused(), watcher.getPosition() setBy = watcher @@ -263,7 +263,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 @@ -312,7 +312,7 @@ class Watcher(object): 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) @@ -349,7 +349,7 @@ class Watcher(object): position += messageAge self.setPosition(position) if doSeek or pauseChanged: - self._server.forcePositionUpdate(self._room, self, doSeek) + self._server.forcePositionUpdate(self, doSeek) class ConfigurationGetter(object): From 51e2e422935af34e085c4516d6d5d1680c6a6117 Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sat, 11 Oct 2014 22:06:54 +0200 Subject: [PATCH 12/58] Completed "backend" for controlled rooms --- syncplay/client.py | 2 +- syncplay/protocols.py | 2 +- syncplay/server.py | 78 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index ba30bba..b5ecd9b 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -225,7 +225,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) diff --git a/syncplay/protocols.py b/syncplay/protocols.py index 0ccca71..ce18661 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -386,7 +386,7 @@ 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(), diff --git a/syncplay/server.py b/syncplay/server.py index c88c9b9..d87a48e 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -13,10 +13,6 @@ import os from string import Template import argparse -# TODO: Check if room should have status of "Controlled room" -# TODO: Make only controllers able to control a room -# TODO: Send list of controllers -# TODO: Broadcast information about controller auth class SyncFactory(Factory): def __init__(self, password='', motdFilePath=None, isolateRooms=False): print getMessage("welcome-server-notification").format(syncplay.version) @@ -73,6 +69,9 @@ class SyncFactory(Factory): self.sendJoinMessage(watcher) else: self.sendRoomSwitchMessage(watcher) + if RoomPasswordProvider.isControlledRoom(roomName): + for controller in watcher.getRoom().getControllers(): + watcher.sendControlledRoomAuthStatus(True, controller) def sendRoomSwitchMessage(self, watcher): l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None) @@ -97,11 +96,14 @@ class SyncFactory(Factory): def forcePositionUpdate(self, watcher, doSeek): 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(), room.isPaused(), doSeek, room.getSetBy(), True) def getAllWatchersForUser(self, forUser): return self._roomManager.getAllWatchersForUser(forUser) @@ -110,6 +112,8 @@ class SyncFactory(Factory): room = watcher.getRoom() try: success = RoomPasswordProvider.check(room.getName(), password, self._salt) + if success: + watcher.getRoom().addController(watcher) self._roomManager.broadcastRoom(watcher, lambda w: w.sendControlledRoomAuthStatus(success, watcher.getName())) except NotControlledRoom: newName = RoomPasswordProvider.getControlledRoomName(room.getName(), password, self._salt) @@ -155,7 +159,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 @@ -248,6 +255,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 @@ -339,14 +384,18 @@ 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, doSeek) @@ -368,11 +417,14 @@ class ConfigurationGetter(object): 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")) - 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 re.match(RoomPasswordProvider.PASSWORD_REGEX, password): From feef564d40298d0c14be7e4393c116e890a068f2 Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sat, 11 Oct 2014 22:45:52 +0200 Subject: [PATCH 13/58] Changed the version and version mismatch disconnect behavior --- syncplay/__init__.py | 4 ++-- syncplay/messages.py | 2 +- syncplay/protocols.py | 6 ++++-- syncplay/server.py | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) 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/messages.py b/syncplay/messages.py index 5632461..8efbdb3 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -270,7 +270,7 @@ 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" + "version-mismatch-server-error" : "Mismatch between versions of client and server, please update your client or server software. Current client version is {}, while the server version: {}" } diff --git a/syncplay/protocols.py b/syncplay/protocols.py index ce18661..2c0b34d 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -293,14 +293,16 @@ class SyncServerProtocol(JSONCommandProtocol): username, serverPassword, roomName, version = self._extractHelloArguments(hello) if not username or not roomName or not version: self.dropWithError(getMessage("hello-server-error")) + return elif version.split(".")[0:2] != syncplay.version.split(".")[0:2]: - self.dropWithError(getMessage("version-mismatch-server-error")) + self.dropWithError(getMessage("version-mismatch-server-error").format(version, syncplay.version)) + return else: if not self._checkPassword(serverPassword): return self._factory.addWatcher(self, username, roomName) self._logged = True - self.sendHello(version) + self.sendHello(syncplay.version) def setWatcher(self, watcher): self._watcher = watcher diff --git a/syncplay/server.py b/syncplay/server.py index d87a48e..8c0d2e3 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -78,7 +78,7 @@ class SyncFactory(Factory): self._roomManager.broadcast(watcher, l) def removeWatcher(self, watcher): - if watcher.getRoom(): + if watcher and watcher.getRoom(): self.sendLeftMessage(watcher) self._roomManager.removeWatcher(watcher) From ea119fca79bd03e08b449cd1ce0de29db569a91c Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sun, 12 Oct 2014 20:26:04 +0200 Subject: [PATCH 14/58] Fix the BC with 1.2.9 --- syncplay/protocols.py | 7 ++----- syncplay/server.py | 7 ++++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/syncplay/protocols.py b/syncplay/protocols.py index 2c0b34d..68e4095 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -294,15 +294,12 @@ class SyncServerProtocol(JSONCommandProtocol): if not username or not roomName or not version: self.dropWithError(getMessage("hello-server-error")) return - elif version.split(".")[0:2] != syncplay.version.split(".")[0:2]: - self.dropWithError(getMessage("version-mismatch-server-error").format(version, syncplay.version)) - return else: if not self._checkPassword(serverPassword): return self._factory.addWatcher(self, username, roomName) self._logged = True - self.sendHello(syncplay.version) + self.sendHello(version) def setWatcher(self, watcher): self._watcher = watcher @@ -314,7 +311,7 @@ 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 # syncplay.version - Don't BC with 1.2.x hello["motd"] = self._factory.getMotd(userIp, username, room, clientVersion) self.sendMessage({"Hello": hello}) diff --git a/syncplay/server.py b/syncplay/server.py index 8c0d2e3..7697397 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -94,7 +94,7 @@ class SyncFactory(Factory): l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), watcher.getFile(), None) self._roomManager.broadcast(watcher, l) - def forcePositionUpdate(self, watcher, doSeek): + def forcePositionUpdate(self, watcher, doSeek, watcherPauseState): room = watcher.getRoom() if room.canControl(watcher): paused, position = room.isPaused(), watcher.getPosition() @@ -103,7 +103,8 @@ class SyncFactory(Factory): room.setPosition(watcher.getPosition(), setBy) self._roomManager.broadcastRoom(watcher, l) else: - watcher.sendState(room.getPosition(), room.isPaused(), doSeek, room.getSetBy(), True) + 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) @@ -398,7 +399,7 @@ class Watcher(object): position = self._updatePositionByAge(messageAge, paused, position) self.setPosition(position) if doSeek or pauseChanged: - self._server.forcePositionUpdate(self, doSeek) + self._server.forcePositionUpdate(self, doSeek, paused) class ConfigurationGetter(object): From a28431340ec64e69c039af3346e8da4b05965843 Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sun, 12 Oct 2014 20:39:22 +0200 Subject: [PATCH 15/58] Add method "isController" to the user --- syncplay/client.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/syncplay/client.py b/syncplay/client.py index b5ecd9b..6110ffd 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -435,6 +435,7 @@ class SyncplayClient(object): self.ui.showErrorMessage("<{}> failed to identify as a room controller.".format(username)) def controllerIdentificationSuccess(self, username): + self.userlist.setUserAsController(username) # TODO: More UI stuff self.ui.showErrorMessage("<{}> authenticated as a room controller".format(username)) @@ -494,6 +495,7 @@ class SyncplayUser(object): self.username = username self.room = room self.file = file_ + self._controller = False def setFile(self, filename, duration, size): file_ = { @@ -520,6 +522,12 @@ class SyncplayUser(object): else: return "{}".format(self.username) + def setAsController(self): + self._controller = True + + def isController(self): + return self._controller + class SyncplayUserlist(object): def __init__(self, ui, client): self.currentUser = SyncplayUser() @@ -610,6 +618,11 @@ class SyncplayUserlist(object): self.addUser(username, room, file_) self.userListChange() + def setUserAsController(self, username): + if self._users.has_key(username): + user = self._users[username] + user.setAsController() + 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): From 778fb21d0b7815753c88094f7301a1a0bddf5669 Mon Sep 17 00:00:00 2001 From: Et0h Date: Sun, 12 Oct 2014 20:45:53 +0100 Subject: [PATCH 16/58] Move RoomPasswordProvider to utils --- syncplay/server.py | 34 +--------------------------------- syncplay/utils.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/syncplay/server.py b/syncplay/server.py index 7697397..e576eb7 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -12,6 +12,7 @@ import codecs import os from string import Template import argparse +from syncplay.utils import RoomPasswordProvider, NotControlledRoom class SyncFactory(Factory): def __init__(self, password='', motdFilePath=None, isolateRooms=False): @@ -417,36 +418,3 @@ class ConfigurationGetter(object): 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")) - -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 re.match(RoomPasswordProvider.PASSWORD_REGEX, password): - raise ValueError() - - 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): - salt = hashlib.sha256(salt).hexdigest() - provisionalHash = hashlib.sha256(roomName + salt).hexdigest() - return hashlib.sha1(provisionalHash + salt + password).hexdigest()[:12].upper() - -class NotControlledRoom(Exception): - pass \ No newline at end of file diff --git a/syncplay/utils.py b/syncplay/utils.py index 40bf3d2..cfd6eca 100644 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -182,3 +182,36 @@ def sameFileduration (duration1, duration2): return True else: return False + +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 re.match(RoomPasswordProvider.PASSWORD_REGEX, password): + raise ValueError() + + 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): + salt = hashlib.sha256(salt).hexdigest() + provisionalHash = hashlib.sha256(roomName + salt).hexdigest() + return hashlib.sha1(provisionalHash + salt + password).hexdigest()[:12].upper() + +class NotControlledRoom(Exception): + pass \ No newline at end of file From 7032a9634c72346af990204d42d2e49aa921becc Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sun, 12 Oct 2014 20:51:00 +0200 Subject: [PATCH 17/58] Remove droping on client side due to version mismatch --- syncplay/messages.py | 3 --- syncplay/protocols.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/syncplay/messages.py b/syncplay/messages.py index 8efbdb3..35ae499 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -270,9 +270,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, please update your client or server software. Current client version is {}, while the server version: {}" - - } pl = { diff --git a/syncplay/protocols.py b/syncplay/protocols.py index 68e4095..e56d457 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -78,8 +78,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) From e88863e2ac2ec5d6aa1285be2776b6e47141961b Mon Sep 17 00:00:00 2001 From: Uriziel Date: Sun, 12 Oct 2014 22:07:01 +0200 Subject: [PATCH 18/58] Added sending the "controller" status with a list --- syncplay/client.py | 4 +++- syncplay/protocols.py | 9 +++++++-- syncplay/server.py | 4 +++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index 6110ffd..2833fe0 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -574,10 +574,12 @@ 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=False): if username == self.currentUser.username: return user = SyncplayUser(username, room, file_) + if isController: + user.setAsController() self._users[username] = user if not noMessage: self.__showUserChangeMessage(username, room, file_) diff --git a/syncplay/protocols.py b/syncplay/protocols.py index e56d457..36d15b8 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -148,7 +148,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): @@ -360,7 +361,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): diff --git a/syncplay/server.py b/syncplay/server.py index e576eb7..2d79067 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -1,6 +1,5 @@ import hashlib import random -import re from twisted.internet import task, reactor from twisted.internet.protocol import Factory import syncplay @@ -402,6 +401,9 @@ class Watcher(object): if doSeek or pauseChanged: 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): From 64d78637a5bc52fd0aadcace13f9e4a29c022217 Mon Sep 17 00:00:00 2001 From: Et0h Date: Wed, 15 Oct 2014 00:53:12 +0100 Subject: [PATCH 19/58] Improve controller UI code --- syncplay/client.py | 41 +++++++++++++++++++++++++--------------- syncplay/protocols.py | 7 ++++--- syncplay/server.py | 10 +++++----- syncplay/ui/consoleUI.py | 5 ++++- syncplay/ui/gui.py | 29 ++++++++++++++++------------ syncplay/utils.py | 1 + 6 files changed, 57 insertions(+), 36 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index 2833fe0..8854850 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -417,27 +417,29 @@ class SyncplayClient(object): def createControlledRoom(self): controlPassword = RoomPasswordGenerator.generate_password() - self.ui.showMessage("Attempting to create controlled room suffix with password '{}'...".format(controlPassword)) + self.ui.showMessage(u"Attempting to create controlled room suffix with password '{}'...".format(controlPassword)) self._protocol.requestControlledRoom(controlPassword) def controlledRoomCreated(self, controlPassword, roomName): - self.ui.showMessage("Created controlled room suffix '{}' with password '{}'. Please save this information for future reference!".format(roomName, controlPassword)) + self.ui.showMessage(u"Created controlled room suffix '{}' with password '{}'. Please save this information for future reference!".format(roomName, controlPassword)) self.setRoom(roomName) self.sendRoom() self._protocol.requestControlledRoom(controlPassword) self.ui.updateRoomName(roomName) def identifyAsController(self, controlPassword): - self.ui.showMessage("Identifying as room controller with password '{}'...".format(controlPassword)) + self.ui.showMessage(u"Identifying as room controller with password '{}'...".format(controlPassword)) self._protocol.requestControlledRoom(controlPassword) - def controllerIdentificationError(self, username): - self.ui.showErrorMessage("<{}> failed to identify as a room controller.".format(username)) + def controllerIdentificationError(self, username, room): + self.ui.showErrorMessage(u"<{}> failed to identify as a room controller.".format(username)) - def controllerIdentificationSuccess(self, username): + def controllerIdentificationSuccess(self, username, roomname): self.userlist.setUserAsController(username) - # TODO: More UI stuff - self.ui.showErrorMessage("<{}> authenticated as a room controller".format(username)) + self.ui.setControllerStatus(username, isController=True) + if self.userlist.isRoomSame(roomname): + hideFromOSD = not constants.SHOW_SAME_ROOM_OSD + self.ui.showMessage(u"<{}> authenticated as a room controller".format(username), hideFromOSD) # TODO: Mark person as a room controller # TODO: Disable UI's "Create new Controlled Room when in Controlled Room" @@ -522,8 +524,8 @@ class SyncplayUser(object): else: return "{}".format(self.username) - def setAsController(self): - self._controller = True + def setControllerStatus(self, isController): + self._controller = isController def isController(self): return self._controller @@ -574,12 +576,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, isController=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: - user.setAsController() + if isController is not None: + user.setControllerStatus(isController) self._users[username] = user if not noMessage: self.__showUserChangeMessage(username, room, file_) @@ -610,6 +614,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_: @@ -621,9 +627,11 @@ class SyncplayUserlist(object): self.userListChange() def setUserAsController(self, username): - if self._users.has_key(username): + if self.currentUser.username == username: + self.currentUser.setControllerStatus(True) + elif self._users.has_key(username): user = self._users[username] - user.setAsController() + user.setControllerStatus(True) def areAllFilesInRoomSame(self): for user in self._users.itervalues(): @@ -691,6 +699,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 diff --git a/syncplay/protocols.py b/syncplay/protocols.py index 36d15b8..2fa8cdf 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -120,9 +120,9 @@ class SyncClientProtocol(JSONCommandProtocol): self._SetUser(values) elif command == "controllerAuth": if values['success']: - self._client.controllerIdentificationSuccess(values["user"]) + self._client.controllerIdentificationSuccess(values["user"], values["room"]) else: - self._client.controllerIdentificationError(values["user"]) + self._client.controllerIdentificationError(values["user"], values["room"]) elif command == "newControlledRoom": controlPassword = values['password'] roomName = values['roomName'] @@ -338,10 +338,11 @@ class SyncServerProtocol(JSONCommandProtocol): } }) - def sendControlledRoomAuthStatus(self, success, username): + def sendControlledRoomAuthStatus(self, success, username, roomname): self.sendSet({ "controllerAuth": { "user": username, + "room": roomname, "success": success } }) diff --git a/syncplay/server.py b/syncplay/server.py index 2d79067..4b1699a 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -71,7 +71,7 @@ class SyncFactory(Factory): self.sendRoomSwitchMessage(watcher) if RoomPasswordProvider.isControlledRoom(roomName): for controller in watcher.getRoom().getControllers(): - watcher.sendControlledRoomAuthStatus(True, controller) + watcher.sendControlledRoomAuthStatus(True, controller, roomName) def sendRoomSwitchMessage(self, watcher): l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None) @@ -115,12 +115,12 @@ class SyncFactory(Factory): success = RoomPasswordProvider.check(room.getName(), password, self._salt) if success: watcher.getRoom().addController(watcher) - self._roomManager.broadcastRoom(watcher, lambda w: w.sendControlledRoomAuthStatus(success, watcher.getName())) + self._roomManager.broadcast(watcher, lambda w: w.sendControlledRoomAuthStatus(success, watcher.getName(), room._name)) except NotControlledRoom: newName = RoomPasswordProvider.getControlledRoomName(room.getName(), password, self._salt) watcher.sendNewControlledRoom(newName, password) except ValueError: - self._roomManager.broadcastRoom(watcher, lambda w: w.sendControlledRoomAuthStatus(False, watcher.getName())) + self._roomManager.broadcastRoom(watcher, lambda w: w.sendControlledRoomAuthStatus(False, watcher.getName(), room._name)) class RoomManager(object): @@ -346,8 +346,8 @@ class Watcher(object): def sendNewControlledRoom(self, roomName, password): self._connector.sendNewControlledRoom(roomName, password) - def sendControlledRoomAuthStatus(self, success, username): - self._connector.sendControlledRoomAuthStatus(success, username) + 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: diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index 57a5981..c7d10e3 100644 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -39,6 +39,9 @@ class ConsoleUI(threading.Thread): def updateRoomName(self, room=""): pass + def setControllerStatus(self, username, isController): + pass + def promptFor(self, prompt=">", message=""): if message <> "": print(message) @@ -77,7 +80,7 @@ class ConsoleUI(threading.Thread): def showDebugMessage(self, message): print(message) - + def showErrorMessage(self, message, criticalerror = False): print("ERROR:\t" + message) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 41bee36..2c1f718 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -6,7 +6,7 @@ import sys import time import re import os -from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration +from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration, RoomPasswordProvider class UserlistItemDelegate(QtGui.QStyledItemDelegate): def __init__(self): @@ -79,6 +79,19 @@ class MainWindow(QtGui.QMainWindow): # TODO: Prompt user return None + def setControllerStatus(self, username, isController): + roomtocheck = 0 + while self.listTreeModel.item(roomtocheck): + itemtocheck = 0 + while self.listTreeModel.item(roomtocheck).child(itemtocheck): + item = self.listTreeModel.item(roomtocheck).child(itemtocheck) + if item.data(0) == username: + item.setData(isController, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE) + return + itemtocheck = itemtocheck + 1 + roomtocheck += 1 + self.listTreeView.resizeColumnToContents(True) + def showMessage(self, message, noTimestamp=False): message = unicode(message) message = message.replace("&", "&").replace('"', """).replace("<", "<").replace(">", ">") @@ -107,21 +120,13 @@ class MainWindow(QtGui.QMainWindow): roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable) blankitem.setFlags(blankitem.flags() & ~Qt.ItemIsEditable) usertreeRoot.appendRow((roomitem, blankitem)) - #usercounter = 0 # UNCOMMENT FOR DEMONSTRATION for user in rooms[room]: useritem = QtGui.QStandardItem(user.username) - '''# UNCOMMENT FOR DEMONSTRATION - usercounter = usercounter + 1 - - if usercounter == 1: - useritem.setData(True, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE) - useritem.setData(True, Qt.UserRole + constants.USERITEM_READY_ROLE) - if usercounter == 2: - useritem.setData(False, Qt.UserRole + constants.USERITEM_READY_ROLE)l''' + 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']))) + fileitem = QtGui.QStandardItem(u"{} ({})".format(user.file['name'], formatTime(user.file['duration']))) if currentUser.file: sameName = sameFilename(user.file['name'], currentUser.file['name']) sameSize = sameFilesize(user.file['size'], currentUser.file['size']) diff --git a/syncplay/utils.py b/syncplay/utils.py index cfd6eca..16debcf 100644 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -209,6 +209,7 @@ class RoomPasswordProvider(object): @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() From 94ce0221b82a675210ef6c4f121652c8077e7cb8 Mon Sep 17 00:00:00 2001 From: Et0h Date: Wed, 15 Oct 2014 01:55:44 +0100 Subject: [PATCH 20/58] Don't die on empty identify password --- syncplay/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncplay/utils.py b/syncplay/utils.py index 16debcf..4a611ea 100644 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -193,7 +193,7 @@ class RoomPasswordProvider(object): @staticmethod def check(roomName, password, salt): - if not re.match(RoomPasswordProvider.PASSWORD_REGEX, password): + if not password or not re.match(RoomPasswordProvider.PASSWORD_REGEX, password): raise ValueError() match = re.match(RoomPasswordProvider.CONTROLLED_ROOM_REGEX, roomName) From 68d163e476e176cc183e8ee2eb11c1760c76bf36 Mon Sep 17 00:00:00 2001 From: Et0h Date: Wed, 15 Oct 2014 02:07:33 +0100 Subject: [PATCH 21/58] Add console support for controlled rooms --- syncplay/constants.py | 2 ++ syncplay/messages.py | 2 ++ syncplay/ui/consoleUI.py | 14 ++++++++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/syncplay/constants.py b/syncplay/constants.py index facce07..12545e5 100644 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -45,6 +45,8 @@ COMMANDS_LIST = ["l", "list", "users"] COMMANDS_PAUSE = ["p", "play", "pause"] COMMANDS_ROOM = ["r", "room"] COMMANDS_HELP = ['help', 'h', '?', '/?', r'\?'] +COMMANDS_CREATE = ['c','create'] +COMMANDS_IDENTIFY = ['i','identify'] MPC_MIN_VER = "1.6.4" VLC_MIN_VERSION = "2.0.0" VLC_INTERFACE_MIN_VERSION = "0.2.1" diff --git a/syncplay/messages.py b/syncplay/messages.py index 35ae499..23ed522 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -53,6 +53,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 - create controlled room using name of current room", + "commandlist-notification/identify" : "\ti [password] - identify as room controller with controller password", "syncplay-version-notification" : "Syncplay version: {}", # syncplay.version "more-info-notification" : "More info available at: {}", # projectURL diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index c7d10e3..8a60c68 100644 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -55,14 +55,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) @@ -140,6 +143,11 @@ class ConsoleUI(threading.Thread): self._syncplayClient.setRoom(room) self._syncplayClient.sendRoom() + elif command.group('command') in constants.COMMANDS_CREATE: + self._syncplayClient.createControlledRoom() + elif command.group('command') in constants.COMMANDS_IDENTIFY: + controlpassword = command.group('parameter') + self._syncplayClient.identifyAsController(controlpassword) else: if self._tryAdvancedCommands(data): return @@ -152,6 +160,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/identify"), True) self.showMessage(getMessage("syncplay-version-notification").format(syncplay.version), True) self.showMessage(getMessage("more-info-notification").format(syncplay.projectURL), True) From a06b691f297325ac35b18fff0b4b89232ecd8bd6 Mon Sep 17 00:00:00 2001 From: Et0h Date: Fri, 17 Oct 2014 15:39:22 +0100 Subject: [PATCH 22/58] Rename 'i'/'identify' to 'auth' to avoid confusion with 'l'/'list' --- syncplay/constants.py | 2 +- syncplay/messages.py | 2 +- syncplay/ui/consoleUI.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/syncplay/constants.py b/syncplay/constants.py index 12545e5..0e6b66d 100644 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -46,7 +46,7 @@ COMMANDS_PAUSE = ["p", "play", "pause"] COMMANDS_ROOM = ["r", "room"] COMMANDS_HELP = ['help', 'h', '?', '/?', r'\?'] COMMANDS_CREATE = ['c','create'] -COMMANDS_IDENTIFY = ['i','identify'] +COMMANDS_AUTH = ['a','auth'] MPC_MIN_VER = "1.6.4" VLC_MIN_VERSION = "2.0.0" VLC_INTERFACE_MIN_VERSION = "0.2.1" diff --git a/syncplay/messages.py b/syncplay/messages.py index 23ed522..77a7d07 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -54,7 +54,7 @@ en = { "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 - create controlled room using name of current room", - "commandlist-notification/identify" : "\ti [password] - identify as room controller with controller password", + "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 diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index 8a60c68..6af11b6 100644 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -145,7 +145,7 @@ class ConsoleUI(threading.Thread): self._syncplayClient.sendRoom() elif command.group('command') in constants.COMMANDS_CREATE: self._syncplayClient.createControlledRoom() - elif command.group('command') in constants.COMMANDS_IDENTIFY: + elif command.group('command') in constants.COMMANDS_AUTH: controlpassword = command.group('parameter') self._syncplayClient.identifyAsController(controlpassword) else: @@ -161,7 +161,7 @@ class ConsoleUI(threading.Thread): 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/identify"), 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) From cbf509b057970ff5e9b8cbd2a9cb67e52619bfa9 Mon Sep 17 00:00:00 2001 From: Et0h Date: Fri, 17 Oct 2014 16:45:50 +0100 Subject: [PATCH 23/58] Strip improper chars from control pass auth --- syncplay/client.py | 8 ++++++++ syncplay/constants.py | 1 + 2 files changed, 9 insertions(+) diff --git a/syncplay/client.py b/syncplay/client.py index 8854850..821d08b 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -3,6 +3,7 @@ import os.path import random import string import time +import re from twisted.internet.protocol import ClientFactory from twisted.internet import reactor, task from syncplay.protocols import SyncClientProtocol @@ -427,7 +428,14 @@ class SyncplayClient(object): self._protocol.requestControlledRoom(controlPassword) self.ui.updateRoomName(roomName) + def stripControlPassword(self, controlPassword): + if controlPassword: + return re.sub(constants.CONTROL_PASSWORD_STRIP_REGEX, "", controlPassword).upper() + else: + return "" + def identifyAsController(self, controlPassword): + controlPassword = self.stripControlPassword(controlPassword) self.ui.showMessage(u"Identifying as room controller with password '{}'...".format(controlPassword)) self._protocol.requestControlledRoom(controlPassword) diff --git a/syncplay/constants.py b/syncplay/constants.py index 0e6b66d..c2473ad 100644 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -40,6 +40,7 @@ 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\-]" COMMANDS_UNDO = ["u", "undo", "revert"] COMMANDS_LIST = ["l", "list", "users"] COMMANDS_PAUSE = ["p", "play", "pause"] From b47c8a00b36e49fa0d08c27b5dee1d1ef8b7cd21 Mon Sep 17 00:00:00 2001 From: Et0h Date: Mon, 20 Oct 2014 23:44:47 +0100 Subject: [PATCH 24/58] Allow user to specify name of controlled room --- syncplay/client.py | 14 +++++++------- syncplay/constants.py | 1 + syncplay/messages.py | 2 +- syncplay/protocols.py | 8 +++++--- syncplay/server.py | 11 ++++++----- syncplay/ui/consoleUI.py | 6 +++++- syncplay/ui/gui.py | 6 +++++- syncplay/utils.py | 16 +++++++++++++++- 8 files changed, 45 insertions(+), 19 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index 821d08b..0e2c50f 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -416,16 +416,16 @@ class SyncplayClient(object): if promptForAction: self.ui.promptFor(getMessage("enter-to-exit-prompt")) - def createControlledRoom(self): + def createControlledRoom(self, roomName): controlPassword = RoomPasswordGenerator.generate_password() - self.ui.showMessage(u"Attempting to create controlled room suffix with password '{}'...".format(controlPassword)) - self._protocol.requestControlledRoom(controlPassword) + self.ui.showMessage(u"Attempting to create controlled room '{}' with password '{}'...".format(roomName, controlPassword)) + self._protocol.requestControlledRoom(roomName, controlPassword) - def controlledRoomCreated(self, controlPassword, roomName): - self.ui.showMessage(u"Created controlled room suffix '{}' with password '{}'. Please save this information for future reference!".format(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(controlPassword) + self._protocol.requestControlledRoom(roomName, controlPassword) self.ui.updateRoomName(roomName) def stripControlPassword(self, controlPassword): @@ -437,7 +437,7 @@ class SyncplayClient(object): def identifyAsController(self, controlPassword): controlPassword = self.stripControlPassword(controlPassword) self.ui.showMessage(u"Identifying as room controller with password '{}'...".format(controlPassword)) - self._protocol.requestControlledRoom(controlPassword) + self._protocol.requestControlledRoom(self.getRoom(), controlPassword) def controllerIdentificationError(self, username, room): self.ui.showErrorMessage(u"<{}> failed to identify as a room controller.".format(username)) diff --git a/syncplay/constants.py b/syncplay/constants.py index c2473ad..9bd0065 100644 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -41,6 +41,7 @@ SYNC_ON_PAUSE = True # Client seek to global position - subtitles may disappear #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"] diff --git a/syncplay/messages.py b/syncplay/messages.py index 77a7d07..187ae16 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -53,7 +53,7 @@ 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 - create controlled room using name of current room", + "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 diff --git a/syncplay/protocols.py b/syncplay/protocols.py index 2fa8cdf..67b6026 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -126,7 +126,7 @@ class SyncClientProtocol(JSONCommandProtocol): elif command == "newControlledRoom": controlPassword = values['password'] roomName = values['roomName'] - self._client.controlledRoomCreated(controlPassword, roomName) + self._client.controlledRoomCreated(roomName, controlPassword) def sendSet(self, setting): self.sendMessage({"Set": setting}) @@ -217,9 +217,10 @@ class SyncClientProtocol(JSONCommandProtocol): state["ignoringOnTheFly"]["client"] = self.clientIgnoringOnTheFly self.sendMessage({"State": state}) - def requestControlledRoom(self, password): + def requestControlledRoom(self, room, password): self.sendSet({ "controllerAuth": { + "room": room, "password": password } }) @@ -325,7 +326,8 @@ class SyncServerProtocol(JSONCommandProtocol): self._watcher.setFile(set_[1]) elif command == "controllerAuth": password = set_[1]["password"] if set_[1].has_key("password") else None - self._factory.authRoomController(self._watcher, password) + 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}) diff --git a/syncplay/server.py b/syncplay/server.py index 4b1699a..f586be3 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -109,15 +109,16 @@ class SyncFactory(Factory): def getAllWatchersForUser(self, forUser): return self._roomManager.getAllWatchersForUser(forUser) - def authRoomController(self, watcher, password): + def authRoomController(self, watcher, password, roomBaseName=None): room = watcher.getRoom() + roomName = roomBaseName if roomBaseName else room.getName() try: - success = RoomPasswordProvider.check(room.getName(), password, self._salt) + 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(room.getName(), password, self._salt) + 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)) @@ -343,8 +344,8 @@ class Watcher(object): def sendSetting(self, user, room, file_, event): self._connector.sendUserSetting(user, room, file_, event) - def sendNewControlledRoom(self, roomName, password): - self._connector.sendNewControlledRoom(roomName, password) + def sendNewControlledRoom(self, roomBaseName, password): + self._connector.sendNewControlledRoom(roomBaseName, password) def sendControlledRoomAuthStatus(self, success, username, room): self._connector.sendControlledRoomAuthStatus(success, username, room) diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index 6af11b6..70acede 100644 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -144,7 +144,11 @@ class ConsoleUI(threading.Thread): self._syncplayClient.setRoom(room) self._syncplayClient.sendRoom() elif command.group('command') in constants.COMMANDS_CREATE: - self._syncplayClient.createControlledRoom() + 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) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 2c1f718..db015a4 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -275,7 +275,11 @@ class MainWindow(QtGui.QMainWindow): self._syncplayClient._player.openFile(fileName) def createControlledRoom(self): - self._syncplayClient.createControlledRoom() + controlroom, ok = QtGui.QInputDialog.getText(self, "Create controlled room", + "Enter name of controlled room\r\n(see http://syncplay.pl/guide/ for usage instructions):", QtGui.QLineEdit.Normal, + utils.stripRoomName(self._syncplayClient.getRoom())) + if ok and controlroom != '': + self._syncplayClient.createControlledRoom(controlroom) def identifyAsController(self): tooltip = "Enter controller password for this room\r\n(see http://syncplay.pl/guide/ for usage instructions):" diff --git a/syncplay/utils.py b/syncplay/utils.py index 4a611ea..5d6728f 100644 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -141,7 +141,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] @@ -196,6 +208,8 @@ class RoomPasswordProvider(object): 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() From aaac02f6fceaee53c8727d73b5aa34b14bb1bc9d Mon Sep 17 00:00:00 2001 From: Uriziel Date: Tue, 21 Oct 2014 23:21:30 +0200 Subject: [PATCH 25/58] Fixed mpv problem with unicode filenames (hack?) --- syncplay/players/mplayer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/syncplay/players/mplayer.py b/syncplay/players/mplayer.py index 343a201..aca166e 100644 --- a/syncplay/players/mplayer.py +++ b/syncplay/players/mplayer.py @@ -277,7 +277,9 @@ class MplayerPlayer(BasePlayer): def sendLine(self, line): try: - line = (line.decode('utf8') + u"\n").encode('utf8') + if not isinstance(line, unicode): + line = line.decode('utf8') + line = (line + u"\n").encode('utf8') self.__process.stdin.write(line) except IOError: pass From fc5f1e62c93ff391d21e86cacf53b5e2b89577f5 Mon Sep 17 00:00:00 2001 From: Et0h Date: Tue, 21 Oct 2014 23:29:20 +0100 Subject: [PATCH 26/58] Properly update userlist on auth --- syncplay/client.py | 2 +- syncplay/ui/consoleUI.py | 3 --- syncplay/ui/gui.py | 13 ------------- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index 0e2c50f..6b786e5 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -444,10 +444,10 @@ class SyncplayClient(object): def controllerIdentificationSuccess(self, username, roomname): self.userlist.setUserAsController(username) - self.ui.setControllerStatus(username, isController=True) if self.userlist.isRoomSame(roomname): hideFromOSD = not constants.SHOW_SAME_ROOM_OSD self.ui.showMessage(u"<{}> authenticated as a room controller".format(username), hideFromOSD) + self.ui.userListChange() # TODO: Mark person as a room controller # TODO: Disable UI's "Create new Controlled Room when in Controlled Room" diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index 70acede..1b060ef 100644 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -39,9 +39,6 @@ class ConsoleUI(threading.Thread): def updateRoomName(self, room=""): pass - def setControllerStatus(self, username, isController): - pass - def promptFor(self, prompt=">", message=""): if message <> "": print(message) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index db015a4..70c3804 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -79,19 +79,6 @@ class MainWindow(QtGui.QMainWindow): # TODO: Prompt user return None - def setControllerStatus(self, username, isController): - roomtocheck = 0 - while self.listTreeModel.item(roomtocheck): - itemtocheck = 0 - while self.listTreeModel.item(roomtocheck).child(itemtocheck): - item = self.listTreeModel.item(roomtocheck).child(itemtocheck) - if item.data(0) == username: - item.setData(isController, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE) - return - itemtocheck = itemtocheck + 1 - roomtocheck += 1 - self.listTreeView.resizeColumnToContents(True) - def showMessage(self, message, noTimestamp=False): message = unicode(message) message = message.replace("&", "&").replace('"', """).replace("<", "<").replace(">", ">") From 1db7c73c42393ae68d77b7fce69d69c92e38b7d2 Mon Sep 17 00:00:00 2001 From: Et0h Date: Tue, 21 Oct 2014 23:29:59 +0100 Subject: [PATCH 27/58] Controllers listed first --- syncplay/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/syncplay/client.py b/syncplay/client.py index 6b786e5..e31c44e 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -524,7 +524,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: From 76e1d977cfd6d4e0ba7ffaabef87bf8f8b6aee17 Mon Sep 17 00:00:00 2001 From: Et0h Date: Wed, 22 Oct 2014 00:43:00 +0100 Subject: [PATCH 28/58] Lie w/ "version" and use "realversion" for true version (1.2.X compatibility) --- syncplay/protocols.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/syncplay/protocols.py b/syncplay/protocols.py index 67b6026..18da515 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -71,6 +71,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 @@ -94,7 +95,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): @@ -277,6 +279,7 @@ class SyncServerProtocol(JSONCommandProtocol): roomName = room["name"] if room.has_key("name") else None roomName = roomName.strip() version = hello["version"] if hello.has_key("version") else None + version = hello["realversion"] if hello.has_key("realversion") else version return username, serverPassword, roomName, version def _checkPassword(self, serverPassword): @@ -311,7 +314,8 @@ class SyncServerProtocol(JSONCommandProtocol): userIp = self.transport.getPeer().host room = self._watcher.getRoom() if room: hello["room"] = {"name": room.getName()} - hello["version"] = clientVersion # syncplay.version - Don't BC with 1.2.x + 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}) From 1e715cc11c0b8f0c926cfc4439ed9a0fe7c7226d Mon Sep 17 00:00:00 2001 From: Et0h Date: Fri, 24 Oct 2014 19:58:56 +0100 Subject: [PATCH 29/58] Make individual columns for filename, size & duration --- syncplay/messages.py | 6 +++- syncplay/ui/gui.py | 66 +++++++++++++++++++++++--------------------- syncplay/utils.py | 13 +++++++++ 3 files changed, 53 insertions(+), 32 deletions(-) diff --git a/syncplay/messages.py b/syncplay/messages.py index 187ae16..965cf6d 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -172,7 +172,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", @@ -192,6 +194,8 @@ en = { "setoffset-msgbox-label" : "Set offset", "offsetinfo-msgbox-label" : "Offset (see http://syncplay.pl/guide/ for usage instructions):", + "mebibyte-suffix" : " MiB", + # 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.", diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 70c3804..59fd0c4 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -6,7 +6,7 @@ import sys import time import re import os -from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration, RoomPasswordProvider +from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration, RoomPasswordProvider, formatSize class UserlistItemDelegate(QtGui.QStyledItemDelegate): def __init__(self): @@ -92,9 +92,8 @@ class MainWindow(QtGui.QMainWindow): def showUserList(self, currentUser, rooms): self._usertreebuffer = QtGui.QStandardItemModel() - self._usertreebuffer.setColumnCount(2) self._usertreebuffer.setHorizontalHeaderLabels( - (getMessage("roomuser-heading-label"), getMessage("fileplayed-heading-label"))) + (getMessage("roomuser-heading-label"), getMessage("size-heading-label"), getMessage("duration-heading-label"), getMessage("filename-heading-label") )) usertreeRoot = self._usertreebuffer.invisibleRootItem() for room in rooms: @@ -113,49 +112,54 @@ class MainWindow(QtGui.QMainWindow): 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))) + if sameRoom: + if not sameName: + filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR))) + 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.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR))) + if not sameDuration: + filedurationitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR))) 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))) 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)) - + 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.expandAll() - self.listTreeView.resizeColumnToContents(0) - self.listTreeView.resizeColumnToContents(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() def roomClicked(self, item): while item.parent().row() != -1: @@ -356,8 +360,8 @@ class MainWindow(QtGui.QMainWindow): window.topSplit.addWidget(window.outputFrame) window.topSplit.addWidget(window.listFrame) - window.topSplit.setStretchFactor(0, 4) - window.topSplit.setStretchFactor(1, 5) + window.topSplit.setStretchFactor(0,4) + window.topSplit.setStretchFactor(1,5) window.mainLayout.addWidget(window.topSplit) window.topSplit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) diff --git a/syncplay/utils.py b/syncplay/utils.py index 5d6728f..cf04c2d 100644 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -93,6 +93,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: + mebibytes = int(bytes) / 1048576.0 + if precise: + mebibytes = round(mebibytes, 1) + else: + mebibytes = int(mebibytes) + return str(mebibytes) + getMessage("mebibyte-suffix") + except: # E.g. when filesize is hashed + return "???" + def findWorkingDir(): frozen = getattr(sys, 'frozen', '') if not frozen: From c78ce70afaa6356038d98ed96461f5102e65a4dd Mon Sep 17 00:00:00 2001 From: Et0h Date: Fri, 24 Oct 2014 20:19:21 +0100 Subject: [PATCH 30/58] Make room name column span entire row --- syncplay/ui/gui.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 59fd0c4..fa6fd18 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -104,8 +104,7 @@ class MainWindow(QtGui.QMainWindow): roomitem.setFont(font) blankitem = QtGui.QStandardItem("") roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable) - blankitem.setFlags(blankitem.flags() & ~Qt.ItemIsEditable) - usertreeRoot.appendRow((roomitem, blankitem)) + usertreeRoot.appendRow(roomitem) for user in rooms[room]: useritem = QtGui.QStandardItem(user.username) @@ -149,6 +148,10 @@ class MainWindow(QtGui.QMainWindow): self.listTreeView.setItemDelegate(UserlistItemDelegate()) self.listTreeView.setItemsExpandable(False) self.listTreeView.expandAll() + 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) From 7794a94aa7fca924d5e3ab79835d1de60617e170 Mon Sep 17 00:00:00 2001 From: Et0h Date: Fri, 24 Oct 2014 20:32:04 +0100 Subject: [PATCH 31/58] Make rooms italicised --- syncplay/ui/gui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index fa6fd18..c750018 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -98,11 +98,11 @@ class MainWindow(QtGui.QMainWindow): 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.setFont(font) roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable) usertreeRoot.appendRow(roomitem) From 2aa7b50ac6d3a4864f9ad243968ee0e7e67f7d4e Mon Sep 17 00:00:00 2001 From: Et0h Date: Fri, 24 Oct 2014 23:03:25 +0100 Subject: [PATCH 32/58] Use different room icon for controlled/uncontroleld rooms --- resources/lock.png | Bin 0 -> 749 bytes syncplay/ui/gui.py | 13 +++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 resources/lock.png diff --git a/resources/lock.png b/resources/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..2ebc4f6f9663e32cad77d67ef93ab8843dfea3c0 GIT binary patch literal 749 zcmVe|tv9>?g+k#9o0pTxd@;_sq{kwlU;^VvV*?BV8P@}BoaZTQUROpWV6|-M`|^n&)=+8tHo3*<<$NU zU`%V~ZF;?hBSYsjJ6%JzV}E(D{pOLqQklliUf9um_tGl-wty`y*p?eYNW56P>X@1s zZs7KrRZKtmV7Lqj^5Fgr7_`LjhdJK@ltF&O`j7?*NUM$KvmNGz)3WjM?V$vHlPT0AFyF?kLE<#HZabCSW3-oa*6;Z zrXD`Ulwd<^2glP%1Y1Kc1Ij%DU^=ME(jKf6APNlA$Uu;J4bVilQHSWX5uJ$9Zsp4M z0%!@LvyTxz=Z6stxlichODIY+yNGt%RM;m`>H4LOKLFs9Y%b5aUN|2|{0Zw|<_~i} fmXz*V19AKYa Date: Fri, 24 Oct 2014 23:04:14 +0100 Subject: [PATCH 33/58] Make usernames of non-controllers in controlled rooms grey --- syncplay/constants.py | 1 + syncplay/ui/gui.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/syncplay/constants.py b/syncplay/constants.py index 9bd0065..c300163 100644 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -106,6 +106,7 @@ STYLE_USERNAME = "color: #367AA9; font-weight:bold;" STYLE_ERRORNOTIFICATION = "color: red;" STYLE_DIFFERENTITEM_COLOR = 'red' STYLE_NOFILEITEM_COLOR = 'blue' +STYLE_NOTCONTROLLER_COLOR = 'grey' MPLAYER_SLAVE_ARGS = ['-slave', '--hr-seek=always', '-nomsgcolor', '-msglevel', 'all=1:global=4:cplayer=4'] # --quiet works with both mpv 0.2 and 0.3 diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 629408f..823490d 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -122,6 +122,8 @@ class MainWindow(QtGui.QMainWindow): useritem = QtGui.QStandardItem(user.username) isController = user.isController() useritem.setData(isController, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE) + if isControlledRoom and not isController: + useritem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOTCONTROLLER_COLOR))) if user.file: filesizeitem = QtGui.QStandardItem(formatSize(user.file['size'])) filedurationitem = QtGui.QStandardItem("({})".format(formatTime(user.file['duration']))) From 526cb80b86e5c90e6819f5fcd069049350380708 Mon Sep 17 00:00:00 2001 From: Et0h Date: Fri, 24 Oct 2014 23:08:36 +0100 Subject: [PATCH 34/58] Don't make non-controller grey if they are the current user --- syncplay/ui/gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 823490d..2d24c31 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -122,8 +122,6 @@ class MainWindow(QtGui.QMainWindow): useritem = QtGui.QStandardItem(user.username) isController = user.isController() useritem.setData(isController, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE) - if isControlledRoom and not isController: - useritem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOTCONTROLLER_COLOR))) if user.file: filesizeitem = QtGui.QStandardItem(formatSize(user.file['size'])) filedurationitem = QtGui.QStandardItem("({})".format(formatTime(user.file['duration']))) @@ -152,6 +150,8 @@ class MainWindow(QtGui.QMainWindow): font = QtGui.QFont() font.setWeight(QtGui.QFont.Bold) useritem.setFont(font) + elif isControlledRoom and not isController: + useritem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOTCONTROLLER_COLOR))) useritem.setFlags(useritem.flags() & ~Qt.ItemIsEditable) filenameitem.setFlags(filenameitem.flags() & ~Qt.ItemIsEditable) filesizeitem.setFlags(filesizeitem.flags() & ~Qt.ItemIsEditable) From 0c9128cf4481dce0edd61dc2d31359db24889565 Mon Sep 17 00:00:00 2001 From: Et0h Date: Fri, 24 Oct 2014 23:16:17 +0100 Subject: [PATCH 35/58] Use bullet_key icon instead of controller --- resources/bullet_key.png | Bin 0 -> 436 bytes resources/controller.png | Bin 666 -> 0 bytes syncplay/ui/gui.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 resources/bullet_key.png delete mode 100644 resources/controller.png diff --git a/resources/bullet_key.png b/resources/bullet_key.png new file mode 100644 index 0000000000000000000000000000000000000000..3d37f2ea49a445bd104e2afa39601a01b1acb04a GIT binary patch literal 436 zcmV;l0ZaagP))D9~S@raHZk@o8xu=?=BAbx-CyJk|+c2F0@|%^WDn-Kc3b7|M6nh z{|^`I{(pPa@c-rv`wv7JaBHSX+n2kY|Gz!R`v3h=@&9l4pV{WeA)K@>rLPPXC_&Fo|~u=OHx8Q*=y2zb-wG%)4f&?mZqzQ e4THQu3 zlg*1#VHC#iD%uAHty;DTg0}q?ty;8bDFzCDreJ80{BB+R< zJENR4PZMQi-K$n$6{M+e{`y zCzHvi{{^55g~BV5NOa+FIKC8%#WFEpk_S+;0uA4flMsu=wvk9g@eiQYYGoUZ#;;nf zwk?;-FdB`3VHmiWEa5y`gl|tCa~huHAQ%j?r0MVf0GiFFt=sK#C%yTk;Ec=V5(eW1 zy!^<*iw^+Bx5qH>dSE^ra{Kc6JlJfuBMv}5)@Yi~=Nw?K*=zV6|Ft zLOAX<8jU;B^k)=l4vT=l&3kn^9UxVL)9GYU)9I9}vfXZjQmOn(B@&4pQul^EU^1CN zHk*ZXIt^qGkTG9cEEbJkuV?WZo6QC!5(!Z8c%13?`&?OQ#B#^uF;uHnNTpKPc_2n0 zqyCI{R;g4puH&IXp%AEOGKB zFaZe_Htgq(0OW7HPl9o7Hk+@=e6O@xZTY%@-!Rz$1Rlkwh5!Hn07*qoM6N<$f($t; AN&o-= diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 2d24c31..b431c33 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -27,7 +27,7 @@ class UserlistItemDelegate(QtGui.QStyledItemDelegate): resourcespath = utils.findWorkingDir() + "\\resources\\" else: resourcespath = utils.findWorkingDir() + "/resources/" - controlIconQPixmap = QtGui.QPixmap(resourcespath + "controller.png") + controlIconQPixmap = QtGui.QPixmap(resourcespath + "bullet_key.png") tickIconQPixmap = QtGui.QPixmap(resourcespath + "tick.png") crossIconQPixmap = QtGui.QPixmap(resourcespath + "cross.png") roomController = currentQAbstractItemModel.data(itemQModelIndex, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE) From 984db44d1220d64a9f12999716090d615ebe3b91 Mon Sep 17 00:00:00 2001 From: Et0h Date: Fri, 24 Oct 2014 23:17:57 +0100 Subject: [PATCH 36/58] Move controller icon right until userReady is implemented --- syncplay/ui/gui.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index b431c33..4c11b2c 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -35,19 +35,19 @@ class UserlistItemDelegate(QtGui.QStyledItemDelegate): if roomController and not controlIconQPixmap.isNull(): itemQPainter.drawPixmap ( - optionQStyleOptionViewItem.rect.x()-32, + optionQStyleOptionViewItem.rect.x()-14, optionQStyleOptionViewItem.rect.y(), controlIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio)) if userReady and not tickIconQPixmap.isNull(): itemQPainter.drawPixmap ( - (optionQStyleOptionViewItem.rect.x()-14), + (optionQStyleOptionViewItem.rect.x()-28), optionQStyleOptionViewItem.rect.y(), tickIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio)) elif userReady == False and not crossIconQPixmap.isNull(): itemQPainter.drawPixmap ( - (optionQStyleOptionViewItem.rect.x()-14), + (optionQStyleOptionViewItem.rect.x()-28), optionQStyleOptionViewItem.rect.y(), crossIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio)) QtGui.QStyledItemDelegate.paint(self, itemQPainter, optionQStyleOptionViewItem, indexQModelIndex) From 6160ec1fecbc090e86676281da3f9fe5500f8aa8 Mon Sep 17 00:00:00 2001 From: Et0h Date: Fri, 24 Oct 2014 23:45:08 +0100 Subject: [PATCH 37/58] Add user to key icon --- resources/bullet_key.png | Bin 436 -> 0 bytes resources/user_key.png | Bin 0 -> 834 bytes syncplay/ui/gui.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 resources/bullet_key.png create mode 100644 resources/user_key.png diff --git a/resources/bullet_key.png b/resources/bullet_key.png deleted file mode 100644 index 3d37f2ea49a445bd104e2afa39601a01b1acb04a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 436 zcmV;l0ZaagP))D9~S@raHZk@o8xu=?=BAbx-CyJk|+c2F0@|%^WDn-Kc3b7|M6nh z{|^`I{(pPa@c-rv`wv7JaBHSX+n2kY|Gz!R`v3h=@&9l4pV{WeA)K@>rLPPXC_&Fo|~u=OHx8Q*=y2zb-wG%)4f&?mZqzQ e4THQu3gYv3TinXy-2k+}*Gy>bL}L->cqhw$fP*EJb$|SH9*B zJZZj&cz8jvbL6FhjuFt=)&N^Uu4O@CO=euG?YyIIN#V2@=WTCMNs?HihUMzx8`*F+ z7K7k7tw|fes+?r#)3+cv+>fYVg14`e>wfpKEM{Qbc8?N795yHun;udFq6cNzX(hX;quQ*1)P!!FoGESadza0_mQ!A1Bmbxl;}sN zJuH)WVNgr(O@_&#mchHR6zIkIXTEeaN7a#$=)>D@p^PKQ)7i0ZYaXJWCQw2EpPq|o z=*Yo?p{x+^_k>Mr3u>ogv>m)$Z%EHKBwHoRxF^uqUVn4gH|CQl<~tab8IH$1>g;L$ zPbB*p(P~pnX&cS5rg|#I+3L-O%vzm6U>Ep`M=c(+3)RTW6d536Lc#|5acUt^fc4 M07*qoM6N<$f{Dj_?*IS* literal 0 HcmV?d00001 diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 4c11b2c..073b2cc 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -27,7 +27,7 @@ class UserlistItemDelegate(QtGui.QStyledItemDelegate): resourcespath = utils.findWorkingDir() + "\\resources\\" else: resourcespath = utils.findWorkingDir() + "/resources/" - controlIconQPixmap = QtGui.QPixmap(resourcespath + "bullet_key.png") + 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) From e220c3c3b39d67e7874b54f3af639be73fdd7bc8 Mon Sep 17 00:00:00 2001 From: Et0h Date: Fri, 24 Oct 2014 23:55:58 +0100 Subject: [PATCH 38/58] Re-work username offset to avoid messing up later columns --- syncplay/ui/gui.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 073b2cc..25d405d 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -17,10 +17,6 @@ class UserlistItemDelegate(QtGui.QStyledItemDelegate): if column == 0: isRoomRow = indexQModelIndex.parent() == indexQModelIndex.parent().parent() itemQPainter.resetTransform() - if not isRoomRow: - transformer = QtGui.QTransform() - transformer.translate(21,0) - itemQPainter.setTransform(transformer) currentQAbstractItemModel = indexQModelIndex.model() itemQModelIndex = currentQAbstractItemModel.index(indexQModelIndex.row(), 0, indexQModelIndex.parent()) if sys.platform.startswith('win'): @@ -35,21 +31,23 @@ class UserlistItemDelegate(QtGui.QStyledItemDelegate): if roomController and not controlIconQPixmap.isNull(): itemQPainter.drawPixmap ( - optionQStyleOptionViewItem.rect.x()-14, + optionQStyleOptionViewItem.rect.x()+6, optionQStyleOptionViewItem.rect.y(), controlIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio)) if userReady and not tickIconQPixmap.isNull(): itemQPainter.drawPixmap ( - (optionQStyleOptionViewItem.rect.x()-28), + (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()-28), + (optionQStyleOptionViewItem.rect.x()-10), optionQStyleOptionViewItem.rect.y(), crossIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio)) + if not isRoomRow: + optionQStyleOptionViewItem.rect.setX(optionQStyleOptionViewItem.rect.x()+21) QtGui.QStyledItemDelegate.paint(self, itemQPainter, optionQStyleOptionViewItem, indexQModelIndex) class MainWindow(QtGui.QMainWindow): From 94c3bf4aa68f165fc409d41579b5b22bf947a380 Mon Sep 17 00:00:00 2001 From: Et0h Date: Fri, 24 Oct 2014 23:58:36 +0100 Subject: [PATCH 39/58] Tidy up user file offset code --- syncplay/ui/gui.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 25d405d..c1ff3e2 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -15,8 +15,6 @@ class UserlistItemDelegate(QtGui.QStyledItemDelegate): def paint(self, itemQPainter, optionQStyleOptionViewItem, indexQModelIndex): column = indexQModelIndex.column() if column == 0: - isRoomRow = indexQModelIndex.parent() == indexQModelIndex.parent().parent() - itemQPainter.resetTransform() currentQAbstractItemModel = indexQModelIndex.model() itemQModelIndex = currentQAbstractItemModel.index(indexQModelIndex.row(), 0, indexQModelIndex.parent()) if sys.platform.startswith('win'): @@ -46,7 +44,8 @@ class UserlistItemDelegate(QtGui.QStyledItemDelegate): (optionQStyleOptionViewItem.rect.x()-10), optionQStyleOptionViewItem.rect.y(), crossIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio)) - if not isRoomRow: + isUserRow = indexQModelIndex.parent() != indexQModelIndex.parent().parent() + if isUserRow: optionQStyleOptionViewItem.rect.setX(optionQStyleOptionViewItem.rect.x()+21) QtGui.QStyledItemDelegate.paint(self, itemQPainter, optionQStyleOptionViewItem, indexQModelIndex) From 106ecc1e943951f6b5793b6853b6f26759965054 Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 25 Oct 2014 00:01:38 +0100 Subject: [PATCH 40/58] Update installer to use new icons --- buildPy2exe.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/buildPy2exe.py b/buildPy2exe.py index 1692e95..e94dbad 100644 --- a/buildPy2exe.py +++ b/buildPy2exe.py @@ -583,7 +583,8 @@ guiIcons = ['resources/accept.png', 'resources/arrow_undo.png', 'resources/clock '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/controller.png', 'resources/key_go.png', 'resources/page_white_key.png', 'resources/tick.png' + 'resources/user_key.png', 'resources/lock.png', 'resources/key_go.png', 'resources/page_white_key.png', + 'resources/tick.png' ] resources = ["resources/icon.ico", "resources/syncplay.png"] resources.extend(guiIcons) From 261b3e663d6c2bce33d9294e956d105d0bae07c3 Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 25 Oct 2014 00:08:03 +0100 Subject: [PATCH 41/58] Use open lock icon if you are controller of room --- buildPy2exe.py | 2 +- resources/lock_open.png | Bin 0 -> 727 bytes syncplay/ui/gui.py | 5 ++++- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 resources/lock_open.png diff --git a/buildPy2exe.py b/buildPy2exe.py index e94dbad..f94fb2f 100644 --- a/buildPy2exe.py +++ b/buildPy2exe.py @@ -584,7 +584,7 @@ guiIcons = ['resources/accept.png', 'resources/arrow_undo.png', 'resources/clock '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/user_key.png', 'resources/lock.png', 'resources/key_go.png', 'resources/page_white_key.png', - 'resources/tick.png' + 'resources/tick.png', 'lock_open.png' ] resources = ["resources/icon.ico", "resources/syncplay.png"] resources.extend(guiIcons) diff --git a/resources/lock_open.png b/resources/lock_open.png new file mode 100644 index 0000000000000000000000000000000000000000..a471765ff1432092e7113e56b42f2798968b443f GIT binary patch literal 727 zcmV;|0x127P)rLKlJyH=-*k3W|aiVYC~e{STzB+=%NyKm^5w z3m4+bm8cXnzG}0Gw2c^Ol0K$0_ity0#@^}dhsl97GUJu9lkxh`Okt_0|=(|h`!tol*)tahk*i! zgS8GLCq{|Gd7N1T=a4u`0gM4BU}M^~w|L$b3WtyL{rO$Q?PPfL6z$J7;&2In&vryC zqT@6`g9u_Zg@Xt1vO7RIY-D-g8eo{1!oIz%ujUZ52IBq#JOI%e5ertc^kIf}?KOA? zx9*6-2qdQcN5WDRs@&WP6I?Nxm{No*quRTbIBKa{opLb zA%3`w)e+)sGB0CNfH Date: Sat, 25 Oct 2014 00:19:44 +0100 Subject: [PATCH 42/58] Make room names underlined, non-controllers italic --- syncplay/ui/gui.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index d2de151..65abb69 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -102,7 +102,7 @@ class MainWindow(QtGui.QMainWindow): for room in rooms: roomitem = QtGui.QStandardItem(room) font = QtGui.QFont() - font.setItalic(True) + font.setUnderline(True) if room == currentUser.room: font.setWeight(QtGui.QFont.Bold) roomitem.setFont(font) @@ -146,12 +146,14 @@ class MainWindow(QtGui.QMainWindow): filesizeitem = QtGui.QStandardItem("") if room == currentUser.room: 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) - elif isControlledRoom and not isController: - useritem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOTCONTROLLER_COLOR))) + if isControlledRoom and not isController: + font.setItalic(True) + 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) From 2a62f987709d5b37a14c7dad2734ea6e89c36dab Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 25 Oct 2014 00:22:33 +0100 Subject: [PATCH 43/58] Fix installer path for lock_open --- buildPy2exe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildPy2exe.py b/buildPy2exe.py index f94fb2f..2bb857a 100644 --- a/buildPy2exe.py +++ b/buildPy2exe.py @@ -584,7 +584,7 @@ guiIcons = ['resources/accept.png', 'resources/arrow_undo.png', 'resources/clock '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/user_key.png', 'resources/lock.png', 'resources/key_go.png', 'resources/page_white_key.png', - 'resources/tick.png', 'lock_open.png' + 'resources/tick.png', 'resources/lock_open.png' ] resources = ["resources/icon.ico", "resources/syncplay.png"] resources.extend(guiIcons) From 9c6f4e75a29f8abd4c8480ed3cef6bc1f13a0331 Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 25 Oct 2014 09:21:16 +0100 Subject: [PATCH 44/58] Hacky fix to give usernames enough room --- syncplay/ui/gui.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 65abb69..1902e90 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -171,9 +171,14 @@ class MainWindow(QtGui.QMainWindow): roomtocheck += 1 self.listTreeView.header().setStretchLastSection(False) self.listTreeView.header().setResizeMode(0, QtGui.QHeaderView.ResizeToContents) + namecolumnwidth = self.listTreeView.header().sectionSize(0) + 40 + if self.listTreeView.header().sectionSize(0) == self.listTreeView.header().sectionSizeHint(0): + namecolumnwidth = namecolumnwidth - 20 self.listTreeView.header().setResizeMode(1, QtGui.QHeaderView.ResizeToContents) self.listTreeView.header().setResizeMode(2, QtGui.QHeaderView.ResizeToContents) self.listTreeView.header().setResizeMode(3, QtGui.QHeaderView.ResizeToContents) + self.listTreeView.header().setResizeMode(0, QtGui.QHeaderView.Fixed) + self.listTreeView.header().resizeSection(0,namecolumnwidth) 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) From 110935e5e4bf29d622360d1dbf1aa01687380a82 Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 25 Oct 2014 09:45:27 +0100 Subject: [PATCH 45/58] Less hacky fix to give usernames enough room --- syncplay/constants.py | 2 ++ syncplay/ui/gui.py | 13 +++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/syncplay/constants.py b/syncplay/constants.py index c300163..501a2ba 100644 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -108,6 +108,8 @@ 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'] # --quiet works with both mpv 0.2 and 0.3 MPV_SLAVE_ARGS = ['--hr-seek=always', '--quiet', '--keep-open'] diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 1902e90..53f1d0d 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -12,6 +12,12 @@ 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: @@ -46,7 +52,7 @@ class UserlistItemDelegate(QtGui.QStyledItemDelegate): crossIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio)) isUserRow = indexQModelIndex.parent() != indexQModelIndex.parent().parent() if isUserRow: - optionQStyleOptionViewItem.rect.setX(optionQStyleOptionViewItem.rect.x()+21) + optionQStyleOptionViewItem.rect.setX(optionQStyleOptionViewItem.rect.x()+constants.USERLIST_GUI_USERNAME_OFFSET) QtGui.QStyledItemDelegate.paint(self, itemQPainter, optionQStyleOptionViewItem, indexQModelIndex) class MainWindow(QtGui.QMainWindow): @@ -171,14 +177,9 @@ class MainWindow(QtGui.QMainWindow): roomtocheck += 1 self.listTreeView.header().setStretchLastSection(False) self.listTreeView.header().setResizeMode(0, QtGui.QHeaderView.ResizeToContents) - namecolumnwidth = self.listTreeView.header().sectionSize(0) + 40 - if self.listTreeView.header().sectionSize(0) == self.listTreeView.header().sectionSizeHint(0): - namecolumnwidth = namecolumnwidth - 20 self.listTreeView.header().setResizeMode(1, QtGui.QHeaderView.ResizeToContents) self.listTreeView.header().setResizeMode(2, QtGui.QHeaderView.ResizeToContents) self.listTreeView.header().setResizeMode(3, QtGui.QHeaderView.ResizeToContents) - self.listTreeView.header().setResizeMode(0, QtGui.QHeaderView.Fixed) - self.listTreeView.header().resizeSection(0,namecolumnwidth) 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) From 5519fd481d8d88e4df95038a4d39abef5630f7e9 Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 25 Oct 2014 10:05:51 +0100 Subject: [PATCH 46/58] Change bullet icons to a right double chevron --- buildPy2exe.py | 2 +- resources/bullet_black.png | Bin 211 -> 0 bytes resources/chevrons_right.png | Bin 0 -> 403 bytes syncplay/ui/GuiConfiguration.py | 12 ++++++------ syncplay/ui/gui.py | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 resources/bullet_black.png create mode 100644 resources/chevrons_right.png diff --git a/buildPy2exe.py b/buildPy2exe.py index 2bb857a..9c8ca4a 100644 --- a/buildPy2exe.py +++ b/buildPy2exe.py @@ -582,7 +582,7 @@ 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' ] diff --git a/resources/bullet_black.png b/resources/bullet_black.png deleted file mode 100644 index 57619706d10d9736b1849a83f2c5694fbe09c53b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 211 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^zbpD<_bdI{u9mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-$h^>lFz(Kw&{<9vg>5sw~gS5O!4 zr|{HuUFIBKiQyL}eBJ-L{`UVT|6_O~L{G%N{Wbre{kQtZ_0LvEh!lvI6;>1s;*b z3=DDyK$uZbZ`*dDAbW|YuPgg?ZZ;l0nRXACzd#|W64!{5;QX|b^2DN4hJeJ(yb?V> z*ARs=V?9$nLj!{^MGJwd{&>1LhFJK&ooL(D;vnGGs}_6XO7fdj9~MCqBLPPiS3yB$ zM;;MZmZS4sTm<F};u@GSgK2AS_hOA(74y;; zWZeJWd3sY$GtcJZ@{3o!62J4~?)SfMC#+WB5cizaF?$*3^wdgI-_M(l_uts&`@FKR zE%CsG4JS99akyA0BYfTDEZfY_oU2wjeYQ!?h$x9wfA2eqrT4Li%$vvGWlLhWA7bf# uz3Y4L;h^g#zWnbpA85zt|9c&9h5z`a*3`(SlKQ}KVDNPHb6Mw<&;$UCMx5XP literal 0 HcmV?d00001 diff --git a/syncplay/ui/GuiConfiguration.py b/syncplay/ui/GuiConfiguration.py index a10f506..c69173a 100644 --- a/syncplay/ui/GuiConfiguration.py +++ b/syncplay/ui/GuiConfiguration.py @@ -427,7 +427,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: @@ -444,7 +444,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']) @@ -516,22 +516,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/gui.py b/syncplay/ui/gui.py index 53f1d0d..6885f0e 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -122,7 +122,7 @@ class MainWindow(QtGui.QMainWindow): else: roomitem.setIcon(QtGui.QIcon(self.resourcespath + 'lock.png')) else: - roomitem.setIcon(QtGui.QIcon(self.resourcespath + 'bullet_black.png')) + roomitem.setIcon(QtGui.QIcon(self.resourcespath + 'chevrons_right.png')) for user in rooms[room]: useritem = QtGui.QStandardItem(user.username) From 468c7f665d83c0d4c0b2d846f4b43df3e97be678 Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 25 Oct 2014 10:23:31 +0100 Subject: [PATCH 47/58] Make room icons closer to room name --- resources/chevrons_right.png | Bin 403 -> 418 bytes resources/lock.png | Bin 749 -> 884 bytes resources/lock_open.png | Bin 727 -> 824 bytes syncplay/constants.py | 4 ++-- 4 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/chevrons_right.png b/resources/chevrons_right.png index 8d6adcf3f2c62f0798efe3674a21c2331605a26a..356a05a736b4525b0b13af2a55a7c2f74c3554bf 100644 GIT binary patch delta 298 zcmV+_0oDGK1EK?vfqwxDNklApAJh(5AG->j|G>4hv+>s%KVEsD(0=giMFUeh!q?@8B$d+XxbzR?W+rCi+uIsX8S?<#` z5g0gCRpp@AG)<9pU3Wsy^9`D&+1GU?FtC}XiSNy_Y@g>D8QkOh8w|s6zy_P|`(GD_ wqaJylKQIhU@FOhnD<6li{6s*_9}5`f1B%Jz1tGRajQ{`u07*qoM6N<$g7f2w4gdfE delta 283 zcmV+$0p$Lo1Cs-gfqww}Nkl}5;@5i~LoK?Fq+5d%RC z6h#EZpF~6v58*NT3{GUQL2peX1H!m4!vq`bgGwG>byrtY|Gb$n3|GT27M5i{c13xf zw`!VpaHj-8fa+M<2a}&3gSdjB<>VNQD~R~wSR4kqbS;Vo@bzF-}i~j zvTTD4v&SE!s_GZq_}%yY?U1V=2p36`h?}Mfmt}c1P4lwHkJz?N=Xs8XVZhL^$+FBq z(QzCT*EG!rnez#%szUQTV`x}(T_^8Nk_3(8NF40r_Z@Uyx55OA>$>-i!&28YO%LvA h;45Em{_9ZG4;07Jg=Jag87}|;002ovPDHLkV1lCbflL4Z diff --git a/resources/lock.png b/resources/lock.png index 2ebc4f6f9663e32cad77d67ef93ab8843dfea3c0..2d06336d35df082f8c3d48b3fa1c0c1b528671ba 100644 GIT binary patch delta 845 zcmV-T1G4<>1@s1xG=B+jNLh0L01m?d01m?e$8V@)0000QbVXQnQ*UN;cVTj608n9R zZgehAMN}YmGcGkQF)%Rna-jeK0`N&hK~y+TWs+M+lVKRg3z7~<5D`Qdbx{{tU0BfJ zqAn^BqOzg`WkGe(O+*JuQP9(16S33Pro!PGw~!>FMcRsi~=vf9n5l4lgb)Zg)5w z)Ya7?zRAnWLw~2!Ask?_SVGchH0^4&dhRrUXo$61t=Mcf^7HdMh2>@0+1X*4nVDf} zX=%$+Qc`-&W;2C_g(M{<#ZCi=eJ%2Id9s)qF*PSEIXO8(*r&X_oP>k~i+uJ^fx%#? zcRHPj$z)|^&6%2$rBbQph{;q`R1g;zS1+IaO?v82Vt+2WhOmsVOvjTL+*JqZdbp3y zySwSA+(DZ;ium|=xtR>rDLxq1Ds+$i1i|5#Obowba`Zinw!zO25cn+QhLKMMhh8%_ zU}x-mZAh)U)e7LV?U(?(Kyt@2@ii6ta-r7Jw^Yc zUBW>Oe9xs*A4x^>Zt!UEmT$!)fd0B|NWBkur+-?g0OVU9_dVrT^KpXhXT<)O#qL?~ zLocEZ+=&ESM@4fKKI>+rp8LE_n6CiZ&D$C8w=vRqg#J5Q@mr(u+hg!MPvCbP!!KNY zE!&94xC&p@CIUToX^Nez06vv&5l>S?P-xO;6Hn+A&-Vt#nni39PM#C=T9LloC(wN- zq(;UIVG7`5!G_R}(U!A@R_#h!(wB#BxmmM>cUKniHgQ47a#P$q8ZXYK@%${_oSn&U XaIf_6LXssanuR6PuPaD3lbFCZe|tv9>?g+k#9o0pTxZ{ zR;$HSD&^Gv4`57d=52btg(E}fG&@~Gwqt*Ju>Izdy;7OR&R*EjeD~5S{I-BCE!dVD zLP)$=yXu&l7;fP9nN>_b`CzyV2=d_m4H&e<@P|3y0)Lc2e#82Z1_?;3jpwr+=7rO; z@$&7V1bve(w02)15E_MHjIeMB2*;UL!2TX&>l@7OcPMKg;mIs)v+n^QMJRofiRSuy z>O6+{#cQa&US$7iOkAHu^XobR8i=M&kg$X;OfP_NWZ0w{pS}QJaFp&S$iBp<-7qF# zBA%W^uYVI_?Y_fm0O0%so9=28soruwH<;wN~XbGXSj}boShY<|9Pw3-IC_YLvyNGt%RM;m`>H4LOKLFs9Y%b5a rUN|2|{0Zw|<_~i}mXz*V19AKYaq!0;vb79xBj$ zs0R%MDXp*~HL@Oph-je<5~(QBR<}vTkq~VPGc_$U_dRDaaetq2cO1=SQfFM!=IS`V zz8T0nX7upqeD~b*|J`%Gb5{W?q*AH8Qd3iNl}hCgq1l9lgxt8eIIn-zu1E;urR?l% zn?|FduCC6Zva&Lglapb%&u8kS<`B@nv9GL#8(|kNl76uFVB9Sc5!j> zclIm;C=`nEihqg! z!t9Ny=pfSIN1E?!lmXr8hgldeXSOkb@f=UgMSp&ns{%3Cp2u8s7PIJT`1Jt{%3YWW zJg^Mr(iE~@26VkTBvw;`Rmf&47b~n4>uZ4dRuP*-Cu*$5BBbvhu?*xm)Ocg94Cs96 z?)WjEmQZ?2Gx^S&R7lI2nqAMv1ih>nkv>T!Q52UW# zi0eQ=1jU657vjp5s1!85YO{#6jTmT>KBhDGaa@d#A=m?ldw)3G`|YDHIC%v9Yl`y$ZZkoioO;u&}Tc1VQyrb82d;0<=1vjv5>s z%x`D`$XII;5!-udVq#*D4MK?sjaOIX+00RlQ&?*`bL0&f(TFvONm6V3)OFiWf%6JE z?TO*wwRb8}Aj0_NV<c10RmR@v3l_=aDNtH;9xruB%v z+zyn=gX@QZ0*HgP4kIT2k^2xKsjt=dEXjfn3%%8y{xb15VHp2{sKGz(Hap8 zR^(ogj2GXC&Z1ee%O14JyjT~`^{ zok3-D$rpP4;4H);ez=U)5#nqzFJn@GIHLaKb|^9nV-TZ%r}=wgZL(O_Cbdfz_7}jm VaaS&KPgno|002ovPDHLkV1kq3LdgIC diff --git a/syncplay/constants.py b/syncplay/constants.py index 501a2ba..3ad462b 100644 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -97,8 +97,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; }" From b672c21bfb2063cce28329f9297abbcb36279779 Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 25 Oct 2014 13:16:21 +0100 Subject: [PATCH 48/58] Use SI megabyte (MB) rather than Mebibyte (MiB) --- syncplay/messages.py | 2 +- syncplay/utils.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/syncplay/messages.py b/syncplay/messages.py index 965cf6d..26992d7 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -194,7 +194,7 @@ en = { "setoffset-msgbox-label" : "Set offset", "offsetinfo-msgbox-label" : "Offset (see http://syncplay.pl/guide/ for usage instructions):", - "mebibyte-suffix" : " MiB", + "megabyte-suffix" : " MB", # Tooltips diff --git a/syncplay/utils.py b/syncplay/utils.py index cf04c2d..b1c52e7 100644 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -97,12 +97,12 @@ def formatSize (bytes, precise=False): if bytes == 0: # E.g. when file size privacy is enabled return "???" try: - mebibytes = int(bytes) / 1048576.0 + megabytes = int(bytes) / 1000000.0 if precise: - mebibytes = round(mebibytes, 1) + megabytes = round(megabytes, 1) else: - mebibytes = int(mebibytes) - return str(mebibytes) + getMessage("mebibyte-suffix") + megabytes = int(megabytes) + return str(megabytes) + getMessage("megabyte-suffix") except: # E.g. when filesize is hashed return "???" From cea3be53b38138b1e6869ee28b682472ab6434da Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 25 Oct 2014 23:44:31 +0100 Subject: [PATCH 49/58] Re-format: Underline diff items rather than room; Leave italics for unready users --- syncplay/ui/gui.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 6885f0e..d916868 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -108,7 +108,6 @@ class MainWindow(QtGui.QMainWindow): for room in rooms: roomitem = QtGui.QStandardItem(room) font = QtGui.QFont() - font.setUnderline(True) if room == currentUser.room: font.setWeight(QtGui.QFont.Bold) roomitem.setFont(font) @@ -137,15 +136,20 @@ class MainWindow(QtGui.QMainWindow): sameSize = sameFilesize(user.file['size'], currentUser.file['size']) sameDuration = sameFileduration(user.file['duration'], currentUser.file['duration']) sameRoom = room == currentUser.room + 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: filenameitem = QtGui.QStandardItem(getMessage("nofile-note")) filedurationitem = QtGui.QStandardItem("") @@ -156,7 +160,6 @@ class MainWindow(QtGui.QMainWindow): if currentUser.username == user.username: font.setWeight(QtGui.QFont.Bold) if isControlledRoom and not isController: - font.setItalic(True) if currentUser.username == user.username: useritem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOTCONTROLLER_COLOR))) useritem.setFont(font) From 27af49927ba77d04dce341b35dcd484bc3fccb14 Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 25 Oct 2014 23:54:27 +0100 Subject: [PATCH 50/58] Make room item italicised --- syncplay/ui/gui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index d916868..204703f 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -108,6 +108,7 @@ class MainWindow(QtGui.QMainWindow): for room in rooms: roomitem = QtGui.QStandardItem(room) font = QtGui.QFont() + font.setItalic(True) if room == currentUser.room: font.setWeight(QtGui.QFont.Bold) roomitem.setFont(font) From 751a7015860ca1d43909fb6a76fca187fb8257ba Mon Sep 17 00:00:00 2001 From: Et0h Date: Sun, 26 Oct 2014 14:11:32 +0000 Subject: [PATCH 51/58] Add @requireMinServerVersion and use it for controlled rooms --- syncplay/client.py | 18 ++++++++++++++++++ syncplay/protocols.py | 1 + 2 files changed, 19 insertions(+) diff --git a/syncplay/client.py b/syncplay/client.py index e31c44e..ff86c61 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -6,6 +6,7 @@ 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 @@ -62,6 +63,7 @@ class SyncplayClient(object): constants.SHOW_DIFFERENT_ROOM_OSD = config['showDifferentRoomOSD'] constants.SHOW_SAME_ROOM_OSD = config['showSameRoomOSD'] constants.SHOW_DURATION_NOTIFICATION = config['showDurationNotification'] + self.serverVersion = "0.0.0" self.lastLeftTime = 0 self.lastLeftUser = u"" self.protocolFactory = SyncClientFactory(self) @@ -335,6 +337,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_: @@ -416,6 +421,18 @@ 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 int(self.serverVersion.replace(".", "")) < int(minVersion.replace(".", "")): + 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("1.3.0") def createControlledRoom(self, roomName): controlPassword = RoomPasswordGenerator.generate_password() self.ui.showMessage(u"Attempting to create controlled room '{}' with password '{}'...".format(roomName, controlPassword)) @@ -434,6 +451,7 @@ class SyncplayClient(object): else: return "" + @requireMinServerVersion("1.3.0") def identifyAsController(self, controlPassword): controlPassword = self.stripControlPassword(controlPassword) self.ui.showMessage(u"Identifying as room controller with password '{}'...".format(controlPassword)) diff --git a/syncplay/protocols.py b/syncplay/protocols.py index 18da515..8603ea1 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -87,6 +87,7 @@ class SyncClientProtocol(JSONCommandProtocol): self._client.ui.showMessage(motd, True, True) self._client.ui.showMessage(getMessage("connected-successful-notification")) self._client.sendFile() + self._client.setServerVersion(version) def sendHello(self): hello = {} From 8cc3eddaf2cbf92da9f70455ce38ebb6c6ad34ac Mon Sep 17 00:00:00 2001 From: Et0h Date: Mon, 27 Oct 2014 15:07:07 +0000 Subject: [PATCH 52/58] Auto-re-identify as controller on reconnect / changing rooms (not saved on exit) --- syncplay/client.py | 27 ++++++++++++++++++++++++--- syncplay/protocols.py | 1 + 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index ff86c61..4242486 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -63,6 +63,8 @@ class SyncplayClient(object): constants.SHOW_DIFFERENT_ROOM_OSD = config['showDifferentRoomOSD'] constants.SHOW_SAME_ROOM_OSD = config['showSameRoomOSD'] constants.SHOW_DURATION_NOTIFICATION = config['showDurationNotification'] + self.controlpasswords = {} + self.lastControlPasswordAttempt = None self.serverVersion = "0.0.0" self.lastLeftTime = 0 self.lastLeftUser = u"" @@ -365,6 +367,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 @@ -436,6 +449,7 @@ class SyncplayClient(object): def createControlledRoom(self, roomName): controlPassword = RoomPasswordGenerator.generate_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): @@ -455,6 +469,7 @@ class SyncplayClient(object): def identifyAsController(self, controlPassword): controlPassword = self.stripControlPassword(controlPassword) self.ui.showMessage(u"Identifying as room controller with password '{}'...".format(controlPassword)) + self.lastControlPasswordAttempt = controlPassword self._protocol.requestControlledRoom(self.getRoom(), controlPassword) def controllerIdentificationError(self, username, room): @@ -465,11 +480,17 @@ class SyncplayClient(object): if self.userlist.isRoomSame(roomname): hideFromOSD = not constants.SHOW_SAME_ROOM_OSD self.ui.showMessage(u"<{}> authenticated as a room controller".format(username), hideFromOSD) + if username == self.userlist.currentUser.username: + self.storeControlPassword(roomname, self.lastControlPasswordAttempt) self.ui.userListChange() - # TODO: Mark person as a room controller - # TODO: Disable UI's "Create new Controlled Room when in Controlled Room" - # TODO: Disable authenticate when authenticated + 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): diff --git a/syncplay/protocols.py b/syncplay/protocols.py index 8603ea1..43f4395 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -86,6 +86,7 @@ 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) From 960cef15c9b37dab25f2e34bdb2b25adc7b8ca8c Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 1 Nov 2014 11:56:47 +0000 Subject: [PATCH 53/58] Hide label text if window is too narrow --- syncplay/ui/gui.py | 104 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 79 insertions(+), 25 deletions(-) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 204703f..5164751 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -1,5 +1,5 @@ from PySide import QtGui # @UnresolvedImport -from PySide.QtCore import Qt, QSettings, QSize, QPoint # @UnresolvedImport +from PySide.QtCore import Qt, QSettings, QSize, QPoint from syncplay import utils, constants, version from syncplay.messages import getMessage import sys @@ -60,26 +60,13 @@ class MainWindow(QtGui.QMainWindow): 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 @@ -175,6 +162,9 @@ class MainWindow(QtGui.QMainWindow): self.listTreeView.setItemsExpandable(False) self.listTreeView.setRootIsDecorated(False) self.listTreeView.expandAll() + self.updateListGeometry() + + def updateListGeometry(self): roomtocheck = 0 while self.listTreeModel.item(roomtocheck): self.listTreeView.setFirstColumnSpanned(roomtocheck, self.listTreeView.rootIndex(), True) @@ -262,6 +252,69 @@ class MainWindow(QtGui.QMainWindow): 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") @@ -402,9 +455,9 @@ 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) @@ -425,7 +478,7 @@ class MainWindow(QtGui.QMainWindow): 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")) @@ -446,7 +499,7 @@ class MainWindow(QtGui.QMainWindow): 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")) @@ -479,7 +532,7 @@ class MainWindow(QtGui.QMainWindow): 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): @@ -589,9 +642,10 @@ 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.WindowMinimizeButtonHint & ~Qt.WindowContextHelpButtonHint) self.show() - self.setAcceptDrops(True) \ No newline at end of file + self.setAcceptDrops(True) From e3d204aeb385ab4547ab2d6f0c467bbd90398ec2 Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 1 Nov 2014 15:24:13 +0000 Subject: [PATCH 54/58] Update list display on splitter move --- syncplay/ui/gui.py | 51 ++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 5164751..2a9e39e 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -1,4 +1,4 @@ -from PySide import QtGui # @UnresolvedImport +from PySide import QtGui from PySide.QtCore import Qt, QSettings, QSize, QPoint from syncplay import utils, constants, version from syncplay.messages import getMessage @@ -56,6 +56,19 @@ class UserlistItemDelegate(QtGui.QStyledItemDelegate): 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()) @@ -70,7 +83,6 @@ class MainWindow(QtGui.QMainWindow): except (): pass - def promptFor(self, prompt=">", message=""): # TODO: Prompt user return None @@ -165,21 +177,24 @@ class MainWindow(QtGui.QMainWindow): self.updateListGeometry() def updateListGeometry(self): - 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() + 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: @@ -402,7 +417,7 @@ class MainWindow(QtGui.QMainWindow): self.close() def addTopLayout(self, window): - window.topSplit = QtGui.QSplitter(Qt.Horizontal) + window.topSplit = self.topSplitter(Qt.Horizontal, self) window.outputLayout = QtGui.QVBoxLayout() window.outputbox = QtGui.QTextEdit() From 09df5072354af03afb7f0d8aa5530b847d208e6f Mon Sep 17 00:00:00 2001 From: Et0h Date: Sun, 2 Nov 2014 11:00:44 +0000 Subject: [PATCH 55/58] Add --salt to server. If no room control salt specified, generate random 10-char string and let admin know --- syncplay/client.py | 21 ++------------------- syncplay/messages.py | 4 ++-- syncplay/server.py | 13 ++++++++----- syncplay/utils.py | 27 +++++++++++++++++++++++++++ syncplayServer.py | 2 +- 5 files changed, 40 insertions(+), 27 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index 4242486..2d42674 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -1,7 +1,5 @@ import hashlib import os.path -import random -import string import time import re from twisted.internet.protocol import ClientFactory @@ -447,7 +445,8 @@ class SyncplayClient(object): @requireMinServerVersion("1.3.0") def createControlledRoom(self, roomName): - controlPassword = RoomPasswordGenerator.generate_password() + 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) @@ -772,20 +771,4 @@ class UiManager(object): def drop(self): self.__ui.drop() -class RoomPasswordGenerator(object): - @staticmethod - def generate_password(): - parts = ( - RoomPasswordGenerator._get_random_letters(2), - RoomPasswordGenerator._get_random_numbers(3), - RoomPasswordGenerator._get_random_numbers(3) - ) - 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)) diff --git a/syncplay/messages.py b/syncplay/messages.py index 26992d7..b5ad9a6 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -254,6 +254,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 @@ -262,11 +263,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 diff --git a/syncplay/server.py b/syncplay/server.py index f586be3..f2cad52 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -11,16 +11,18 @@ import codecs import os from string import Template import argparse -from syncplay.utils import RoomPasswordProvider, NotControlledRoom +from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomStringGenerator 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 - # TODO: Make salt come from more reasonable place - self._salt = str(random.random()) + 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() @@ -420,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/utils.py b/syncplay/utils.py index b1c52e7..9c2715d 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. @@ -241,5 +243,30 @@ class RoomPasswordProvider(object): 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() From e3255d1b92f932009565b118762c5ca039532098 Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 29 Nov 2014 17:17:39 +0000 Subject: [PATCH 56/58] Move controlled room messages to messages.py --- syncplay/client.py | 9 ++++----- syncplay/messages.py | 16 +++++++++++++++- syncplay/ui/gui.py | 14 +++++++------- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index 2d42674..1c34c04 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -445,8 +445,7 @@ class SyncplayClient(object): @requireMinServerVersion("1.3.0") def createControlledRoom(self, roomName): - controlPassword = utils.RandomStringGenerator.generate_room_ - password() + 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) @@ -467,18 +466,18 @@ class SyncplayClient(object): @requireMinServerVersion("1.3.0") def identifyAsController(self, controlPassword): controlPassword = self.stripControlPassword(controlPassword) - self.ui.showMessage(u"Identifying as room controller with password '{}'...".format(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(u"<{}> failed to identify as a room controller.".format(username)) + 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(u"<{}> authenticated as a room controller".format(username), hideFromOSD) + 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() diff --git a/syncplay/messages.py b/syncplay/messages.py index b5ad9a6..7d6d481 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", @@ -188,12 +192,22 @@ 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 diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 2a9e39e..c1765fc 100644 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -364,16 +364,16 @@ class MainWindow(QtGui.QMainWindow): self._syncplayClient._player.openFile(fileName) def createControlledRoom(self): - controlroom, ok = QtGui.QInputDialog.getText(self, "Create controlled room", - "Enter name of controlled room\r\n(see http://syncplay.pl/guide/ for usage instructions):", QtGui.QLineEdit.Normal, + 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): - tooltip = "Enter controller password for this room\r\n(see http://syncplay.pl/guide/ for usage instructions):" - name = "Identify as Room Controller" - controlpassword, ok = QtGui.QInputDialog.getText(self, name, tooltip, QtGui.QLineEdit.Normal, "") + 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) @@ -568,10 +568,10 @@ class MainWindow(QtGui.QMainWindow): window.setoffsetAction.triggered.connect(self.setOffset) window.createcontrolledroomAction = window.advancedMenu.addAction( - QtGui.QIcon(self.resourcespath + 'page_white_key.png'), "&Create controlled room suffix") + 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'), - "&Identify as room controller") + getMessage("identifyascontroller-menu-label")) window.identifyascontroller.triggered.connect(self.identifyAsController) window.menuBar.addMenu(window.advancedMenu) From c8e3426547203a860d519572405c713e3ad985c8 Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 29 Nov 2014 17:30:29 +0000 Subject: [PATCH 57/58] Improve version checking --- syncplay/client.py | 6 +++--- syncplay/constants.py | 1 + syncplay/server.py | 4 ++-- syncplay/utils.py | 7 +++++++ 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index 1c34c04..d8b1684 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -436,14 +436,14 @@ class SyncplayClient(object): def requireMinVersionDecorator(f): @wraps(f) def wrapper(self, *args, **kwds): - if int(self.serverVersion.replace(".", "")) < int(minVersion.replace(".", "")): + 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("1.3.0") + @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)) @@ -463,7 +463,7 @@ class SyncplayClient(object): else: return "" - @requireMinServerVersion("1.3.0") + @requireMinServerVersion(constants.CONTROLLED_ROOMS_MIN_VERSION) def identifyAsController(self, controlPassword): controlPassword = self.stripControlPassword(controlPassword) self.ui.showMessage(getMessage("identifying-as-controller-notification").format(controlPassword)) diff --git a/syncplay/constants.py b/syncplay/constants.py index 3ad462b..265921c 100644 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -52,6 +52,7 @@ 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", diff --git a/syncplay/server.py b/syncplay/server.py index f2cad52..221b262 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -11,7 +11,7 @@ import codecs import os from string import Template import argparse -from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomStringGenerator +from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomStringGenerator, meetsMinVersion class SyncFactory(Factory): def __init__(self, password='', motdFilePath=None, isolateRooms=False, salt=None): @@ -42,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() diff --git a/syncplay/utils.py b/syncplay/utils.py index 9c2715d..f787c9a 100644 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -210,6 +210,13 @@ def sameFileduration (duration1, duration2): else: return False +def meetsMinVersion(version, minVersion): + def versiontotuple(ver): + return tuple(map(int, ver.split("."))) + versionTuple = versiontotuple(version) + minVersionTuple = versiontotuple(minVersion) + return versionTuple >= minVersionTuple + class RoomPasswordProvider(object): CONTROLLED_ROOM_REGEX = re.compile("^\+(.*):(\w{12})$") PASSWORD_REGEX = re.compile("[A-Z]{2}-\d{3}-\d{3}") From 6143786b7459fbdc2b295ebc6b5ecdd8e197615d Mon Sep 17 00:00:00 2001 From: Et0h Date: Sat, 29 Nov 2014 17:34:16 +0000 Subject: [PATCH 58/58] Simply version check util --- syncplay/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/syncplay/utils.py b/syncplay/utils.py index f787c9a..d3ab921 100644 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -213,9 +213,7 @@ def sameFileduration (duration1, duration2): def meetsMinVersion(version, minVersion): def versiontotuple(ver): return tuple(map(int, ver.split("."))) - versionTuple = versiontotuple(version) - minVersionTuple = versiontotuple(minVersion) - return versionTuple >= minVersionTuple + return versiontotuple(version) >= versiontotuple(minVersion) class RoomPasswordProvider(object): CONTROLLED_ROOM_REGEX = re.compile("^\+(.*):(\w{12})$")