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