Merge remote-tracking branch 'origin/controlledRooms-new'

Conflicts:
	syncplay/client.py
	syncplay/constants.py
	syncplay/players/mplayer.py
	syncplay/ui/gui.py
This commit is contained in:
Uriziel 2014-11-29 18:51:27 +01:00
commit 5dc1288fd4
21 changed files with 783 additions and 212 deletions

View File

@ -616,7 +616,9 @@ guiIcons = ['resources/accept.png', 'resources/arrow_undo.png', 'resources/clock
'resources/timeline_marker.png','resources/control_play_blue.png',
'resources/mpc-hc.png','resources/mpc-hc64.png','resources/mplayer.png',
'resources/mpv.png','resources/vlc.png', 'resources/house.png', 'resources/film_link.png',
'resources/eye.png', 'resources/comments.png', 'resources/cog_delete.png', 'resources/bullet_black.png'
'resources/eye.png', 'resources/comments.png', 'resources/cog_delete.png', 'resources/chevrons_right.png',
'resources/user_key.png', 'resources/lock.png', 'resources/key_go.png', 'resources/page_white_key.png',
'resources/tick.png', 'resources/lock_open.png'
]
resources = ["resources/icon.ico", "resources/syncplay.png"]
resources.extend(guiIcons)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

BIN
resources/key_go.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

BIN
resources/lock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

BIN
resources/lock_open.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

BIN
resources/tick.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

BIN
resources/user_key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 B

View File

@ -1,3 +1,3 @@
version = '1.2.9'
milestone = 'Pineapple, Pulverize and Destroy!'
version = '1.3.0'
milestone = 'Akki'
projectURL = 'http://syncplay.pl/'

View File

@ -4,12 +4,12 @@ import time
import re
from twisted.internet.protocol import ClientFactory
from twisted.internet import reactor, task
from functools import wraps
from syncplay.protocols import SyncClientProtocol
from syncplay import utils, constants
from syncplay.messages import getMessage, getMissingStrings
import threading
from syncplay.messages import getMissingStrings, getMessage
from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \
PRIVACY_HIDDENFILENAME, FILENAME_STRIP_REGEX
PRIVACY_HIDDENFILENAME
import collections
class SyncClientFactory(ClientFactory):
@ -62,14 +62,20 @@ class SyncplayClient(object):
constants.SHOW_SAME_ROOM_OSD = config['showSameRoomOSD']
constants.SHOW_DURATION_NOTIFICATION = config['showDurationNotification']
constants.DEBUG_MODE = config['debug']
self.controlpasswords = {}
self.lastControlPasswordAttempt = None
self.serverVersion = "0.0.0"
self.lastLeftTime = 0
self.lastLeftUser = u""
self.protocolFactory = SyncClientFactory(self)
self.ui = UiManager(self, ui)
self.userlist = SyncplayUserlist(self.ui, self)
self._protocol = None
""":type : SyncClientProtocol|None"""
self._player = None
if config['room'] == None or config['room'] == '':
if config['room'] is None or config['room'] == '':
config['room'] = config['name'] # ticket #58
self.defaultRoom = config['room']
self.playerPositionBeforeLastSeek = 0.0
@ -100,7 +106,8 @@ class SyncplayClient(object):
self._warnings = self._WarningManager(self._player, self.userlist, self.ui)
if constants.LIST_RELATIVE_CONFIGS and self._config.has_key('loadedRelativePaths') and self._config['loadedRelativePaths']:
self.ui.showMessage(getMessage("relative-config-notification").format("; ".join(self._config['loadedRelativePaths'])), noPlayer=True, noTimestamp=True)
paths = "; ".join(self._config['loadedRelativePaths'])
self.ui.showMessage(getMessage("relative-config-notification").format(paths), noPlayer=True, noTimestamp=True)
if constants.DEBUG_MODE and constants.WARN_ABOUT_MISSING_STRINGS:
missingStrings = getMissingStrings()
@ -229,7 +236,7 @@ class SyncplayClient(object):
def _changePlayerStateAccordingToGlobalState(self, position, paused, doSeek, setBy):
madeChangeOnPlayer = False
pauseChanged = paused != self.getGlobalPaused()
pauseChanged = paused != self.getGlobalPaused() or paused != self.getPlayerPaused()
diff = self.getPlayerPosition() - position
if self._lastGlobalUpdate is None:
madeChangeOnPlayer = self._initPlayerState(position, paused)
@ -240,7 +247,7 @@ class SyncplayClient(object):
madeChangeOnPlayer = self._serverSeeked(position, setBy)
if diff > self._config['rewindThreshold'] and not doSeek and not self._config['rewindOnDesync'] == False:
madeChangeOnPlayer = self._rewindPlayerDueToTimeDifference(position, setBy)
if (self._player.speedSupported and not doSeek and not paused and not self._config['slowOnDesync'] == False):
if self._player.speedSupported and not doSeek and not paused and not self._config['slowOnDesync'] == False:
madeChangeOnPlayer = self._slowDownToCoverTimeDifference(diff, setBy)
if paused == False and pauseChanged:
madeChangeOnPlayer = self._serverUnpaused(setBy)
@ -323,7 +330,6 @@ class SyncplayClient(object):
size = os.path.getsize(path)
except OSError: # file not accessible (stream?)
size = 0
rawfilename = filename
filename, size = self.__executePrivacySettings(filename, size)
self.userlist.currentUser.setFile(filename, duration, size)
self.sendFile()
@ -339,6 +345,9 @@ class SyncplayClient(object):
size = 0
return filename, size
def setServerVersion(self, version):
self.serverVersion = version
def sendFile(self):
file_ = self.userlist.currentUser.file
if self._protocol and self._protocol.logged and file_:
@ -364,6 +373,17 @@ class SyncplayClient(object):
if self._protocol and self._protocol.logged and room:
self._protocol.sendRoomSetting(room)
self.getUserList()
self.reIdentifyAsController()
def reIdentifyAsController(self):
room = self.userlist.currentUser.room
if utils.RoomPasswordProvider.isControlledRoom(room):
storedRoomPassword = self.getControlledRoomPassword(room)
if storedRoomPassword:
self.identifyAsController(storedRoomPassword)
def connected(self):
self.reIdentifyAsController()
def getRoom(self):
return self.userlist.currentUser.room
@ -420,6 +440,64 @@ class SyncplayClient(object):
if promptForAction:
self.ui.promptFor(getMessage("enter-to-exit-prompt"))
def requireMinServerVersion(minVersion):
def requireMinVersionDecorator(f):
@wraps(f)
def wrapper(self, *args, **kwds):
if not utils.meetsMinVersion(self.serverVersion,minVersion):
self.ui.showErrorMessage(u"This feature is not supported by the server. The feature requires a server running Syncplay {}+, but the server is running Syncplay {}.".format(minVersion, self.serverVersion))
return
return f(self, *args, **kwds)
return wrapper
return requireMinVersionDecorator
@requireMinServerVersion(constants.CONTROLLED_ROOMS_MIN_VERSION)
def createControlledRoom(self, roomName):
controlPassword = utils.RandomStringGenerator.generate_room_password()
self.ui.showMessage(u"Attempting to create controlled room '{}' with password '{}'...".format(roomName, controlPassword))
self.lastControlPasswordAttempt = controlPassword
self._protocol.requestControlledRoom(roomName, controlPassword)
def controlledRoomCreated(self, roomName, controlPassword):
self.ui.showMessage(u"Created controlled room '{}' with password '{}'. Please save this information for future reference!".format(roomName, controlPassword))
self.setRoom(roomName)
self.sendRoom()
self._protocol.requestControlledRoom(roomName, controlPassword)
self.ui.updateRoomName(roomName)
def stripControlPassword(self, controlPassword):
if controlPassword:
return re.sub(constants.CONTROL_PASSWORD_STRIP_REGEX, "", controlPassword).upper()
else:
return ""
@requireMinServerVersion(constants.CONTROLLED_ROOMS_MIN_VERSION)
def identifyAsController(self, controlPassword):
controlPassword = self.stripControlPassword(controlPassword)
self.ui.showMessage(getMessage("identifying-as-controller-notification").format(controlPassword))
self.lastControlPasswordAttempt = controlPassword
self._protocol.requestControlledRoom(self.getRoom(), controlPassword)
def controllerIdentificationError(self, username, room):
self.ui.showErrorMessage(getMessage("failed-to-identify-as-controller-notification").format(username))
def controllerIdentificationSuccess(self, username, roomname):
self.userlist.setUserAsController(username)
if self.userlist.isRoomSame(roomname):
hideFromOSD = not constants.SHOW_SAME_ROOM_OSD
self.ui.showMessage(getMessage("authenticated-as-controller-notification").format(username), hideFromOSD)
if username == self.userlist.currentUser.username:
self.storeControlPassword(roomname, self.lastControlPasswordAttempt)
self.ui.userListChange()
def storeControlPassword(self, room, password):
if password:
self.controlpasswords[room] = password
def getControlledRoomPassword(self, room):
if self.controlpasswords.has_key(room):
return self.controlpasswords[room]
class _WarningManager(object):
def __init__(self, player, userlist, ui):
self._player = player
@ -472,6 +550,7 @@ class SyncplayUser(object):
self.username = username
self.room = room
self.file = file_
self._controller = False
def setFile(self, filename, duration, size):
file_ = {
@ -490,7 +569,10 @@ class SyncplayUser(object):
return sameName and sameSize and sameDuration
def __lt__(self, other):
return self.username.lower() < other.username.lower()
if self.isController() == other.isController():
return self.username.lower() < other.username.lower()
else:
return self.isController() > other.isController()
def __repr__(self, *args, **kwargs):
if self.file:
@ -498,6 +580,12 @@ class SyncplayUser(object):
else:
return "{}".format(self.username)
def setControllerStatus(self, isController):
self._controller = isController
def isController(self):
return self._controller
class SyncplayUserlist(object):
def __init__(self, ui, client):
self.currentUser = SyncplayUser()
@ -544,10 +632,14 @@ class SyncplayUserlist(object):
message = getMessage("file-differences-notification") + ", ".join(differences)
self.ui.showMessage(message, not constants.SHOW_OSD_WARNINGS)
def addUser(self, username, room, file_, noMessage=False):
def addUser(self, username, room, file_, noMessage=False, isController=None):
if username == self.currentUser.username:
if isController is not None:
self.currentUser.setControllerStatus(isController)
return
user = SyncplayUser(username, room, file_)
if isController is not None:
user.setControllerStatus(isController)
self._users[username] = user
if not noMessage:
self.__showUserChangeMessage(username, room, file_)
@ -578,6 +670,8 @@ class SyncplayUserlist(object):
if self._users.has_key(username):
user = self._users[username]
oldRoom = user.room if user.room else None
if user.room != room:
user.setControllerStatus(isController=False)
self.__displayModUserMessage(username, room, file_, user, oldRoom)
user.room = room
if file_:
@ -588,6 +682,13 @@ class SyncplayUserlist(object):
self.addUser(username, room, file_)
self.userListChange()
def setUserAsController(self, username):
if self.currentUser.username == username:
self.currentUser.setControllerStatus(True)
elif self._users.has_key(username):
user = self._users[username]
user.setControllerStatus(True)
def areAllFilesInRoomSame(self):
for user in self._users.itervalues():
if user.room == self.currentUser.room and user.file and not self.currentUser.isFileSame(user.file):
@ -658,6 +759,9 @@ class UiManager(object):
if constants.SHOW_OSD and self._client._player:
self._client._player.displayMessage(message, duration * 1000)
def setControllerStatus(self, username, isController):
self.__ui.setControllerStatus(username, isController)
def showErrorMessage(self, message, criticalerror=False):
if message <> self.lastError: # Avoid double call bug
self.lastError = message
@ -672,5 +776,10 @@ class UiManager(object):
def markEndOfUserlist(self):
self.__ui.markEndOfUserlist()
def updateRoomName(self, room=""):
self.__ui.updateRoomName(room)
def drop(self):
self.__ui.drop()

View File

@ -42,14 +42,19 @@ MERGE_PLAYPAUSE_BUTTONS = False
SYNC_ON_PAUSE = True # Client seek to global position - subtitles may disappear on some media players
#Usually there's no need to adjust these
FILENAME_STRIP_REGEX = u"[-~_\.\[\](): ]"
CONTROL_PASSWORD_STRIP_REGEX = u"[^a-zA-Z0-9\-]"
ROOM_NAME_STRIP_REGEX = u"^(\+)(?P<roomnamebase>.*)(:)(\w{12})$"
COMMANDS_UNDO = ["u", "undo", "revert"]
COMMANDS_LIST = ["l", "list", "users"]
COMMANDS_PAUSE = ["p", "play", "pause"]
COMMANDS_ROOM = ["r", "room"]
COMMANDS_HELP = ['help', 'h', '?', '/?', r'\?']
COMMANDS_CREATE = ['c','create']
COMMANDS_AUTH = ['a','auth']
MPC_MIN_VER = "1.6.4"
VLC_MIN_VERSION = "2.0.0"
VLC_INTERFACE_MIN_VERSION = "0.2.1"
CONTROLLED_ROOMS_MIN_VERSION = "1.3.0"
MPC_PATHS = [
r"C:\Program Files (x86)\MPC-HC\mpc-hc.exe",
r"C:\Program Files\MPC-HC\mpc-hc.exe",
@ -99,8 +104,8 @@ VLC_MAX_PORT = 55000
#These are not changes you're looking for
STYLE_TABLIST = "QListWidget::item { color: black; border-style: solid; border-width: 1px; border-radius: 2px; } QListWidget::item:selected { background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(242, 248, 255, 255), stop:1 rgba(208, 229, 255, 255)); border-color: #84ACDD; } QListWidget::item:!selected { border-color: transparent; } QListWidget::item:!selected:hover { background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(248, 248, 248, 255), stop:1 rgba(229, 229, 229, 255)); border-color: silver; }"
STYLE_SUBCHECKBOX = "QCheckBox, QLabel {{ margin-left: 8px; padding-left: 18px; background:url('{}') left no-repeat }}" #Graphic path
STYLE_SUBLABEL = "QCheckBox, QLabel {{ margin-left: 8px; padding-left: 14px; background:url('{}') left no-repeat }}" #Graphic path
STYLE_SUBCHECKBOX = "QCheckBox, QLabel {{ margin-left: 6px; padding-left: 21px; background:url('{}') left no-repeat }}" #Graphic path
STYLE_SUBLABEL = "QCheckBox, QLabel {{ margin-left: 6px; padding-left: 16px; background:url('{}') left no-repeat }}" #Graphic path
STYLE_ERRORLABEL = "QLabel { color : black; border-style: outset; border-width: 2px; border-radius: 7px; border-color: red; padding: 2px; background: #FFAAAA; }"
STYLE_SUCCESSLABEL = "QLabel { color : black; border-style: outset; border-width: 2px; border-radius: 7px; border-color: green; padding: 2px; background: #AAFFAA; }"
STYLE_NOTIFICATIONBOX = "Username { color: #367AA9; font-weight:bold; }"
@ -108,6 +113,9 @@ STYLE_USERNAME = "color: #367AA9; font-weight:bold;"
STYLE_ERRORNOTIFICATION = "color: red;"
STYLE_DIFFERENTITEM_COLOR = 'red'
STYLE_NOFILEITEM_COLOR = 'blue'
STYLE_NOTCONTROLLER_COLOR = 'grey'
USERLIST_GUI_USERNAME_OFFSET = 21 # Pixels
MPLAYER_SLAVE_ARGS = ['-slave', '--hr-seek=always', '-nomsgcolor', '-msglevel', 'all=1:global=4:cplayer=4', '-af', 'scaletempo']
# --quiet works with both mpv 0.2 and 0.3
@ -135,3 +143,5 @@ ERROR_MESSAGE_MARKER = "*"
LOAD_SAVE_MANUALLY_MARKER = "!"
CONFIG_NAME_MARKER = ":"
CONFIG_VALUE_MARKER = "="
USERITEM_CONTROLLER_ROLE = 0
USERITEM_READY_ROLE = 1

View File

@ -31,8 +31,12 @@ en = {
"playing-notification" : "<{}> is playing '{}' ({})", # User, file, duration
"playing-notification/room-addendum" : " in room: '{}'", # Room
"identifying-as-controller-notification" : u"Identifying as room controller with password '{}'...",
"failed-to-identify-as-controller-notification" : u"<{}> failed to identify as a room controller.",
"authenticated-as-controller-notification" : u"<{}> authenticated as a room controller",
"file-different-notification" : "File you are playing appears to be different from <{}>'s", # User
"file-differences-notification" : "Your file differs in the following way(s): ",
"file-differences-notification" : "Your file differs in the following way(s): ", # controlPassword
"room-files-not-same" : "Not all files played in the room are the same",
"alone-in-the-room": "You're alone in the room",
@ -53,6 +57,8 @@ en = {
"commandlist-notification/pause" : "\tp - toggle pause",
"commandlist-notification/seek" : "\t[s][+-]time - seek to the given value of time, if + or - is not specified it's absolute time in seconds or min:sec",
"commandlist-notification/help" : "\th - this help",
"commandlist-notification/create" : "\tc [name] - create controlled room using name of current room",
"commandlist-notification/auth" : "\ta [password] - authenticate as room controller with controller password",
"syncplay-version-notification" : "Syncplay version: {}", # syncplay.version
"more-info-notification" : "More info available at: {}", # projectURL
@ -175,7 +181,9 @@ en = {
"pause-guibuttonlabel" : "Pause",
"roomuser-heading-label" : "Room / User",
"fileplayed-heading-label" : "File being played",
"size-heading-label" : "Size",
"duration-heading-label" : "Length",
"filename-heading-label" : "Filename",
"notifications-heading-label" : "Notifications",
"userlist-heading-label" : "List of who is playing what",
"othercommands-heading-label" : "Other commands",
@ -189,12 +197,24 @@ en = {
"exit-menu-label" : "E&xit",
"advanced-menu-label" : "&Advanced",
"setoffset-menu-label" : "Set &offset",
"createcontrolledroom-menu-label" : "&Create controlled room",
"identifyascontroller-menu-label" : "&Identify as room controller",
"help-menu-label" : "&Help",
"userguide-menu-label" : "Open user &guide",
"setoffset-msgbox-label" : "Set offset",
"offsetinfo-msgbox-label" : "Offset (see http://syncplay.pl/guide/ for usage instructions):",
"createcontrolledroom-msgbox-label" : "Create controlled room",
"controlledroominfo-msgbox-label" : "Enter name of controlled room\r\n(see http://syncplay.pl/guide/ for usage instructions):",
"identifyascontroller-msgbox-label" : "Identify as Room Controller",
"identifyinfo-msgbox-label" : "Enter controller password for this room\r\n(see http://syncplay.pl/guide/ for usage instructions):",
"megabyte-suffix" : " MB",
# Tooltips
"host-tooltip" : "Hostname or IP to connect to, optionally including port (e.g. syncplay.pl:8999). Only synchronised with people on same server/port.",
@ -253,6 +273,7 @@ en = {
"welcome-server-notification" : "Welcome to Syncplay server, ver. {0}", # version
"client-connected-room-server-notification" : "{0}({2}) connected to room '{1}'", # username, host, room
"client-left-server-notification" : "{0} left server", # name
"no-salt-notification" : "PLEASE NOTE: To allow room control passwords generated by this server instance to still work when the server is restarted, please add the following command line argument when running the Syncplay server in the future: --salt {}", #Salt
# Server arguments
@ -261,11 +282,10 @@ en = {
"server-port-argument" : 'server TCP port',
"server-password-argument" : 'server password',
"server-isolate-room-argument" : 'should rooms be isolated?',
"server-salt-argument" : "random string used to generate controlled room passwords",
"server-motd-argument": "path to file from which motd will be fetched",
"server-messed-up-motd-unescaped-placeholders": "Message of the Day has unescaped placeholders. All $ signs should be doubled ($$).",
"server-messed-up-motd-too-long": "Message of the Day is too long - maximum of {} chars, {} given.",
"server-irc-verbose": "Should server actively report changes in rooms",
"server-irc-config": "Path to irc bot config files",
# Server errors
"unknown-command-server-error" : "Unknown command {}", # message
@ -275,9 +295,6 @@ en = {
"password-required-server-error" : "Password required",
"wrong-password-server-error" : "Wrong password supplied",
"hello-server-error" : "Not enough Hello arguments",
"version-mismatch-server-error" : "Mismatch between versions of client and server"
}
pl = {

View File

@ -145,6 +145,14 @@ class MplayerPlayer(BasePlayer):
def lineReceived(self, line):
if line:
self._client.ui.showDebugMessage("player << {}".format(line))
if "Failed to get value of property" in line:
if "filename" in line:
self._getFilename()
elif "length" in line:
self._getLength()
elif "path" in line:
self._getFilepath()
return
match = self.RE_ANSWER.match(line)
if not match:
self._handleUnknownLine(line)

View File

@ -76,6 +76,7 @@ class SyncClientProtocol(JSONCommandProtocol):
username = hello["username"] if hello.has_key("username") else None
roomName = hello["room"]["name"] if hello.has_key("room") else None
version = hello["version"] if hello.has_key("version") else None
version = hello["realversion"] if hello.has_key("realversion") else version # Used for 1.2.X compatibility
motd = hello["motd"] if hello.has_key("motd") else None
return username, roomName, version, motd
@ -83,8 +84,6 @@ class SyncClientProtocol(JSONCommandProtocol):
username, roomName, version, motd = self._extractHelloArguments(hello)
if not username or not roomName or not version:
self.dropWithError(getMessage("hello-server-error").format(hello))
elif version.split(".")[0:2] != syncplay.version.split(".")[0:2]:
self.dropWithError(getMessage("version-mismatch-server-error".format(hello)))
else:
self._client.setUsername(username)
self._client.setRoom(roomName)
@ -92,7 +91,9 @@ class SyncClientProtocol(JSONCommandProtocol):
if motd:
self._client.ui.showMessage(motd, True, True)
self._client.ui.showMessage(getMessage("connected-successful-notification"))
self._client.connected()
self._client.sendFile()
self._client.setServerVersion(version)
def sendHello(self):
hello = {}
@ -101,7 +102,8 @@ class SyncClientProtocol(JSONCommandProtocol):
if password: hello["password"] = password
room = self._client.getRoom()
if room: hello["room"] = {"name" :room}
hello["version"] = syncplay.version
hello["version"] = "1.2.255" # Used so newer clients work on 1.2.X server
hello["realversion"] = syncplay.version
self.sendMessage({"Hello": hello})
def _SetUser(self, users):
@ -119,13 +121,21 @@ class SyncClientProtocol(JSONCommandProtocol):
self._client.userlist.modUser(username, room, file_)
def handleSet(self, settings):
for set_ in settings.iteritems():
command = set_[0]
for (command, values) in settings.iteritems():
if command == "room":
roomName = set_[1]["name"] if set_[1].has_key("name") else None
roomName = values["name"] if values.has_key("name") else None
self._client.setRoom(roomName)
elif command == "user":
self._SetUser(set_[1])
self._SetUser(values)
elif command == "controllerAuth":
if values['success']:
self._client.controllerIdentificationSuccess(values["user"], values["room"])
else:
self._client.controllerIdentificationError(values["user"], values["room"])
elif command == "newControlledRoom":
controlPassword = values['password']
roomName = values['roomName']
self._client.controlledRoomCreated(roomName, controlPassword)
def sendSet(self, setting):
self.sendMessage({"Set": setting})
@ -147,7 +157,8 @@ class SyncClientProtocol(JSONCommandProtocol):
for user in room[1].iteritems():
userName = user[0]
file_ = user[1]['file'] if user[1]['file'] <> {} else None
self._client.userlist.addUser(userName, roomName, file_, noMessage=True)
isController = user[1]['controller'] if 'controller' in user[1] else False
self._client.userlist.addUser(userName, roomName, file_, noMessage=True, isController=isController)
self._client.userlist.showUserList()
def sendList(self):
@ -215,8 +226,16 @@ class SyncClientProtocol(JSONCommandProtocol):
state["ignoringOnTheFly"]["client"] = self.clientIgnoringOnTheFly
self.sendMessage({"State": state})
def requestControlledRoom(self, room, password):
self.sendSet({
"controllerAuth": {
"room": room,
"password": password
}
})
def handleError(self, error):
self.dropWithError(error["message"]) # TODO: more processing and fallbacking
self.dropWithError(error["message"])
def sendError(self, message):
self.sendMessage({"Error": {"message": message}})
@ -261,7 +280,7 @@ class SyncServerProtocol(JSONCommandProtocol):
return self._logged
def _extractHelloArguments(self, hello):
roomName, roomPassword = None, None
roomName = None
username = hello["username"] if hello.has_key("username") else None
username = username.strip()
serverPassword = hello["password"] if hello.has_key("password") else None
@ -269,9 +288,9 @@ class SyncServerProtocol(JSONCommandProtocol):
if room:
roomName = room["name"] if room.has_key("name") else None
roomName = roomName.strip()
roomPassword = room["password"] if room.has_key("password") else None
version = hello["version"] if hello.has_key("version") else None
return username, serverPassword, roomName, roomPassword, version
version = hello["realversion"] if hello.has_key("realversion") else version
return username, serverPassword, roomName, version
def _checkPassword(self, serverPassword):
if self._factory.password:
@ -284,15 +303,14 @@ class SyncServerProtocol(JSONCommandProtocol):
return True
def handleHello(self, hello):
username, serverPassword, roomName, roomPassword, version = self._extractHelloArguments(hello)
username, serverPassword, roomName, version = self._extractHelloArguments(hello)
if not username or not roomName or not version:
self.dropWithError(getMessage("hello-server-error"))
elif version.split(".")[0:2] != syncplay.version.split(".")[0:2]:
self.dropWithError(getMessage("version-mismatch-server-error"))
return
else:
if not self._checkPassword(serverPassword):
return
self._factory.addWatcher(self, username, roomName, roomPassword)
self._factory.addWatcher(self, username, roomName)
self._logged = True
self.sendHello(version)
@ -306,7 +324,8 @@ class SyncServerProtocol(JSONCommandProtocol):
userIp = self.transport.getPeer().host
room = self._watcher.getRoom()
if room: hello["room"] = {"name": room.getName()}
hello["version"] = syncplay.version
hello["version"] = clientVersion # Used so 1.2.X client works on newer server
hello["realversion"] = syncplay.version
hello["motd"] = self._factory.getMotd(userIp, username, room, clientVersion)
self.sendMessage({"Hello": hello})
@ -319,14 +338,34 @@ class SyncServerProtocol(JSONCommandProtocol):
self._factory.setWatcherRoom(self._watcher, roomName)
elif command == "file":
self._watcher.setFile(set_[1])
elif command == "controllerAuth":
password = set_[1]["password"] if set_[1].has_key("password") else None
room = set_[1]["room"] if set_[1].has_key("room") else None
self._factory.authRoomController(self._watcher, password, room)
def sendSet(self, setting):
self.sendMessage({"Set": setting})
def sendNewControlledRoom(self, roomName, password):
self.sendSet({
"newControlledRoom": {
"password": password,
"roomName": roomName
}
})
def sendControlledRoomAuthStatus(self, success, username, roomname):
self.sendSet({
"controllerAuth": {
"user": username,
"room": roomname,
"success": success
}
})
def sendUserSetting(self, username, room, file_, event):
room = {"name": room.getName()}
user = {}
user[username] = {}
user = {username: {}}
user[username]["room"] = room
if file_:
user[username]["file"] = file_
@ -339,7 +378,11 @@ class SyncServerProtocol(JSONCommandProtocol):
if room:
if room.getName() not in userlist:
userlist[room.getName()] = {}
userFile = { "position": 0, "file": watcher.getFile() if watcher.getFile() else {} }
userFile = {
"position": 0,
"file": watcher.getFile() if watcher.getFile() else {},
"controller": watcher.isController()
}
userlist[room.getName()][watcher.getName()] = userFile
def sendList(self):
@ -362,8 +405,8 @@ class SyncServerProtocol(JSONCommandProtocol):
"position": position if position else 0,
"paused": paused,
"doSeek": doSeek,
"setBy": setBy.getName()
}
"setBy": setBy.getName() if setBy else None
}
ping = {
"latencyCalculation": self._pingService.newTimestamp(),
"serverRtt": self._pingService.getRtt()

View File

@ -1,4 +1,5 @@
import hashlib
import random
from twisted.internet import task, reactor
from twisted.internet.protocol import Factory
import syncplay
@ -10,13 +11,18 @@ import codecs
import os
from string import Template
import argparse
from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomStringGenerator, meetsMinVersion
class SyncFactory(Factory):
def __init__(self, password='', motdFilePath=None, isolateRooms=False):
def __init__(self, password='', motdFilePath=None, isolateRooms=False, salt=None):
print getMessage("welcome-server-notification").format(syncplay.version)
if password:
password = hashlib.md5(password).hexdigest()
self.password = password
if salt is None:
salt = RandomStringGenerator.generate_server_salt()
print getMessage("no-salt-notification").format(salt)
self._salt = salt
self._motdFilePath = motdFilePath
if not isolateRooms:
self._roomManager = RoomManager()
@ -36,7 +42,7 @@ class SyncFactory(Factory):
def getMotd(self, userIp, username, room, clientVersion):
oldClient = False
if constants.WARN_OLD_CLIENTS:
if int(clientVersion.replace(".", "")) < int(constants.RECENT_CLIENT_THRESHOLD.replace(".", "")):
if not meetsMinVersion(clientVersion, constants.RECENT_CLIENT_THRESHOLD):
oldClient = True
if self._motdFilePath and os.path.isfile(self._motdFilePath):
tmpl = codecs.open(self._motdFilePath, "r", "utf-8-sig").read()
@ -54,7 +60,7 @@ class SyncFactory(Factory):
else:
return ""
def addWatcher(self, watcherProtocol, username, roomName, roomPassword):
def addWatcher(self, watcherProtocol, username, roomName):
username = self._roomManager.findFreeUsername(username)
watcher = Watcher(self, watcherProtocol, username)
self.setWatcherRoom(watcher, roomName, asJoin=True)
@ -65,13 +71,16 @@ class SyncFactory(Factory):
self.sendJoinMessage(watcher)
else:
self.sendRoomSwitchMessage(watcher)
if RoomPasswordProvider.isControlledRoom(roomName):
for controller in watcher.getRoom().getControllers():
watcher.sendControlledRoomAuthStatus(True, controller, roomName)
def sendRoomSwitchMessage(self, watcher):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None)
self._roomManager.broadcast(watcher, l)
def removeWatcher(self, watcher):
if watcher.getRoom():
if watcher and watcher.getRoom():
self.sendLeftMessage(watcher)
self._roomManager.removeWatcher(watcher)
@ -83,21 +92,40 @@ class SyncFactory(Factory):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"joined": True}) if w != watcher else None
self._roomManager.broadcast(watcher, l)
def sendFileUpdate(self, watcher, file_):
def sendFileUpdate(self, watcher):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), watcher.getFile(), None)
self._roomManager.broadcast(watcher, l)
def forcePositionUpdate(self, room, watcher, doSeek):
def forcePositionUpdate(self, watcher, doSeek, watcherPauseState):
room = watcher.getRoom()
paused, position = room.isPaused(), watcher.getPosition()
setBy = watcher
room.setPosition(watcher.getPosition(), setBy)
l = lambda w: w.sendState(position, paused, doSeek, setBy, True)
self._roomManager.broadcastRoom(watcher, l)
if room.canControl(watcher):
paused, position = room.isPaused(), watcher.getPosition()
setBy = watcher
l = lambda w: w.sendState(position, paused, doSeek, setBy, True)
room.setPosition(watcher.getPosition(), setBy)
self._roomManager.broadcastRoom(watcher, l)
else:
watcher.sendState(room.getPosition(), watcherPauseState, False, watcher, True) # Fixes BC break with 1.2.x
watcher.sendState(room.getPosition(), room.isPaused(), True, room.getSetBy(), True)
def getAllWatchersForUser(self, forUser):
return self._roomManager.getAllWatchersForUser(forUser)
def authRoomController(self, watcher, password, roomBaseName=None):
room = watcher.getRoom()
roomName = roomBaseName if roomBaseName else room.getName()
try:
success = RoomPasswordProvider.check(roomName, password, self._salt)
if success:
watcher.getRoom().addController(watcher)
self._roomManager.broadcast(watcher, lambda w: w.sendControlledRoomAuthStatus(success, watcher.getName(), room._name))
except NotControlledRoom:
newName = RoomPasswordProvider.getControlledRoomName(roomName, password, self._salt)
watcher.sendNewControlledRoom(newName, password)
except ValueError:
self._roomManager.broadcastRoom(watcher, lambda w: w.sendControlledRoomAuthStatus(False, watcher.getName(), room._name))
class RoomManager(object):
def __init__(self):
self._rooms = {}
@ -135,7 +163,10 @@ class RoomManager(object):
if roomName in self._rooms:
return self._rooms[roomName]
else:
room = Room(roomName)
if RoomPasswordProvider.isControlledRoom(roomName):
room = ControlledRoom(roomName)
else:
room = Room(roomName)
self._rooms[roomName] = room
return room
@ -228,6 +259,44 @@ class Room(object):
def getSetBy(self):
return self._setBy
def canControl(self, watcher):
return True
class ControlledRoom(Room):
def __init__(self, name):
Room.__init__(self, name)
self._controllers = {}
def getPosition(self):
if self._controllers:
watcher = min(self._controllers.values())
self._setBy = watcher
return watcher.getPosition()
else:
return 0
def addController(self, watcher):
self._controllers[watcher.getName()] = watcher
def removeWatcher(self, watcher):
Room.removeWatcher(self, watcher)
if watcher.getName() in self._controllers:
del self._controllers[watcher.getName()]
def setPaused(self, paused=Room.STATE_PAUSED, setBy=None):
if self.canControl(setBy):
Room.setPaused(self, paused, setBy)
def setPosition(self, position, setBy=None):
if self.canControl(setBy):
Room.setPosition(self, position, setBy)
def canControl(self, watcher):
return watcher.getName() in self._controllers
def getControllers(self):
return self._controllers
class Watcher(object):
def __init__(self, server, connector, name):
self._server = server
@ -243,7 +312,7 @@ class Watcher(object):
def setFile(self, file_):
self._file = file_
self._server.sendFileUpdate(self, file_)
self._server.sendFileUpdate(self)
def setRoom(self, room):
self._room = room
@ -277,16 +346,22 @@ class Watcher(object):
def sendSetting(self, user, room, file_, event):
self._connector.sendUserSetting(user, room, file_, event)
def sendNewControlledRoom(self, roomBaseName, password):
self._connector.sendNewControlledRoom(roomBaseName, password)
def sendControlledRoomAuthStatus(self, success, username, room):
self._connector.sendControlledRoomAuthStatus(success, username, room)
def __lt__(self, b):
if self.getPosition() is None or self._file is None:
return False
if b.getPosition is None or b._file is None:
if b.getPosition() is None or b.getFile() is None:
return True
return self.getPosition() < b.getPosition()
def _scheduleSendState(self):
self._sendStateTimer = task.LoopingCall(self._askForStateUpdate)
self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL, True)
self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL)
def _askForStateUpdate(self, doSeek=False, forcedUpdate=False):
self._server.sendState(self, doSeek, forcedUpdate)
@ -313,18 +388,25 @@ class Watcher(object):
return False
return self._room.isPaused() and not paused or not self._room.isPaused() and paused
def _updatePositionByAge(self, messageAge, paused, position):
if not paused:
position += messageAge
return position
def updateState(self, position, paused, doSeek, messageAge):
pauseChanged = self.__hasPauseChanged(paused)
self._lastUpdatedOn = time.time()
if pauseChanged:
self.getRoom().setPaused(Room.STATE_PAUSED if paused else Room.STATE_PLAYING, self)
if position is not None:
if not paused:
position += messageAge
position = self._updatePositionByAge(messageAge, paused, position)
self.setPosition(position)
if doSeek or pauseChanged:
self._server.forcePositionUpdate(self._room, self, doSeek)
self._server.forcePositionUpdate(self, doSeek, paused)
def isController(self):
return RoomPasswordProvider.isControlledRoom(self._room.getName()) \
and self._room.canControl(self)
class ConfigurationGetter(object):
def getConfiguration(self):
@ -340,4 +422,5 @@ class ConfigurationGetter(object):
self._argparser.add_argument('--port', metavar='port', type=str, nargs='?', help=getMessage("server-port-argument"))
self._argparser.add_argument('--password', metavar='password', type=str, nargs='?', help=getMessage("server-password-argument"))
self._argparser.add_argument('--isolate-rooms', action='store_true', help=getMessage("server-isolate-room-argument"))
self._argparser.add_argument('--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"))

View File

@ -426,7 +426,7 @@ class ConfigDialog(QtGui.QDialog):
self.desyncFrame.setMidLineWidth(0)
self.slowdownThresholdLabel = QLabel(getMessage("slowdown-threshold-label"), self)
self.slowdownThresholdLabel.setStyleSheet(constants.STYLE_SUBLABEL.format(self.posixresourcespath + "bullet_black.png"))
self.slowdownThresholdLabel.setStyleSheet(constants.STYLE_SUBLABEL.format(self.posixresourcespath + "chevrons_right.png"))
self.slowdownThresholdSpinbox = QDoubleSpinBox()
try:
@ -443,7 +443,7 @@ class ConfigDialog(QtGui.QDialog):
self.slowdownThresholdSpinbox.adjustSize()
self.rewindThresholdLabel = QLabel(getMessage("rewind-threshold-label"), self)
self.rewindThresholdLabel.setStyleSheet(constants.STYLE_SUBLABEL.format(self.posixresourcespath + "bullet_black.png"))
self.rewindThresholdLabel.setStyleSheet(constants.STYLE_SUBLABEL.format(self.posixresourcespath + "chevrons_right.png"))
self.rewindThresholdSpinbox = QDoubleSpinBox()
try:
rewindThreshold = float(config['rewindThreshold'])
@ -515,22 +515,22 @@ class ConfigDialog(QtGui.QDialog):
self.showSameRoomOSDCheckbox = QCheckBox(getMessage("showsameroomosd-label"))
self.showSameRoomOSDCheckbox.setObjectName("showSameRoomOSD")
self.showSameRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "bullet_black.png"))
self.showSameRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.showSameRoomOSDCheckbox)
self.showDifferentRoomOSDCheckbox = QCheckBox(getMessage("showdifferentroomosd-label"))
self.showDifferentRoomOSDCheckbox.setObjectName("showDifferentRoomOSD")
self.showDifferentRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "bullet_black.png"))
self.showDifferentRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.showDifferentRoomOSDCheckbox)
self.slowdownOSDCheckbox = QCheckBox(getMessage("showslowdownosd-label"))
self.slowdownOSDCheckbox.setObjectName("showSlowdownOSD")
self.slowdownOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "bullet_black.png"))
self.slowdownOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.slowdownOSDCheckbox)
self.showOSDWarningsCheckbox = QCheckBox(getMessage("showosdwarnings-label"))
self.showOSDWarningsCheckbox.setObjectName("showOSDWarnings")
self.showOSDWarningsCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "bullet_black.png"))
self.showOSDWarningsCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.showOSDWarningsCheckbox)
self.subitems['showOSD'] = ["showSameRoomOSD", "showDifferentRoomOSD", "showSlowdownOSD", "showOSDWarnings"]

View File

@ -36,6 +36,9 @@ class ConsoleUI(threading.Thread):
except EOFError:
pass
def updateRoomName(self, room=""):
pass
def promptFor(self, prompt=">", message=""):
if message <> "":
print(message)
@ -49,14 +52,17 @@ class ConsoleUI(threading.Thread):
message = u"In room '{}':".format(room)
self.showMessage(message, True)
for user in rooms[room]:
username = "*<{}>*".format(user.username) if user == currentUser else "<{}>".format(user.username)
userflags = u""
if user.isController():
userflags = userflags + u"(Controller) "
username = userflags + u"*<{}>*".format(user.username) if user == currentUser else userflags + u"<{}>".format(user.username)
if user.file:
message = u"{} is playing:".format(username)
self.showMessage(message, True)
message = u" File: '{}' ({})".format(user.file['name'], formatTime(user.file['duration']))
if currentUser.file:
if user.file['name'] == currentUser.file['name'] and user.file['size'] != currentUser.file['size']:
message += " (their file size is different from yours!)"
message += u" (their file size is different from yours!)"
self.showMessage(message, True)
else:
message = u"{} is not playing a file".format(username)
@ -134,6 +140,15 @@ class ConsoleUI(threading.Thread):
self._syncplayClient.setRoom(room)
self._syncplayClient.sendRoom()
elif command.group('command') in constants.COMMANDS_CREATE:
roombasename = command.group('parameter')
if roombasename == None:
roombasename = self._syncplayClient.getRoom()
roombasename = utils.stripRoomName(roombasename)
self._syncplayClient.createControlledRoom(roombasename)
elif command.group('command') in constants.COMMANDS_AUTH:
controlpassword = command.group('parameter')
self._syncplayClient.identifyAsController(controlpassword)
else:
if self._tryAdvancedCommands(data):
return
@ -146,6 +161,8 @@ class ConsoleUI(threading.Thread):
self.showMessage(getMessage("commandlist-notification/pause"), True)
self.showMessage(getMessage("commandlist-notification/seek"), True)
self.showMessage(getMessage("commandlist-notification/help"), True)
self.showMessage(getMessage("commandlist-notification/create"), True)
self.showMessage(getMessage("commandlist-notification/auth"), True)
self.showMessage(getMessage("syncplay-version-notification").format(syncplay.version), True)
self.showMessage(getMessage("more-info-notification").format(syncplay.projectURL), True)

View File

@ -1,45 +1,90 @@
from PySide import QtGui #@UnresolvedImport
from PySide.QtCore import Qt, QSettings, QSize, QPoint #@UnresolvedImport
from PySide import QtGui
from PySide.QtCore import Qt, QSettings, QSize, QPoint
from syncplay import utils, constants, version
from syncplay.messages import getMessage
import sys
import time
import re
import os
import threading
from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration
from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration, RoomPasswordProvider, formatSize
class UserlistItemDelegate(QtGui.QStyledItemDelegate):
def __init__(self):
QtGui.QStyledItemDelegate.__init__(self)
def sizeHint(self, option, index):
size = QtGui.QStyledItemDelegate.sizeHint(self, option, index)
if (index.column() == 0):
size.setWidth(size.width() + constants.USERLIST_GUI_USERNAME_OFFSET)
return size
def paint(self, itemQPainter, optionQStyleOptionViewItem, indexQModelIndex):
column = indexQModelIndex.column()
if column == 0:
currentQAbstractItemModel = indexQModelIndex.model()
itemQModelIndex = currentQAbstractItemModel.index(indexQModelIndex.row(), 0, indexQModelIndex.parent())
if sys.platform.startswith('win'):
resourcespath = utils.findWorkingDir() + "\\resources\\"
else:
resourcespath = utils.findWorkingDir() + "/resources/"
controlIconQPixmap = QtGui.QPixmap(resourcespath + "user_key.png")
tickIconQPixmap = QtGui.QPixmap(resourcespath + "tick.png")
crossIconQPixmap = QtGui.QPixmap(resourcespath + "cross.png")
roomController = currentQAbstractItemModel.data(itemQModelIndex, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE)
userReady = currentQAbstractItemModel.data(itemQModelIndex, Qt.UserRole + constants.USERITEM_READY_ROLE)
if roomController and not controlIconQPixmap.isNull():
itemQPainter.drawPixmap (
optionQStyleOptionViewItem.rect.x()+6,
optionQStyleOptionViewItem.rect.y(),
controlIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio))
if userReady and not tickIconQPixmap.isNull():
itemQPainter.drawPixmap (
(optionQStyleOptionViewItem.rect.x()-10),
optionQStyleOptionViewItem.rect.y(),
tickIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio))
elif userReady == False and not crossIconQPixmap.isNull():
itemQPainter.drawPixmap (
(optionQStyleOptionViewItem.rect.x()-10),
optionQStyleOptionViewItem.rect.y(),
crossIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio))
isUserRow = indexQModelIndex.parent() != indexQModelIndex.parent().parent()
if isUserRow:
optionQStyleOptionViewItem.rect.setX(optionQStyleOptionViewItem.rect.x()+constants.USERLIST_GUI_USERNAME_OFFSET)
QtGui.QStyledItemDelegate.paint(self, itemQPainter, optionQStyleOptionViewItem, indexQModelIndex)
class MainWindow(QtGui.QMainWindow):
class topSplitter(QtGui.QSplitter):
def createHandle(self):
return self.topSplitterHandle(self.orientation(), self)
class topSplitterHandle(QtGui.QSplitterHandle):
def mouseReleaseEvent(self, event):
QtGui.QSplitterHandle.mouseReleaseEvent(self, event)
self.parent().parent().parent().updateListGeometry()
def mouseMoveEvent(self, event):
QtGui.QSplitterHandle.mouseMoveEvent(self, event)
self.parent().parent().parent().updateListGeometry()
def addClient(self, client):
self._syncplayClient = client
self.roomInput.setText(self._syncplayClient.getRoom())
self.config = self._syncplayClient.getConfig()
constants.SHOW_BUTTON_LABELS = self.config['showButtonLabels']
try:
if self.contactLabel and not self.config['showContactInfo']:
self.contactLabel.hide()
if not self.config['showButtonLabels']:
if constants.MERGE_PLAYPAUSE_BUTTONS:
self.playpauseButton.setText("")
else:
self.playButton.setText("")
self.playButton.setFixedWidth(self.playButton.minimumSizeHint().width())
self.pauseButton.setText("")
self.pauseButton.setFixedWidth(self.pauseButton.minimumSizeHint().width())
self.roomButton.setText("")
self.roomButton.setFixedWidth(self.roomButton.minimumSizeHint().width())
self.seekButton.setText("")
self.seekButton.setFixedWidth(self.seekButton.minimumSizeHint().width())
self.unseekButton.setText("")
self.unseekButton.setFixedWidth(self.unseekButton.minimumSizeHint().width())
self.roomGroup.setFixedWidth(self.roomGroup.sizeHint().width())
self.seekGroup.setFixedWidth(self.seekGroup.minimumSizeHint().width())
self.miscGroup.setFixedWidth(self.miscGroup.minimumSizeHint().width())
if not constants.SHOW_BUTTON_LABELS:
self.hideRoomSeekLabels()
self.hideMiscLabels()
except ():
pass
def promptFor(self, prompt=">", message=""):
#TODO: Prompt user
# TODO: Prompt user
return None
def showMessage(self, message, noTimestamp=False):
@ -55,60 +100,101 @@ 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")))
self._usertreebuffer.setHorizontalHeaderLabels(
(getMessage("roomuser-heading-label"), getMessage("size-heading-label"), getMessage("duration-heading-label"), getMessage("filename-heading-label") ))
usertreeRoot = self._usertreebuffer.invisibleRootItem()
for room in rooms:
roomitem = QtGui.QStandardItem(room)
font = QtGui.QFont()
font.setItalic(True)
if room == currentUser.room:
font = QtGui.QFont()
font.setWeight(QtGui.QFont.Bold)
roomitem.setFont(font)
blankitem = QtGui.QStandardItem("")
roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable)
blankitem.setFlags(blankitem.flags() & ~Qt.ItemIsEditable)
usertreeRoot.appendRow((roomitem, blankitem))
roomitem.setFont(font)
roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable)
usertreeRoot.appendRow(roomitem)
isControlledRoom = RoomPasswordProvider.isControlledRoom(room)
if isControlledRoom:
if room == currentUser.room and currentUser.isController():
roomitem.setIcon(QtGui.QIcon(self.resourcespath + 'lock_open.png'))
else:
roomitem.setIcon(QtGui.QIcon(self.resourcespath + 'lock.png'))
else:
roomitem.setIcon(QtGui.QIcon(self.resourcespath + 'chevrons_right.png'))
for user in rooms[room]:
useritem = QtGui.QStandardItem(user.username)
fileitem = QtGui.QStandardItem("")
isController = user.isController()
useritem.setData(isController, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE)
if user.file:
fileitem = QtGui.QStandardItem(u"{} ({})".format(user.file['name'], formatTime(user.file['duration'])))
filesizeitem = QtGui.QStandardItem(formatSize(user.file['size']))
filedurationitem = QtGui.QStandardItem("({})".format(formatTime(user.file['duration'])))
filenameitem = QtGui.QStandardItem((user.file['name']))
if currentUser.file:
sameName = sameFilename(user.file['name'], currentUser.file['name'])
sameSize = sameFilesize(user.file['size'], currentUser.file['size'])
sameDuration = sameFileduration(user.file['duration'], currentUser.file['duration'])
sameRoom = room == currentUser.room
differentName = not sameName
differentSize = not sameSize
differentDuration = not sameDuration
if sameName or sameRoom:
if differentSize and sameDuration:
fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentsize-note")))
elif differentSize and differentDuration:
fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentsizeandduration-note")))
elif differentDuration:
fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentduration-note")))
if sameRoom and (differentName or differentSize or differentDuration):
fileitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
underlinefont = QtGui.QFont()
underlinefont.setUnderline(True)
if sameRoom:
if not sameName:
filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
filenameitem.setFont(underlinefont)
if not sameSize:
if currentUser.file is not None and formatSize(user.file['size']) == formatSize(currentUser.file['size']):
filesizeitem = QtGui.QStandardItem(formatSize(user.file['size'],precise=True))
filesizeitem.setFont(underlinefont)
filesizeitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
if not sameDuration:
filedurationitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
filedurationitem.setFont(underlinefont)
else:
fileitem = QtGui.QStandardItem(getMessage("nofile-note"))
filenameitem = QtGui.QStandardItem(getMessage("nofile-note"))
filedurationitem = QtGui.QStandardItem("")
filesizeitem = QtGui.QStandardItem("")
if room == currentUser.room:
fileitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOFILEITEM_COLOR)))
filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOFILEITEM_COLOR)))
font = QtGui.QFont()
if currentUser.username == user.username:
font = QtGui.QFont()
font.setWeight(QtGui.QFont.Bold)
useritem.setFont(font)
useritem.setFlags(useritem.flags() & ~Qt.ItemIsEditable)
fileitem.setFlags(fileitem.flags() & ~Qt.ItemIsEditable)
roomitem.appendRow((useritem, fileitem))
if isControlledRoom and not isController:
if currentUser.username == user.username:
useritem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOTCONTROLLER_COLOR)))
useritem.setFont(font)
useritem.setFlags(useritem.flags() & ~Qt.ItemIsEditable)
filenameitem.setFlags(filenameitem.flags() & ~Qt.ItemIsEditable)
filesizeitem.setFlags(filesizeitem.flags() & ~Qt.ItemIsEditable)
filedurationitem.setFlags(filedurationitem.flags() & ~Qt.ItemIsEditable)
roomitem.appendRow((useritem, filesizeitem, filedurationitem, filenameitem))
self.listTreeModel = self._usertreebuffer
self.listTreeView.setModel(self.listTreeModel)
self.listTreeView.setItemDelegate(UserlistItemDelegate())
self.listTreeView.setItemsExpandable(False)
self.listTreeView.setRootIsDecorated(False)
self.listTreeView.expandAll()
self.listTreeView.resizeColumnToContents(0)
self.listTreeView.resizeColumnToContents(1)
self.updateListGeometry()
def updateListGeometry(self):
try:
roomtocheck = 0
while self.listTreeModel.item(roomtocheck):
self.listTreeView.setFirstColumnSpanned(roomtocheck, self.listTreeView.rootIndex(), True)
roomtocheck += 1
self.listTreeView.header().setStretchLastSection(False)
self.listTreeView.header().setResizeMode(0, QtGui.QHeaderView.ResizeToContents)
self.listTreeView.header().setResizeMode(1, QtGui.QHeaderView.ResizeToContents)
self.listTreeView.header().setResizeMode(2, QtGui.QHeaderView.ResizeToContents)
self.listTreeView.header().setResizeMode(3, QtGui.QHeaderView.ResizeToContents)
NarrowTabsWidth = self.listTreeView.header().sectionSize(0)+self.listTreeView.header().sectionSize(1)+self.listTreeView.header().sectionSize(2)
if self.listTreeView.header().width() < (NarrowTabsWidth+self.listTreeView.header().sectionSize(3)):
self.listTreeView.header().resizeSection(3,self.listTreeView.header().width()-NarrowTabsWidth)
else:
self.listTreeView.header().setResizeMode(3, QtGui.QHeaderView.Stretch)
self.listTreeView.expandAll()
except:
pass
def roomClicked(self, item):
while item.parent().row() != -1:
@ -118,20 +204,26 @@ class MainWindow(QtGui.QMainWindow):
def userListChange(self):
self._syncplayClient.showUserList()
def showErrorMessage(self, message, criticalerror = False):
def updateRoomName(self, room=""):
self.roomInput.setText(room)
def showDebugMessage(self, message):
print(message)
def showErrorMessage(self, message, criticalerror=False):
message = unicode(message)
if criticalerror:
QtGui.QMessageBox.critical(self,"Syncplay", message)
QtGui.QMessageBox.critical(self, "Syncplay", message)
message = message.replace("&", "&amp;").replace('"', "&quot;").replace("<", "&lt;").replace(">", "&gt;")
message = message.replace("\n", "<br />")
message = "<span style=\"{}\">".format(constants.STYLE_ERRORNOTIFICATION) + message + "</span>"
self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "<br />")
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
@ -175,6 +267,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")
@ -199,15 +354,29 @@ class MainWindow(QtGui.QMainWindow):
else:
defaultdirectory = ""
browserfilter = "All files (*)"
fileName, filtr = QtGui.QFileDialog.getOpenFileName(self,getMessage("browseformedia-label"),defaultdirectory,
browserfilter, "", options)
fileName, filtr = QtGui.QFileDialog.getOpenFileName(self, getMessage("browseformedia-label"), defaultdirectory,
browserfilter, "", options)
if fileName:
if sys.platform.startswith('win'):
fileName = fileName.replace("/","\\")
fileName = fileName.replace("/", "\\")
self.mediadirectory = os.path.dirname(fileName)
self.saveMediaBrowseSettings()
self._syncplayClient._player.openFile(fileName)
def createControlledRoom(self):
controlroom, ok = QtGui.QInputDialog.getText(self, getMessage("createcontrolledroom-msgbox-label"),
getMessage("controlledroominfo-msgbox-label"), QtGui.QLineEdit.Normal,
utils.stripRoomName(self._syncplayClient.getRoom()))
if ok and controlroom != '':
self._syncplayClient.createControlledRoom(controlroom)
def identifyAsController(self):
msgboxtitle = getMessage("identifyascontroller-msgbox-label")
msgboxtext = getMessage("identifyinfo-msgbox-label")
controlpassword, ok = QtGui.QInputDialog.getText(self, msgboxtitle, msgboxtext, QtGui.QLineEdit.Normal, "")
if ok and controlpassword != '':
self._syncplayClient.identifyAsController(controlpassword)
def _extractSign(self, m):
if m:
if m == "-":
@ -218,9 +387,9 @@ class MainWindow(QtGui.QMainWindow):
return None
def setOffset(self):
newoffset, ok = QtGui.QInputDialog.getText(self,getMessage("setoffset-msgbox-label"),
getMessage("offsetinfo-msgbox-label"), QtGui.QLineEdit.Normal,
"")
newoffset, ok = QtGui.QInputDialog.getText(self, getMessage("setoffset-msgbox-label"),
getMessage("offsetinfo-msgbox-label"), QtGui.QLineEdit.Normal,
"")
if ok and newoffset != '':
o = re.match(constants.UI_OFFSET_REGEX, "o " + newoffset)
if o:
@ -229,9 +398,9 @@ 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")
@ -248,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()
@ -257,7 +426,7 @@ 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)
@ -266,12 +435,13 @@ class MainWindow(QtGui.QMainWindow):
window.listTreeModel = QtGui.QStandardItemModel()
window.listTreeView = QtGui.QTreeView()
window.listTreeView.setModel(window.listTreeModel)
window.listTreeView.setIndentation(21)
window.listTreeView.doubleClicked.connect(self.roomClicked)
window.listlabel = QtGui.QLabel(getMessage("userlist-heading-label"))
window.listFrame = QtGui.QFrame()
window.listFrame.setLineWidth(0)
window.listFrame.setMidLineWidth(0)
window.listLayout.setContentsMargins(0,0,0,0)
window.listLayout.setContentsMargins(0, 0, 0, 0)
window.listLayout.addWidget(window.listlabel)
window.listLayout.addWidget(window.listTreeView)
window.contactLabel = QtGui.QLabel()
@ -291,7 +461,7 @@ class MainWindow(QtGui.QMainWindow):
window.topSplit.setStretchFactor(0,4)
window.topSplit.setStretchFactor(1,5)
window.mainLayout.addWidget(window.topSplit)
window.topSplit.setSizePolicy(QtGui.QSizePolicy.Preferred,QtGui.QSizePolicy.Expanding)
window.topSplit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
def addBottomLayout(self, window):
window.bottomLayout = QtGui.QHBoxLayout()
@ -300,9 +470,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)
@ -311,7 +481,8 @@ class MainWindow(QtGui.QMainWindow):
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)
@ -322,14 +493,15 @@ 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"))
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"))
@ -342,28 +514,32 @@ 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"))
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)
@ -371,26 +547,38 @@ 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):
window.menuBar = QtGui.QMenuBar()
window.fileMenu = QtGui.QMenu(getMessage("file-menu-label"), self)
window.openAction = window.fileMenu.addAction(QtGui.QIcon(self.resourcespath + 'folder_explore.png'), getMessage("openmedia-menu-label"))
window.openAction = window.fileMenu.addAction(QtGui.QIcon(self.resourcespath + 'folder_explore.png'),
getMessage("openmedia-menu-label"))
window.openAction.triggered.connect(self.browseMediapath)
window.exitAction = window.fileMenu.addAction(QtGui.QIcon(self.resourcespath + 'cross.png'), getMessage("exit-menu-label"))
window.exitAction = window.fileMenu.addAction(QtGui.QIcon(self.resourcespath + 'cross.png'),
getMessage("file-menu-label"))
window.exitAction.triggered.connect(self.exitSyncplay)
window.menuBar.addMenu(window.fileMenu)
window.advancedMenu = QtGui.QMenu(getMessage("advanced-menu-label"), self)
window.setoffsetAction = window.advancedMenu.addAction(QtGui.QIcon(self.resourcespath + 'timeline_marker.png'),getMessage("setoffset-menu-label"))
window.setoffsetAction = window.advancedMenu.addAction(QtGui.QIcon(self.resourcespath + 'timeline_marker.png'),
getMessage("setoffset-menu-label"))
window.setoffsetAction.triggered.connect(self.setOffset)
window.createcontrolledroomAction = window.advancedMenu.addAction(
QtGui.QIcon(self.resourcespath + 'page_white_key.png'), getMessage("createcontrolledroom-menu-label"))
window.createcontrolledroomAction.triggered.connect(self.createControlledRoom)
window.identifyascontroller = window.advancedMenu.addAction(QtGui.QIcon(self.resourcespath + 'key_go.png'),
getMessage("identifyascontroller-menu-label"))
window.identifyascontroller.triggered.connect(self.identifyAsController)
window.menuBar.addMenu(window.advancedMenu)
window.helpMenu = QtGui.QMenu(getMessage("help-menu-label"), self)
window.userguideAction = window.helpMenu.addAction(QtGui.QIcon(self.resourcespath + 'help.png'), getMessage("userguide-menu-label"))
window.userguideAction = window.helpMenu.addAction(QtGui.QIcon(self.resourcespath + 'help.png'),
getMessage("userguide-menu-label"))
window.userguideAction.triggered.connect(self.openUserGuide)
window.menuBar.addMenu(window.helpMenu)
@ -400,7 +588,7 @@ class MainWindow(QtGui.QMainWindow):
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)
@ -427,7 +615,7 @@ class MainWindow(QtGui.QMainWindow):
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()
@ -468,6 +656,7 @@ class MainWindow(QtGui.QMainWindow):
self.addBottomLayout(self)
self.addMenubar(self)
self.addMainFrame(self)
self.setupSizes()
self.loadSettings()
self.setWindowIcon(QtGui.QIcon(self.resourcespath + "syncplay.png"))
self.setWindowFlags(self.windowFlags() & Qt.WindowCloseButtonHint & Qt.AA_DontUseNativeMenuBar & Qt.WindowMinimizeButtonHint & ~Qt.WindowContextHelpButtonHint)

View File

@ -7,6 +7,8 @@ import sys
import os
import itertools
import hashlib
import random
import string
def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None):
"""Retry calling the decorated function using an exponential backoff.
@ -93,6 +95,19 @@ def formatTime(timeInSeconds, weeksAsTitles=True):
formattedTime = "{0:} (Title {1:.0f})".format(formattedTime, title)
return formattedTime
def formatSize (bytes, precise=False):
if bytes == 0: # E.g. when file size privacy is enabled
return "???"
try:
megabytes = int(bytes) / 1000000.0
if precise:
megabytes = round(megabytes, 1)
else:
megabytes = int(megabytes)
return str(megabytes) + getMessage("megabyte-suffix")
except: # E.g. when filesize is hashed
return "???"
def findWorkingDir():
frozen = getattr(sys, 'frozen', '')
if not frozen:
@ -141,7 +156,19 @@ def blackholeStdoutForFrozenWindow():
# Relate to file hashing / difference checking:
def stripfilename(filename):
return re.sub(constants.FILENAME_STRIP_REGEX, "", filename)
if filename:
return re.sub(constants.FILENAME_STRIP_REGEX, "", filename)
else:
return ""
def stripRoomName(RoomName):
if RoomName:
try:
return re.sub(constants.ROOM_NAME_STRIP_REGEX, "\g<roomnamebase>", RoomName)
except IndexError:
return RoomName
else:
return ""
def hashFilename(filename):
return hashlib.sha256(stripfilename(filename).encode('utf-8')).hexdigest()[:12]
@ -182,3 +209,69 @@ def sameFileduration (duration1, duration2):
return True
else:
return False
def meetsMinVersion(version, minVersion):
def versiontotuple(ver):
return tuple(map(int, ver.split(".")))
return versiontotuple(version) >= versiontotuple(minVersion)
class RoomPasswordProvider(object):
CONTROLLED_ROOM_REGEX = re.compile("^\+(.*):(\w{12})$")
PASSWORD_REGEX = re.compile("[A-Z]{2}-\d{3}-\d{3}")
@staticmethod
def isControlledRoom(roomName):
return bool(re.match(RoomPasswordProvider.CONTROLLED_ROOM_REGEX, roomName))
@staticmethod
def check(roomName, password, salt):
if not password or not re.match(RoomPasswordProvider.PASSWORD_REGEX, password):
raise ValueError()
if not roomName:
raise NotControlledRoom()
match = re.match(RoomPasswordProvider.CONTROLLED_ROOM_REGEX, roomName)
if not match:
raise NotControlledRoom()
roomHash = match.group(2)
computedHash = RoomPasswordProvider._computeRoomHash(match.group(1), password, salt)
return roomHash == computedHash
@staticmethod
def getControlledRoomName(roomName, password, salt):
return "+" + roomName + ":" + RoomPasswordProvider._computeRoomHash(roomName, password, salt)
@staticmethod
def _computeRoomHash(roomName, password, salt):
roomName = roomName.encode('utf8')
salt = hashlib.sha256(salt).hexdigest()
provisionalHash = hashlib.sha256(roomName + salt).hexdigest()
return hashlib.sha1(provisionalHash + salt + password).hexdigest()[:12].upper()
class RandomStringGenerator(object):
@staticmethod
def generate_room_password():
parts = (
RandomStringGenerator._get_random_letters(2),
RandomStringGenerator._get_random_numbers(3),
RandomStringGenerator._get_random_numbers(3)
)
return "{}-{}-{}".format(*parts)
@staticmethod
def generate_server_salt():
parts = (
RandomStringGenerator._get_random_letters(10),
)
return "{}".format(*parts)
@staticmethod
def _get_random_letters(quantity):
return ''.join(random.choice(string.ascii_uppercase) for _ in xrange(quantity))
@staticmethod
def _get_random_numbers(quantity):
return ''.join(random.choice(string.digits) for _ in xrange(quantity))
class NotControlledRoom(Exception):
pass

View File

@ -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()