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/timeline_marker.png','resources/control_play_blue.png',
'resources/mpc-hc.png','resources/mpc-hc64.png','resources/mplayer.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/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 = ["resources/icon.ico", "resources/syncplay.png"]
resources.extend(guiIcons) 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' version = '1.3.0'
milestone = 'Pineapple, Pulverize and Destroy!' milestone = 'Akki'
projectURL = 'http://syncplay.pl/' projectURL = 'http://syncplay.pl/'

View File

@ -4,12 +4,12 @@ import time
import re import re
from twisted.internet.protocol import ClientFactory from twisted.internet.protocol import ClientFactory
from twisted.internet import reactor, task from twisted.internet import reactor, task
from functools import wraps
from syncplay.protocols import SyncClientProtocol from syncplay.protocols import SyncClientProtocol
from syncplay import utils, constants from syncplay import utils, constants
from syncplay.messages import getMessage, getMissingStrings from syncplay.messages import getMissingStrings, getMessage
import threading
from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \ from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \
PRIVACY_HIDDENFILENAME, FILENAME_STRIP_REGEX PRIVACY_HIDDENFILENAME
import collections import collections
class SyncClientFactory(ClientFactory): class SyncClientFactory(ClientFactory):
@ -62,14 +62,20 @@ class SyncplayClient(object):
constants.SHOW_SAME_ROOM_OSD = config['showSameRoomOSD'] constants.SHOW_SAME_ROOM_OSD = config['showSameRoomOSD']
constants.SHOW_DURATION_NOTIFICATION = config['showDurationNotification'] constants.SHOW_DURATION_NOTIFICATION = config['showDurationNotification']
constants.DEBUG_MODE = config['debug'] constants.DEBUG_MODE = config['debug']
self.controlpasswords = {}
self.lastControlPasswordAttempt = None
self.serverVersion = "0.0.0"
self.lastLeftTime = 0 self.lastLeftTime = 0
self.lastLeftUser = u"" self.lastLeftUser = u""
self.protocolFactory = SyncClientFactory(self) self.protocolFactory = SyncClientFactory(self)
self.ui = UiManager(self, ui) self.ui = UiManager(self, ui)
self.userlist = SyncplayUserlist(self.ui, self) self.userlist = SyncplayUserlist(self.ui, self)
self._protocol = None self._protocol = None
""":type : SyncClientProtocol|None"""
self._player = 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 config['room'] = config['name'] # ticket #58
self.defaultRoom = config['room'] self.defaultRoom = config['room']
self.playerPositionBeforeLastSeek = 0.0 self.playerPositionBeforeLastSeek = 0.0
@ -100,7 +106,8 @@ class SyncplayClient(object):
self._warnings = self._WarningManager(self._player, self.userlist, self.ui) 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']: 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: if constants.DEBUG_MODE and constants.WARN_ABOUT_MISSING_STRINGS:
missingStrings = getMissingStrings() missingStrings = getMissingStrings()
@ -229,7 +236,7 @@ class SyncplayClient(object):
def _changePlayerStateAccordingToGlobalState(self, position, paused, doSeek, setBy): def _changePlayerStateAccordingToGlobalState(self, position, paused, doSeek, setBy):
madeChangeOnPlayer = False madeChangeOnPlayer = False
pauseChanged = paused != self.getGlobalPaused() pauseChanged = paused != self.getGlobalPaused() or paused != self.getPlayerPaused()
diff = self.getPlayerPosition() - position diff = self.getPlayerPosition() - position
if self._lastGlobalUpdate is None: if self._lastGlobalUpdate is None:
madeChangeOnPlayer = self._initPlayerState(position, paused) madeChangeOnPlayer = self._initPlayerState(position, paused)
@ -240,7 +247,7 @@ class SyncplayClient(object):
madeChangeOnPlayer = self._serverSeeked(position, setBy) madeChangeOnPlayer = self._serverSeeked(position, setBy)
if diff > self._config['rewindThreshold'] and not doSeek and not self._config['rewindOnDesync'] == False: if diff > self._config['rewindThreshold'] and not doSeek and not self._config['rewindOnDesync'] == False:
madeChangeOnPlayer = self._rewindPlayerDueToTimeDifference(position, setBy) 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) madeChangeOnPlayer = self._slowDownToCoverTimeDifference(diff, setBy)
if paused == False and pauseChanged: if paused == False and pauseChanged:
madeChangeOnPlayer = self._serverUnpaused(setBy) madeChangeOnPlayer = self._serverUnpaused(setBy)
@ -323,7 +330,6 @@ class SyncplayClient(object):
size = os.path.getsize(path) size = os.path.getsize(path)
except OSError: # file not accessible (stream?) except OSError: # file not accessible (stream?)
size = 0 size = 0
rawfilename = filename
filename, size = self.__executePrivacySettings(filename, size) filename, size = self.__executePrivacySettings(filename, size)
self.userlist.currentUser.setFile(filename, duration, size) self.userlist.currentUser.setFile(filename, duration, size)
self.sendFile() self.sendFile()
@ -339,6 +345,9 @@ class SyncplayClient(object):
size = 0 size = 0
return filename, size return filename, size
def setServerVersion(self, version):
self.serverVersion = version
def sendFile(self): def sendFile(self):
file_ = self.userlist.currentUser.file file_ = self.userlist.currentUser.file
if self._protocol and self._protocol.logged and 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: if self._protocol and self._protocol.logged and room:
self._protocol.sendRoomSetting(room) self._protocol.sendRoomSetting(room)
self.getUserList() 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): def getRoom(self):
return self.userlist.currentUser.room return self.userlist.currentUser.room
@ -420,6 +440,64 @@ class SyncplayClient(object):
if promptForAction: if promptForAction:
self.ui.promptFor(getMessage("enter-to-exit-prompt")) 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): class _WarningManager(object):
def __init__(self, player, userlist, ui): def __init__(self, player, userlist, ui):
self._player = player self._player = player
@ -472,6 +550,7 @@ class SyncplayUser(object):
self.username = username self.username = username
self.room = room self.room = room
self.file = file_ self.file = file_
self._controller = False
def setFile(self, filename, duration, size): def setFile(self, filename, duration, size):
file_ = { file_ = {
@ -490,7 +569,10 @@ class SyncplayUser(object):
return sameName and sameSize and sameDuration return sameName and sameSize and sameDuration
def __lt__(self, other): 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): def __repr__(self, *args, **kwargs):
if self.file: if self.file:
@ -498,6 +580,12 @@ class SyncplayUser(object):
else: else:
return "{}".format(self.username) return "{}".format(self.username)
def setControllerStatus(self, isController):
self._controller = isController
def isController(self):
return self._controller
class SyncplayUserlist(object): class SyncplayUserlist(object):
def __init__(self, ui, client): def __init__(self, ui, client):
self.currentUser = SyncplayUser() self.currentUser = SyncplayUser()
@ -544,10 +632,14 @@ class SyncplayUserlist(object):
message = getMessage("file-differences-notification") + ", ".join(differences) message = getMessage("file-differences-notification") + ", ".join(differences)
self.ui.showMessage(message, not constants.SHOW_OSD_WARNINGS) 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 username == self.currentUser.username:
if isController is not None:
self.currentUser.setControllerStatus(isController)
return return
user = SyncplayUser(username, room, file_) user = SyncplayUser(username, room, file_)
if isController is not None:
user.setControllerStatus(isController)
self._users[username] = user self._users[username] = user
if not noMessage: if not noMessage:
self.__showUserChangeMessage(username, room, file_) self.__showUserChangeMessage(username, room, file_)
@ -578,6 +670,8 @@ class SyncplayUserlist(object):
if self._users.has_key(username): if self._users.has_key(username):
user = self._users[username] user = self._users[username]
oldRoom = user.room if user.room else None oldRoom = user.room if user.room else None
if user.room != room:
user.setControllerStatus(isController=False)
self.__displayModUserMessage(username, room, file_, user, oldRoom) self.__displayModUserMessage(username, room, file_, user, oldRoom)
user.room = room user.room = room
if file_: if file_:
@ -588,6 +682,13 @@ class SyncplayUserlist(object):
self.addUser(username, room, file_) self.addUser(username, room, file_)
self.userListChange() 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): def areAllFilesInRoomSame(self):
for user in self._users.itervalues(): for user in self._users.itervalues():
if user.room == self.currentUser.room and user.file and not self.currentUser.isFileSame(user.file): 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: if constants.SHOW_OSD and self._client._player:
self._client._player.displayMessage(message, duration * 1000) self._client._player.displayMessage(message, duration * 1000)
def setControllerStatus(self, username, isController):
self.__ui.setControllerStatus(username, isController)
def showErrorMessage(self, message, criticalerror=False): def showErrorMessage(self, message, criticalerror=False):
if message <> self.lastError: # Avoid double call bug if message <> self.lastError: # Avoid double call bug
self.lastError = message self.lastError = message
@ -672,5 +776,10 @@ class UiManager(object):
def markEndOfUserlist(self): def markEndOfUserlist(self):
self.__ui.markEndOfUserlist() self.__ui.markEndOfUserlist()
def updateRoomName(self, room=""):
self.__ui.updateRoomName(room)
def drop(self): def drop(self):
self.__ui.drop() 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 SYNC_ON_PAUSE = True # Client seek to global position - subtitles may disappear on some media players
#Usually there's no need to adjust these #Usually there's no need to adjust these
FILENAME_STRIP_REGEX = u"[-~_\.\[\](): ]" 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_UNDO = ["u", "undo", "revert"]
COMMANDS_LIST = ["l", "list", "users"] COMMANDS_LIST = ["l", "list", "users"]
COMMANDS_PAUSE = ["p", "play", "pause"] COMMANDS_PAUSE = ["p", "play", "pause"]
COMMANDS_ROOM = ["r", "room"] COMMANDS_ROOM = ["r", "room"]
COMMANDS_HELP = ['help', 'h', '?', '/?', r'\?'] COMMANDS_HELP = ['help', 'h', '?', '/?', r'\?']
COMMANDS_CREATE = ['c','create']
COMMANDS_AUTH = ['a','auth']
MPC_MIN_VER = "1.6.4" MPC_MIN_VER = "1.6.4"
VLC_MIN_VERSION = "2.0.0" VLC_MIN_VERSION = "2.0.0"
VLC_INTERFACE_MIN_VERSION = "0.2.1" VLC_INTERFACE_MIN_VERSION = "0.2.1"
CONTROLLED_ROOMS_MIN_VERSION = "1.3.0"
MPC_PATHS = [ MPC_PATHS = [
r"C:\Program Files (x86)\MPC-HC\mpc-hc.exe", r"C:\Program Files (x86)\MPC-HC\mpc-hc.exe",
r"C:\Program Files\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 #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_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_SUBCHECKBOX = "QCheckBox, QLabel {{ margin-left: 6px; padding-left: 21px; 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_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_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_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; }" STYLE_NOTIFICATIONBOX = "Username { color: #367AA9; font-weight:bold; }"
@ -108,6 +113,9 @@ STYLE_USERNAME = "color: #367AA9; font-weight:bold;"
STYLE_ERRORNOTIFICATION = "color: red;" STYLE_ERRORNOTIFICATION = "color: red;"
STYLE_DIFFERENTITEM_COLOR = 'red' STYLE_DIFFERENTITEM_COLOR = 'red'
STYLE_NOFILEITEM_COLOR = 'blue' 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'] 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 # --quiet works with both mpv 0.2 and 0.3
@ -135,3 +143,5 @@ ERROR_MESSAGE_MARKER = "*"
LOAD_SAVE_MANUALLY_MARKER = "!" LOAD_SAVE_MANUALLY_MARKER = "!"
CONFIG_NAME_MARKER = ":" CONFIG_NAME_MARKER = ":"
CONFIG_VALUE_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" : "<{}> is playing '{}' ({})", # User, file, duration
"playing-notification/room-addendum" : " in room: '{}'", # Room "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-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", "room-files-not-same" : "Not all files played in the room are the same",
"alone-in-the-room": "You're alone in the room", "alone-in-the-room": "You're alone in the room",
@ -53,6 +57,8 @@ en = {
"commandlist-notification/pause" : "\tp - toggle pause", "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/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/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 "syncplay-version-notification" : "Syncplay version: {}", # syncplay.version
"more-info-notification" : "More info available at: {}", # projectURL "more-info-notification" : "More info available at: {}", # projectURL
@ -175,7 +181,9 @@ en = {
"pause-guibuttonlabel" : "Pause", "pause-guibuttonlabel" : "Pause",
"roomuser-heading-label" : "Room / User", "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", "notifications-heading-label" : "Notifications",
"userlist-heading-label" : "List of who is playing what", "userlist-heading-label" : "List of who is playing what",
"othercommands-heading-label" : "Other commands", "othercommands-heading-label" : "Other commands",
@ -189,12 +197,24 @@ en = {
"exit-menu-label" : "E&xit", "exit-menu-label" : "E&xit",
"advanced-menu-label" : "&Advanced", "advanced-menu-label" : "&Advanced",
"setoffset-menu-label" : "Set &offset", "setoffset-menu-label" : "Set &offset",
"createcontrolledroom-menu-label" : "&Create controlled room",
"identifyascontroller-menu-label" : "&Identify as room controller",
"help-menu-label" : "&Help", "help-menu-label" : "&Help",
"userguide-menu-label" : "Open user &guide", "userguide-menu-label" : "Open user &guide",
"setoffset-msgbox-label" : "Set offset", "setoffset-msgbox-label" : "Set offset",
"offsetinfo-msgbox-label" : "Offset (see http://syncplay.pl/guide/ for usage instructions):", "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 # 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.", "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 "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-connected-room-server-notification" : "{0}({2}) connected to room '{1}'", # username, host, room
"client-left-server-notification" : "{0} left server", # name "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 # Server arguments
@ -261,11 +282,10 @@ en = {
"server-port-argument" : 'server TCP port', "server-port-argument" : 'server TCP port',
"server-password-argument" : 'server password', "server-password-argument" : 'server password',
"server-isolate-room-argument" : 'should rooms be isolated?', "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-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-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-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 # Server errors
"unknown-command-server-error" : "Unknown command {}", # message "unknown-command-server-error" : "Unknown command {}", # message
@ -275,9 +295,6 @@ en = {
"password-required-server-error" : "Password required", "password-required-server-error" : "Password required",
"wrong-password-server-error" : "Wrong password supplied", "wrong-password-server-error" : "Wrong password supplied",
"hello-server-error" : "Not enough Hello arguments", "hello-server-error" : "Not enough Hello arguments",
"version-mismatch-server-error" : "Mismatch between versions of client and server"
} }
pl = { pl = {

View File

@ -145,6 +145,14 @@ class MplayerPlayer(BasePlayer):
def lineReceived(self, line): def lineReceived(self, line):
if line: if line:
self._client.ui.showDebugMessage("player << {}".format(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) match = self.RE_ANSWER.match(line)
if not match: if not match:
self._handleUnknownLine(line) self._handleUnknownLine(line)

View File

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

View File

@ -1,4 +1,5 @@
import hashlib import hashlib
import random
from twisted.internet import task, reactor from twisted.internet import task, reactor
from twisted.internet.protocol import Factory from twisted.internet.protocol import Factory
import syncplay import syncplay
@ -10,13 +11,18 @@ import codecs
import os import os
from string import Template from string import Template
import argparse import argparse
from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomStringGenerator, meetsMinVersion
class SyncFactory(Factory): 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) print getMessage("welcome-server-notification").format(syncplay.version)
if password: if password:
password = hashlib.md5(password).hexdigest() password = hashlib.md5(password).hexdigest()
self.password = password 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 self._motdFilePath = motdFilePath
if not isolateRooms: if not isolateRooms:
self._roomManager = RoomManager() self._roomManager = RoomManager()
@ -36,7 +42,7 @@ class SyncFactory(Factory):
def getMotd(self, userIp, username, room, clientVersion): def getMotd(self, userIp, username, room, clientVersion):
oldClient = False oldClient = False
if constants.WARN_OLD_CLIENTS: 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 oldClient = True
if self._motdFilePath and os.path.isfile(self._motdFilePath): if self._motdFilePath and os.path.isfile(self._motdFilePath):
tmpl = codecs.open(self._motdFilePath, "r", "utf-8-sig").read() tmpl = codecs.open(self._motdFilePath, "r", "utf-8-sig").read()
@ -54,7 +60,7 @@ class SyncFactory(Factory):
else: else:
return "" return ""
def addWatcher(self, watcherProtocol, username, roomName, roomPassword): def addWatcher(self, watcherProtocol, username, roomName):
username = self._roomManager.findFreeUsername(username) username = self._roomManager.findFreeUsername(username)
watcher = Watcher(self, watcherProtocol, username) watcher = Watcher(self, watcherProtocol, username)
self.setWatcherRoom(watcher, roomName, asJoin=True) self.setWatcherRoom(watcher, roomName, asJoin=True)
@ -65,13 +71,16 @@ class SyncFactory(Factory):
self.sendJoinMessage(watcher) self.sendJoinMessage(watcher)
else: else:
self.sendRoomSwitchMessage(watcher) self.sendRoomSwitchMessage(watcher)
if RoomPasswordProvider.isControlledRoom(roomName):
for controller in watcher.getRoom().getControllers():
watcher.sendControlledRoomAuthStatus(True, controller, roomName)
def sendRoomSwitchMessage(self, watcher): def sendRoomSwitchMessage(self, watcher):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None) l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None)
self._roomManager.broadcast(watcher, l) self._roomManager.broadcast(watcher, l)
def removeWatcher(self, watcher): def removeWatcher(self, watcher):
if watcher.getRoom(): if watcher and watcher.getRoom():
self.sendLeftMessage(watcher) self.sendLeftMessage(watcher)
self._roomManager.removeWatcher(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 l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"joined": True}) if w != watcher else None
self._roomManager.broadcast(watcher, l) 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) l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), watcher.getFile(), None)
self._roomManager.broadcast(watcher, l) self._roomManager.broadcast(watcher, l)
def forcePositionUpdate(self, room, watcher, doSeek): def forcePositionUpdate(self, watcher, doSeek, watcherPauseState):
room = watcher.getRoom() room = watcher.getRoom()
paused, position = room.isPaused(), watcher.getPosition() if room.canControl(watcher):
setBy = watcher paused, position = room.isPaused(), watcher.getPosition()
room.setPosition(watcher.getPosition(), setBy) setBy = watcher
l = lambda w: w.sendState(position, paused, doSeek, setBy, True) l = lambda w: w.sendState(position, paused, doSeek, setBy, True)
self._roomManager.broadcastRoom(watcher, l) 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): def getAllWatchersForUser(self, forUser):
return self._roomManager.getAllWatchersForUser(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): class RoomManager(object):
def __init__(self): def __init__(self):
self._rooms = {} self._rooms = {}
@ -135,7 +163,10 @@ class RoomManager(object):
if roomName in self._rooms: if roomName in self._rooms:
return self._rooms[roomName] return self._rooms[roomName]
else: else:
room = Room(roomName) if RoomPasswordProvider.isControlledRoom(roomName):
room = ControlledRoom(roomName)
else:
room = Room(roomName)
self._rooms[roomName] = room self._rooms[roomName] = room
return room return room
@ -228,6 +259,44 @@ class Room(object):
def getSetBy(self): def getSetBy(self):
return self._setBy 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): class Watcher(object):
def __init__(self, server, connector, name): def __init__(self, server, connector, name):
self._server = server self._server = server
@ -243,7 +312,7 @@ class Watcher(object):
def setFile(self, file_): def setFile(self, file_):
self._file = file_ self._file = file_
self._server.sendFileUpdate(self, file_) self._server.sendFileUpdate(self)
def setRoom(self, room): def setRoom(self, room):
self._room = room self._room = room
@ -277,16 +346,22 @@ class Watcher(object):
def sendSetting(self, user, room, file_, event): def sendSetting(self, user, room, file_, event):
self._connector.sendUserSetting(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): def __lt__(self, b):
if self.getPosition() is None or self._file is None: if self.getPosition() is None or self._file is None:
return False 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 True
return self.getPosition() < b.getPosition() return self.getPosition() < b.getPosition()
def _scheduleSendState(self): def _scheduleSendState(self):
self._sendStateTimer = task.LoopingCall(self._askForStateUpdate) 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): def _askForStateUpdate(self, doSeek=False, forcedUpdate=False):
self._server.sendState(self, doSeek, forcedUpdate) self._server.sendState(self, doSeek, forcedUpdate)
@ -313,18 +388,25 @@ class Watcher(object):
return False return False
return self._room.isPaused() and not paused or not self._room.isPaused() and paused 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): def updateState(self, position, paused, doSeek, messageAge):
pauseChanged = self.__hasPauseChanged(paused) pauseChanged = self.__hasPauseChanged(paused)
self._lastUpdatedOn = time.time() self._lastUpdatedOn = time.time()
if pauseChanged: if pauseChanged:
self.getRoom().setPaused(Room.STATE_PAUSED if paused else Room.STATE_PLAYING, self) self.getRoom().setPaused(Room.STATE_PAUSED if paused else Room.STATE_PLAYING, self)
if position is not None: if position is not None:
if not paused: position = self._updatePositionByAge(messageAge, paused, position)
position += messageAge
self.setPosition(position) self.setPosition(position)
if doSeek or pauseChanged: 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): class ConfigurationGetter(object):
def getConfiguration(self): 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('--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('--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('--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"))

View File

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

View File

@ -35,7 +35,10 @@ class ConsoleUI(threading.Thread):
self._executeCommand(data) self._executeCommand(data)
except EOFError: except EOFError:
pass pass
def updateRoomName(self, room=""):
pass
def promptFor(self, prompt=">", message=""): def promptFor(self, prompt=">", message=""):
if message <> "": if message <> "":
print(message) print(message)
@ -49,14 +52,17 @@ class ConsoleUI(threading.Thread):
message = u"In room '{}':".format(room) message = u"In room '{}':".format(room)
self.showMessage(message, True) self.showMessage(message, True)
for user in rooms[room]: 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: if user.file:
message = u"{} is playing:".format(username) message = u"{} is playing:".format(username)
self.showMessage(message, True) self.showMessage(message, True)
message = u" File: '{}' ({})".format(user.file['name'], formatTime(user.file['duration'])) message = u" File: '{}' ({})".format(user.file['name'], formatTime(user.file['duration']))
if currentUser.file: if currentUser.file:
if user.file['name'] == currentUser.file['name'] and user.file['size'] != currentUser.file['size']: 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) self.showMessage(message, True)
else: else:
message = u"{} is not playing a file".format(username) message = u"{} is not playing a file".format(username)
@ -74,7 +80,7 @@ class ConsoleUI(threading.Thread):
def showDebugMessage(self, message): def showDebugMessage(self, message):
print(message) print(message)
def showErrorMessage(self, message, criticalerror = False): def showErrorMessage(self, message, criticalerror = False):
print("ERROR:\t" + message) print("ERROR:\t" + message)
@ -134,6 +140,15 @@ class ConsoleUI(threading.Thread):
self._syncplayClient.setRoom(room) self._syncplayClient.setRoom(room)
self._syncplayClient.sendRoom() 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: else:
if self._tryAdvancedCommands(data): if self._tryAdvancedCommands(data):
return return
@ -146,6 +161,8 @@ class ConsoleUI(threading.Thread):
self.showMessage(getMessage("commandlist-notification/pause"), True) self.showMessage(getMessage("commandlist-notification/pause"), True)
self.showMessage(getMessage("commandlist-notification/seek"), True) self.showMessage(getMessage("commandlist-notification/seek"), True)
self.showMessage(getMessage("commandlist-notification/help"), 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("syncplay-version-notification").format(syncplay.version), True)
self.showMessage(getMessage("more-info-notification").format(syncplay.projectURL), True) self.showMessage(getMessage("more-info-notification").format(syncplay.projectURL), True)

View File

@ -1,45 +1,90 @@
from PySide import QtGui #@UnresolvedImport from PySide import QtGui
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 import utils, constants, version
from syncplay.messages import getMessage from syncplay.messages import getMessage
import sys import sys
import time import time
import re import re
import os import os
import threading from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration, RoomPasswordProvider, formatSize
from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration
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 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): def addClient(self, client):
self._syncplayClient = client self._syncplayClient = client
self.roomInput.setText(self._syncplayClient.getRoom()) self.roomInput.setText(self._syncplayClient.getRoom())
self.config = self._syncplayClient.getConfig() self.config = self._syncplayClient.getConfig()
constants.SHOW_BUTTON_LABELS = self.config['showButtonLabels']
try: try:
if self.contactLabel and not self.config['showContactInfo']: if self.contactLabel and not self.config['showContactInfo']:
self.contactLabel.hide() self.contactLabel.hide()
if not self.config['showButtonLabels']: if not constants.SHOW_BUTTON_LABELS:
if constants.MERGE_PLAYPAUSE_BUTTONS: self.hideRoomSeekLabels()
self.playpauseButton.setText("") self.hideMiscLabels()
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())
except (): except ():
pass pass
def promptFor(self, prompt=">", message=""): def promptFor(self, prompt=">", message=""):
#TODO: Prompt user # TODO: Prompt user
return None return None
def showMessage(self, message, noTimestamp=False): def showMessage(self, message, noTimestamp=False):
@ -52,86 +97,133 @@ class MainWindow(QtGui.QMainWindow):
self.newMessage(message + "<br />") self.newMessage(message + "<br />")
else: else:
self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "<br />") self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "<br />")
def showUserList(self, currentUser, rooms): def showUserList(self, currentUser, rooms):
self._usertreebuffer = QtGui.QStandardItemModel() self._usertreebuffer = QtGui.QStandardItemModel()
self._usertreebuffer.setColumnCount(2) self._usertreebuffer.setHorizontalHeaderLabels(
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() usertreeRoot = self._usertreebuffer.invisibleRootItem()
for room in rooms: for room in rooms:
roomitem = QtGui.QStandardItem(room) roomitem = QtGui.QStandardItem(room)
font = QtGui.QFont()
font.setItalic(True)
if room == currentUser.room: if room == currentUser.room:
font = QtGui.QFont()
font.setWeight(QtGui.QFont.Bold) font.setWeight(QtGui.QFont.Bold)
roomitem.setFont(font) roomitem.setFont(font)
blankitem = QtGui.QStandardItem("") roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable)
roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable) usertreeRoot.appendRow(roomitem)
blankitem.setFlags(blankitem.flags() & ~Qt.ItemIsEditable) isControlledRoom = RoomPasswordProvider.isControlledRoom(room)
usertreeRoot.appendRow((roomitem, blankitem))
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]: for user in rooms[room]:
useritem = QtGui.QStandardItem(user.username) useritem = QtGui.QStandardItem(user.username)
fileitem = QtGui.QStandardItem("") isController = user.isController()
useritem.setData(isController, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE)
if user.file: 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: if currentUser.file:
sameName = sameFilename(user.file['name'], currentUser.file['name']) sameName = sameFilename(user.file['name'], currentUser.file['name'])
sameSize = sameFilesize(user.file['size'], currentUser.file['size']) sameSize = sameFilesize(user.file['size'], currentUser.file['size'])
sameDuration = sameFileduration(user.file['duration'], currentUser.file['duration']) sameDuration = sameFileduration(user.file['duration'], currentUser.file['duration'])
sameRoom = room == currentUser.room sameRoom = room == currentUser.room
differentName = not sameName underlinefont = QtGui.QFont()
differentSize = not sameSize underlinefont.setUnderline(True)
differentDuration = not sameDuration if sameRoom:
if sameName or sameRoom: if not sameName:
if differentSize and sameDuration: filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentsize-note"))) filenameitem.setFont(underlinefont)
elif differentSize and differentDuration: if not sameSize:
fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentsizeandduration-note"))) if currentUser.file is not None and formatSize(user.file['size']) == formatSize(currentUser.file['size']):
elif differentDuration: filesizeitem = QtGui.QStandardItem(formatSize(user.file['size'],precise=True))
fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentduration-note"))) filesizeitem.setFont(underlinefont)
if sameRoom and (differentName or differentSize or differentDuration): filesizeitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
fileitem.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: else:
fileitem = QtGui.QStandardItem(getMessage("nofile-note")) filenameitem = QtGui.QStandardItem(getMessage("nofile-note"))
filedurationitem = QtGui.QStandardItem("")
filesizeitem = QtGui.QStandardItem("")
if room == currentUser.room: 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: if currentUser.username == user.username:
font = QtGui.QFont()
font.setWeight(QtGui.QFont.Bold) font.setWeight(QtGui.QFont.Bold)
useritem.setFont(font) if isControlledRoom and not isController:
useritem.setFlags(useritem.flags() & ~Qt.ItemIsEditable) if currentUser.username == user.username:
fileitem.setFlags(fileitem.flags() & ~Qt.ItemIsEditable) useritem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOTCONTROLLER_COLOR)))
roomitem.appendRow((useritem, fileitem)) 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.listTreeModel = self._usertreebuffer
self.listTreeView.setModel(self.listTreeModel) self.listTreeView.setModel(self.listTreeModel)
self.listTreeView.setItemDelegate(UserlistItemDelegate())
self.listTreeView.setItemsExpandable(False) self.listTreeView.setItemsExpandable(False)
self.listTreeView.setRootIsDecorated(False)
self.listTreeView.expandAll() self.listTreeView.expandAll()
self.listTreeView.resizeColumnToContents(0) self.updateListGeometry()
self.listTreeView.resizeColumnToContents(1)
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): def roomClicked(self, item):
while item.parent().row() != -1: while item.parent().row() != -1:
item = item.parent() item = item.parent()
self.joinRoom(item.sibling(item.row(), 0).data()) self.joinRoom(item.sibling(item.row(), 0).data())
def userListChange(self): def userListChange(self):
self._syncplayClient.showUserList() 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) message = unicode(message)
if criticalerror: 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("&", "&amp;").replace('"', "&quot;").replace("<", "&lt;").replace(">", "&gt;")
message = message.replace("\n", "<br />") message = message.replace("\n", "<br />")
message = "<span style=\"{}\">".format(constants.STYLE_ERRORNOTIFICATION) + message + "</span>" message = "<span style=\"{}\">".format(constants.STYLE_ERRORNOTIFICATION) + message + "</span>"
self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "<br />") 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: if room == None:
room = self.roomInput.text() room = self.roomInput.text()
if room == "": if room == "":
if self._syncplayClient.userlist.currentUser.file: if self._syncplayClient.userlist.currentUser.file:
room = self._syncplayClient.userlist.currentUser.file["name"] room = self._syncplayClient.userlist.currentUser.file["name"]
else: else:
room = self._syncplayClient.defaultRoom room = self._syncplayClient.defaultRoom
@ -148,45 +240,108 @@ class MainWindow(QtGui.QMainWindow):
if t is None: if t is None:
return return
if sign: if sign:
t = self._syncplayClient.getGlobalPosition() + sign * t t = self._syncplayClient.getGlobalPosition() + sign * t
self._syncplayClient.setPosition(t) self._syncplayClient.setPosition(t)
else: else:
self.showErrorMessage("Invalid seek value") self.showErrorMessage("Invalid seek value")
def undoSeek(self): def undoSeek(self):
tmp_pos = self._syncplayClient.getPlayerPosition() tmp_pos = self._syncplayClient.getPlayerPosition()
self._syncplayClient.setPosition(self._syncplayClient.playerPositionBeforeLastSeek) self._syncplayClient.setPosition(self._syncplayClient.playerPositionBeforeLastSeek)
self._syncplayClient.playerPositionBeforeLastSeek = tmp_pos self._syncplayClient.playerPositionBeforeLastSeek = tmp_pos
def togglePause(self): def togglePause(self):
self._syncplayClient.setPaused(not self._syncplayClient.getPlayerPaused()) self._syncplayClient.setPaused(not self._syncplayClient.getPlayerPaused())
def play(self): def play(self):
self._syncplayClient.setPaused(False) self._syncplayClient.setPaused(False)
def pause(self): def pause(self):
self._syncplayClient.setPaused(True) self._syncplayClient.setPaused(True)
def exitSyncplay(self): def exitSyncplay(self):
self._syncplayClient.stop() self._syncplayClient.stop()
def closeEvent(self, event): def closeEvent(self, event):
self.exitSyncplay() self.exitSyncplay()
self.saveSettings() 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): def loadMediaBrowseSettings(self):
settings = QSettings("Syncplay", "MediaBrowseDialog") settings = QSettings("Syncplay", "MediaBrowseDialog")
settings.beginGroup("MediaBrowseDialog") settings.beginGroup("MediaBrowseDialog")
self.mediadirectory = settings.value("mediadir", "") self.mediadirectory = settings.value("mediadir", "")
settings.endGroup() settings.endGroup()
def saveMediaBrowseSettings(self): def saveMediaBrowseSettings(self):
settings = QSettings("Syncplay", "MediaBrowseDialog") settings = QSettings("Syncplay", "MediaBrowseDialog")
settings.beginGroup("MediaBrowseDialog") settings.beginGroup("MediaBrowseDialog")
settings.setValue("mediadir", self.mediadirectory) settings.setValue("mediadir", self.mediadirectory)
settings.endGroup() settings.endGroup()
def browseMediapath(self): def browseMediapath(self):
self.loadMediaBrowseSettings() self.loadMediaBrowseSettings()
options = QtGui.QFileDialog.Options() options = QtGui.QFileDialog.Options()
@ -198,16 +353,30 @@ class MainWindow(QtGui.QMainWindow):
defaultdirectory = QtGui.QDesktopServices.storageLocation(QtGui.QDesktopServices.HomeLocation) defaultdirectory = QtGui.QDesktopServices.storageLocation(QtGui.QDesktopServices.HomeLocation)
else: else:
defaultdirectory = "" defaultdirectory = ""
browserfilter = "All files (*)" browserfilter = "All files (*)"
fileName, filtr = QtGui.QFileDialog.getOpenFileName(self,getMessage("browseformedia-label"),defaultdirectory, fileName, filtr = QtGui.QFileDialog.getOpenFileName(self, getMessage("browseformedia-label"), defaultdirectory,
browserfilter, "", options) browserfilter, "", options)
if fileName: if fileName:
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
fileName = fileName.replace("/","\\") fileName = fileName.replace("/", "\\")
self.mediadirectory = os.path.dirname(fileName) self.mediadirectory = os.path.dirname(fileName)
self.saveMediaBrowseSettings() self.saveMediaBrowseSettings()
self._syncplayClient._player.openFile(fileName) 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): def _extractSign(self, m):
if m: if m:
if m == "-": if m == "-":
@ -216,11 +385,11 @@ class MainWindow(QtGui.QMainWindow):
return 1 return 1
else: else:
return None return None
def setOffset(self): def setOffset(self):
newoffset, ok = QtGui.QInputDialog.getText(self,getMessage("setoffset-msgbox-label"), newoffset, ok = QtGui.QInputDialog.getText(self, getMessage("setoffset-msgbox-label"),
getMessage("offsetinfo-msgbox-label"), QtGui.QLineEdit.Normal, getMessage("offsetinfo-msgbox-label"), QtGui.QLineEdit.Normal,
"") "")
if ok and newoffset != '': if ok and newoffset != '':
o = re.match(constants.UI_OFFSET_REGEX, "o " + newoffset) o = re.match(constants.UI_OFFSET_REGEX, "o " + newoffset)
if o: if o:
@ -229,13 +398,13 @@ class MainWindow(QtGui.QMainWindow):
if t is None: if t is None:
return return
if o.group('sign') == "/": if o.group('sign') == "/":
t = self._syncplayClient.getPlayerPosition() - t t = self._syncplayClient.getPlayerPosition() - t
elif sign: elif sign:
t = self._syncplayClient.getUserOffset() + sign * t t = self._syncplayClient.getUserOffset() + sign * t
self._syncplayClient.setUserOffset(t) self._syncplayClient.setUserOffset(t)
else: else:
self.showErrorMessage("Invalid offset value") self.showErrorMessage("Invalid offset value")
def openUserGuide(self): def openUserGuide(self):
if sys.platform.startswith('linux'): if sys.platform.startswith('linux'):
self.QtGui.QDesktopServices.openUrl("http://syncplay.pl/guide/linux/") self.QtGui.QDesktopServices.openUrl("http://syncplay.pl/guide/linux/")
@ -246,9 +415,9 @@ class MainWindow(QtGui.QMainWindow):
def drop(self): def drop(self):
self.close() self.close()
def addTopLayout(self, window): def addTopLayout(self, window):
window.topSplit = QtGui.QSplitter(Qt.Horizontal) window.topSplit = self.topSplitter(Qt.Horizontal, self)
window.outputLayout = QtGui.QVBoxLayout() window.outputLayout = QtGui.QVBoxLayout()
window.outputbox = QtGui.QTextEdit() window.outputbox = QtGui.QTextEdit()
@ -257,21 +426,22 @@ class MainWindow(QtGui.QMainWindow):
window.outputFrame = QtGui.QFrame() window.outputFrame = QtGui.QFrame()
window.outputFrame.setLineWidth(0) window.outputFrame.setLineWidth(0)
window.outputFrame.setMidLineWidth(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.outputlabel)
window.outputLayout.addWidget(window.outputbox) window.outputLayout.addWidget(window.outputbox)
window.outputFrame.setLayout(window.outputLayout) window.outputFrame.setLayout(window.outputLayout)
window.listLayout = QtGui.QVBoxLayout() window.listLayout = QtGui.QVBoxLayout()
window.listTreeModel = QtGui.QStandardItemModel() window.listTreeModel = QtGui.QStandardItemModel()
window.listTreeView = QtGui.QTreeView() window.listTreeView = QtGui.QTreeView()
window.listTreeView.setModel(window.listTreeModel) window.listTreeView.setModel(window.listTreeModel)
window.listTreeView.setIndentation(21)
window.listTreeView.doubleClicked.connect(self.roomClicked) window.listTreeView.doubleClicked.connect(self.roomClicked)
window.listlabel = QtGui.QLabel(getMessage("userlist-heading-label")) window.listlabel = QtGui.QLabel(getMessage("userlist-heading-label"))
window.listFrame = QtGui.QFrame() window.listFrame = QtGui.QFrame()
window.listFrame.setLineWidth(0) window.listFrame.setLineWidth(0)
window.listFrame.setMidLineWidth(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.listlabel)
window.listLayout.addWidget(window.listTreeView) window.listLayout.addWidget(window.listTreeView)
window.contactLabel = QtGui.QLabel() window.contactLabel = QtGui.QLabel()
@ -285,13 +455,13 @@ class MainWindow(QtGui.QMainWindow):
window.contactLabel.setOpenExternalLinks(True) window.contactLabel.setOpenExternalLinks(True)
window.listLayout.addWidget(window.contactLabel) window.listLayout.addWidget(window.contactLabel)
window.listFrame.setLayout(window.listLayout) window.listFrame.setLayout(window.listLayout)
window.topSplit.addWidget(window.outputFrame) window.topSplit.addWidget(window.outputFrame)
window.topSplit.addWidget(window.listFrame) window.topSplit.addWidget(window.listFrame)
window.topSplit.setStretchFactor(0,4) window.topSplit.setStretchFactor(0,4)
window.topSplit.setStretchFactor(1,5) window.topSplit.setStretchFactor(1,5)
window.mainLayout.addWidget(window.topSplit) 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): def addBottomLayout(self, window):
window.bottomLayout = QtGui.QHBoxLayout() window.bottomLayout = QtGui.QHBoxLayout()
@ -300,134 +470,152 @@ class MainWindow(QtGui.QMainWindow):
window.addSeekBox(MainWindow) window.addSeekBox(MainWindow)
window.addMiscBox(MainWindow) window.addMiscBox(MainWindow)
window.bottomLayout.addWidget(window.roomGroup, Qt.AlignLeft) window.bottomLayout.addWidget(window.roomGroup)
window.bottomLayout.addWidget(window.seekGroup, Qt.AlignLeft) window.bottomLayout.addWidget(window.seekGroup)
window.bottomLayout.addWidget(window.miscGroup, Qt.AlignLeft) window.bottomLayout.addWidget(window.miscGroup)
window.mainLayout.addLayout(window.bottomLayout, Qt.AlignLeft) window.mainLayout.addLayout(window.bottomLayout, Qt.AlignLeft)
def addRoomBox(self, window): def addRoomBox(self, window):
window.roomGroup = QtGui.QGroupBox(getMessage("room-heading-label")) window.roomGroup = QtGui.QGroupBox(getMessage("room-heading-label"))
window.roomInput = QtGui.QLineEdit() window.roomInput = QtGui.QLineEdit()
window.roomInput.returnPressed.connect(self.joinRoom) 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.roomButton.pressed.connect(self.joinRoom)
window.roomLayout = QtGui.QHBoxLayout() window.roomLayout = QtGui.QHBoxLayout()
window.roomInput.setFixedWidth(150) window.roomInput.setFixedWidth(150)
self.roomButton.setToolTip(getMessage("joinroom-tooltip")) self.roomButton.setToolTip(getMessage("joinroom-tooltip"))
window.roomLayout.addWidget(window.roomInput) window.roomLayout.addWidget(window.roomInput)
window.roomLayout.addWidget(window.roomButton) window.roomLayout.addWidget(window.roomButton)
window.roomGroup.setLayout(window.roomLayout) window.roomGroup.setLayout(window.roomLayout)
window.roomGroup.setFixedSize(window.roomGroup.sizeHint()) window.roomGroup.setFixedHeight(window.roomGroup.sizeHint().height())
def addSeekBox(self, window): def addSeekBox(self, window):
window.seekGroup = QtGui.QGroupBox(getMessage("seek-heading-label")) window.seekGroup = QtGui.QGroupBox(getMessage("seek-heading-label"))
window.seekInput = QtGui.QLineEdit() window.seekInput = QtGui.QLineEdit()
window.seekInput.returnPressed.connect(self.seekPosition) 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) window.seekButton.pressed.connect(self.seekPosition)
self.seekButton.setToolTip(getMessage("seektime-tooltip")) self.seekButton.setToolTip(getMessage("seektime-tooltip"))
window.seekLayout = QtGui.QHBoxLayout() window.seekLayout = QtGui.QHBoxLayout()
window.seekInput.setText("0:00") window.seekInput.setText("0:00")
window.seekInput.setFixedWidth(60) window.seekInput.setFixedWidth(60)
window.seekLayout.addWidget(window.seekInput) window.seekLayout.addWidget(window.seekInput)
window.seekLayout.addWidget(window.seekButton) window.seekLayout.addWidget(window.seekButton)
window.seekGroup.setLayout(window.seekLayout) window.seekGroup.setLayout(window.seekLayout)
window.seekGroup.setFixedSize(window.seekGroup.sizeHint()) window.seekGroup.setFixedHeight(window.seekGroup.sizeHint().height())
def addMiscBox(self, window): def addMiscBox(self, window):
window.miscGroup = QtGui.QGroupBox(getMessage("othercommands-heading-label")) 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) window.unseekButton.pressed.connect(self.undoSeek)
self.unseekButton.setToolTip(getMessage("undoseek-tooltip")) self.unseekButton.setToolTip(getMessage("undoseek-tooltip"))
window.miscLayout = QtGui.QHBoxLayout() window.miscLayout = QtGui.QHBoxLayout()
window.miscLayout.addWidget(window.unseekButton) window.miscLayout.addWidget(window.unseekButton)
if constants.MERGE_PLAYPAUSE_BUTTONS == True: 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.playpauseButton.pressed.connect(self.togglePause)
window.miscLayout.addWidget(window.playpauseButton) window.miscLayout.addWidget(window.playpauseButton)
self.playpauseButton.setToolTip(getMessage("togglepause-tooltip")) self.playpauseButton.setToolTip(getMessage("togglepause-tooltip"))
else: 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.pressed.connect(self.play)
window.playButton.setMaximumWidth(60) window.playButton.setMaximumWidth(60)
window.miscLayout.addWidget(window.playButton) 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.pressed.connect(self.pause)
window.pauseButton.setMaximumWidth(60) window.pauseButton.setMaximumWidth(60)
window.miscLayout.addWidget(window.pauseButton) window.miscLayout.addWidget(window.pauseButton)
self.playButton.setToolTip(getMessage("play-tooltip")) self.playButton.setToolTip(getMessage("play-tooltip"))
self.pauseButton.setToolTip(getMessage("pause-tooltip")) self.pauseButton.setToolTip(getMessage("pause-tooltip"))
window.miscGroup.setLayout(window.miscLayout) window.miscGroup.setLayout(window.miscLayout)
window.miscGroup.setFixedSize(window.miscGroup.sizeHint()) window.miscGroup.setFixedHeight(window.miscGroup.sizeHint().height())
def addMenubar(self, window): def addMenubar(self, window):
window.menuBar = QtGui.QMenuBar() window.menuBar = QtGui.QMenuBar()
window.fileMenu = QtGui.QMenu(getMessage("file-menu-label"), self) 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.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.exitAction.triggered.connect(self.exitSyncplay)
window.menuBar.addMenu(window.fileMenu) window.menuBar.addMenu(window.fileMenu)
window.advancedMenu = QtGui.QMenu(getMessage("advanced-menu-label"), self) 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.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.menuBar.addMenu(window.advancedMenu)
window.helpMenu = QtGui.QMenu(getMessage("help-menu-label"), self) 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.userguideAction.triggered.connect(self.openUserGuide)
window.menuBar.addMenu(window.helpMenu) window.menuBar.addMenu(window.helpMenu)
window.mainLayout.setMenuBar(window.menuBar) window.mainLayout.setMenuBar(window.menuBar)
def addMainFrame(self, window): def addMainFrame(self, window):
window.mainFrame = QtGui.QFrame() window.mainFrame = QtGui.QFrame()
window.mainFrame.setLineWidth(0) window.mainFrame.setLineWidth(0)
window.mainFrame.setMidLineWidth(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.mainFrame.setLayout(window.mainLayout)
window.setCentralWidget(window.mainFrame) window.setCentralWidget(window.mainFrame)
def newMessage(self, message): def newMessage(self, message):
self.outputbox.moveCursor(QtGui.QTextCursor.End) self.outputbox.moveCursor(QtGui.QTextCursor.End)
self.outputbox.insertHtml(message) self.outputbox.insertHtml(message)
self.outputbox.moveCursor(QtGui.QTextCursor.End) self.outputbox.moveCursor(QtGui.QTextCursor.End)
def resetList(self): def resetList(self):
self.listbox.setText("") self.listbox.setText("")
def newListItem(self, item): def newListItem(self, item):
self.listbox.moveCursor(QtGui.QTextCursor.End) self.listbox.moveCursor(QtGui.QTextCursor.End)
self.listbox.insertHtml(item) self.listbox.insertHtml(item)
self.listbox.moveCursor(QtGui.QTextCursor.End) self.listbox.moveCursor(QtGui.QTextCursor.End)
def dragEnterEvent(self, event): def dragEnterEvent(self, event):
data = event.mimeData() data = event.mimeData()
urls = data.urls() urls = data.urls()
if urls and urls[0].scheme() == 'file': if urls and urls[0].scheme() == 'file':
event.acceptProposedAction() event.acceptProposedAction()
def dropEvent(self, event): def dropEvent(self, event):
rewindFile = False rewindFile = False
if QtGui.QDropEvent.proposedAction(event) == Qt.MoveAction: 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 rewindFile = True
data = event.mimeData() data = event.mimeData()
urls = data.urls() urls = data.urls()
@ -439,14 +627,14 @@ class MainWindow(QtGui.QMainWindow):
self._syncplayClient.setPosition(0) self._syncplayClient.setPosition(0)
self._syncplayClient._player.openFile(dropfilepath, resetPosition=True) self._syncplayClient._player.openFile(dropfilepath, resetPosition=True)
self._syncplayClient.setPosition(0) self._syncplayClient.setPosition(0)
def saveSettings(self): def saveSettings(self):
settings = QSettings("Syncplay", "MainWindow") settings = QSettings("Syncplay", "MainWindow")
settings.beginGroup("MainWindow") settings.beginGroup("MainWindow")
settings.setValue("size", self.size()) settings.setValue("size", self.size())
settings.setValue("pos", self.pos()) settings.setValue("pos", self.pos())
settings.endGroup() settings.endGroup()
def loadSettings(self): def loadSettings(self):
settings = QSettings("Syncplay", "MainWindow") settings = QSettings("Syncplay", "MainWindow")
settings.beginGroup("MainWindow") settings.beginGroup("MainWindow")
@ -468,8 +656,9 @@ class MainWindow(QtGui.QMainWindow):
self.addBottomLayout(self) self.addBottomLayout(self)
self.addMenubar(self) self.addMenubar(self)
self.addMainFrame(self) self.addMainFrame(self)
self.setupSizes()
self.loadSettings() self.loadSettings()
self.setWindowIcon(QtGui.QIcon(self.resourcespath + "syncplay.png")) self.setWindowIcon(QtGui.QIcon(self.resourcespath + "syncplay.png"))
self.setWindowFlags(self.windowFlags() & Qt.WindowCloseButtonHint & Qt.AA_DontUseNativeMenuBar & Qt.WindowMinimizeButtonHint & ~Qt.WindowContextHelpButtonHint) self.setWindowFlags(self.windowFlags() & Qt.WindowCloseButtonHint & Qt.AA_DontUseNativeMenuBar & Qt.WindowMinimizeButtonHint & ~Qt.WindowContextHelpButtonHint)
self.show() self.show()
self.setAcceptDrops(True) self.setAcceptDrops(True)

View File

@ -7,6 +7,8 @@ import sys
import os import os
import itertools import itertools
import hashlib import hashlib
import random
import string
def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None): def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None):
"""Retry calling the decorated function using an exponential backoff. """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) formattedTime = "{0:} (Title {1:.0f})".format(formattedTime, title)
return formattedTime 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(): def findWorkingDir():
frozen = getattr(sys, 'frozen', '') frozen = getattr(sys, 'frozen', '')
if not frozen: if not frozen:
@ -141,7 +156,19 @@ def blackholeStdoutForFrozenWindow():
# Relate to file hashing / difference checking: # Relate to file hashing / difference checking:
def stripfilename(filename): 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): def hashFilename(filename):
return hashlib.sha256(stripfilename(filename).encode('utf-8')).hexdigest()[:12] return hashlib.sha256(stripfilename(filename).encode('utf-8')).hexdigest()[:12]
@ -182,3 +209,69 @@ def sameFileduration (duration1, duration2):
return True return True
else: else:
return False 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() argsGetter = ConfigurationGetter()
args = argsGetter.getConfiguration() 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() reactor.run()