diff --git a/syncplay/clientManager.py b/syncplay/clientManager.py index 58df21a..d2c269f 100644 --- a/syncplay/clientManager.py +++ b/syncplay/clientManager.py @@ -1,26 +1,27 @@ -from syncplay.client import SyncplayClient -from syncplay.ui.ConfigurationGetter import ConfigurationGetter -from syncplay import ui -try: - from syncplay.players.mpc import MPCHCAPIPlayer -except ImportError: - MPCHCAPIPlayer = None -from syncplay.players.mplayer import MplayerPlayer - -class SyncplayClientManager(object): - def run(self): - config = ConfigurationGetter().getConfiguration() - interface = ui.getUi(graphical=not config["noGui"]) - syncplayClient = None - if(config['playerType'] == "mpc"): - syncplayClient = SyncplayClient(MPCHCAPIPlayer, interface, config) - elif(config['playerType'] == "mplayer"): - syncplayClient = SyncplayClient(MplayerPlayer, interface, config) - if(syncplayClient): - interface.addClient(syncplayClient) - syncplayClient.start(config['host'], config['port']) - else: - interface.showErrorMessage("Unable to start client") - - - +from syncplay.client import SyncplayClient +from syncplay.ui.ConfigurationGetter import ConfigurationGetter +from syncplay import ui +from syncplay.messages import getMessage +try: + from syncplay.players.mpc import MPCHCAPIPlayer +except ImportError: + MPCHCAPIPlayer = None +from syncplay.players.mplayer import MplayerPlayer + +class SyncplayClientManager(object): + def run(self): + config = ConfigurationGetter().getConfiguration() + interface = ui.getUi(graphical=not config["noGui"]) + syncplayClient = None + if(config['playerType'] == "mpc"): + syncplayClient = SyncplayClient(MPCHCAPIPlayer, interface, config) + elif(config['playerType'] == "mplayer"): + syncplayClient = SyncplayClient(MplayerPlayer, interface, config) + if(syncplayClient): + interface.addClient(syncplayClient) + syncplayClient.start(config['host'], config['port']) + else: + interface.showErrorMessage(getMessage("en", "unable-to-start-client-error")) + + + diff --git a/syncplay/messages.py b/syncplay/messages.py index d51190e..301bdbf 100644 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -7,6 +7,8 @@ en = { "reconnection-attempt-notification" : "Connection with server lost, attempting to reconnect", "disconnection-notification" : "Disconnected from server", "connection-failed-notification" : "Connection with server failed", + "connected-successful-notification" : "Successfully connected to server", + "retrying-notification" : "%s, Retrying in %d seconds...", #Seconds "rewind-notification" : "Rewinded due to time difference with <{}>", #User "slowdown-notification" : "Slowing down due to time difference with <{}>", #User @@ -31,11 +33,85 @@ en = { "notplaying-notification" : "People who are not playing any file:", "userlist-room-notification" : "In room '{}':", #Room + "mplayer-file-required-notification" : "Syncplay using mplayer requires you to provide file when starting", + "mplayer-file-required-notification/example" : "Usage example: syncplay [options] [url|path/]filename", + + "unrecognized-command-notification" : "Unrecognized command", + "commandlist-notification" : "Available commands:", + "commandlist-notification/room" : "\tr [name] - change room", + "commandlist-notification/list" : "\tl - show user list", + "commandlist-notification/undo" : "\tu - undo last seek", + "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", + "syncplay-version-notification" : "Syncplay version: {}", #syncplay.version + "more-info-notification" : "More info available at: {}", #projectURL + # Client prompts "enter-to-exit-prompt" : "Press enter to exit\n", # Client errors - "server-timeout-error" : "Connection with server timed out" + "server-timeout-error" : "Connection with server timed out", + "mpc-slave-error" : "Unable to start MPC in slave mode!", + "mpc-version-insufficient-error" : "MPC version not sufficient, please use `mpc-hc` >= `1.6.4`", + "player-file-open-error" : "Player failed opening file", + "player-path-error" : "Player path is not set properly", + "hostname-empty-error" : "Hostname can't be empty", + "empty-error" : "{} can't be empty", #Configuration + + "arguments-missing-error" : "Some necessary arguments are missing, refer to --help", + + "unable-to-start-client-error" : "Unable to start client", + + "not-json-error" : "Not a json encoded string\n", + "hello-arguments-error" : "Not enough Hello arguments\n", + "version-mismatch-error" : "Mismatch between versions of client and server\n", + + # Client arguments + "argument-description" : 'Solution to synchronize playback of multiple MPlayer and MPC-HC instances over the network.', + "argument-epilog" : 'If no options supplied _config values will be used', + "nogui-argument" : 'show no GUI', + "host-argument" : 'server\'s address', + "name-argument" : 'desired username', + "debug-argument" : 'debug mode', + "force-gui-prompt-argument" : 'make configuration prompt appear', + "no-store-argument" : 'don\'t store values in .syncplay', + "room-argument" : 'default room', + "password-argument" : 'server password', + "player-path-argument" : 'path to your player executable', + "file-argument" : 'file to play', + "args-argument" : 'player options, if you need to pass options starting with - prepend them with single \'--\' argument', + + # Client labels + "host-label" : 'Host: ', + "username-label" : 'Username: ', + "room-label" : 'Default room (optional): ', + "password-label" : 'Server password (optional): ', + "path-label" : 'Path to player executable: ', + + # Server notifications + "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 + + + #Server arguments + "server-argument-description" : 'Solution to synchronize playback of multiple MPlayer and MPC-HC instances over the network. Server instance', + "server-argument-epilog" : 'If no options supplied _config values will be used', + "server-port-argument" : 'server TCP port', + "server-password-argument" : 'server password', + "server-isolate-room-argument" : 'should rooms be isolated?', + + #Server errors + "not-known-server-error" : "You must be known to server before sending this command", + "client-drop-server-error" : "Client drop: %s -- %s", #host, error + "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", + "wrong-password-server-error" : "Wrong password supplied" + + } pl = { diff --git a/syncplay/players/mpc.py b/syncplay/players/mpc.py index f6124cf..d8418dc 100644 --- a/syncplay/players/mpc.py +++ b/syncplay/players/mpc.py @@ -1,454 +1,455 @@ -#coding:utf8 -import time -import threading -import thread -import win32con, win32api, win32gui, ctypes, ctypes.wintypes #@UnresolvedImport @UnusedImport -from functools import wraps -from syncplay.players.basePlayer import BasePlayer -import re -from syncplay.utils import retry -from syncplay import constants - -class MpcHcApi: - def __init__(self): - self.callbacks = self.__Callbacks() - self.loadState = None - self.playState = None - self.filePlaying = None - self.fileDuration = None - self.filePath = None - self.lastFilePosition = None - self.version = None - self.__playpause_warden = False - self.__locks = self.__Locks() - self.__mpcExistenceChecking = threading.Thread(target=self.__mpcReadyInSlaveMode, name="Check MPC window") - self.__mpcExistenceChecking.setDaemon(True) - self.__listener = self.__Listener(self, self.__locks) - self.__listener.setDaemon(True) - self.__listener.start() - self.__locks.listenerStart.wait() - - def waitForFileStateReady(f): #@NoSelf - @wraps(f) - def wrapper(self, *args, **kwds): - if(not self.__locks.fileReady.wait(constants.MPC_LOCK_WAIT_TIME)): - raise self.PlayerNotReadyException() - return f(self, *args, **kwds) - return wrapper - - def startMpc(self, path, args=()): - args = "%s /slave %s" % (" ".join(args), str(self.__listener.hwnd)) - win32api.ShellExecute(0, "open", path, args, None, 1) - if(not self.__locks.mpcStart.wait(constants.MPC_OPEN_MAX_WAIT_TIME)): - raise self.NoSlaveDetectedException("Unable to start MPC in slave mode!") - self.__mpcExistenceChecking.start() - - def openFile(self, filePath): - self.__listener.SendCommand(self.CMD_OPENFILE, filePath) - - def isPaused(self): - return (self.playState <> self.__MPC_PLAYSTATE.PS_PLAY and self.playState <> None) - - def askForVersion(self): - self.__listener.SendCommand(self.CMD_GETVERSION) - - @waitForFileStateReady - def pause(self): - self.__listener.SendCommand(self.CMD_PAUSE) - - @waitForFileStateReady - def playPause(self): - self.__listener.SendCommand(self.CMD_PLAYPAUSE) - - @waitForFileStateReady - def unpause(self): - self.__listener.SendCommand(self.CMD_PLAY) - - @waitForFileStateReady - def askForCurrentPosition(self): - self.__listener.SendCommand(self.CMD_GETCURRENTPOSITION) - - @waitForFileStateReady - def seek(self, position): - self.__listener.SendCommand(self.CMD_SETPOSITION, unicode(position)) - - @waitForFileStateReady - def setSpeed(self, rate): - self.__listener.SendCommand(self.CMD_SETSPEED, unicode(rate)) - - def sendOsd(self, message, MsgPos=constants.MPC_OSD_POSITION, DurationMs=constants.OSD_DURATION): - class __OSDDATASTRUCT(ctypes.Structure): - _fields_ = [ - ('nMsgPos', ctypes.c_int32), - ('nDurationMS', ctypes.c_int32), - ('strMsg', ctypes.c_wchar * (len(message) + 1)) - ] - cmessage = __OSDDATASTRUCT() - cmessage.nMsgPos = MsgPos - cmessage.nDurationMS = DurationMs - cmessage.strMsg = message - self.__listener.SendCommand(self.CMD_OSDSHOWMESSAGE, cmessage) - - def sendRawCommand(self, cmd, value): - self.__listener.SendCommand(cmd, value) - - def handleCommand(self, cmd, value): - if (cmd == self.CMD_CONNECT): - self.__listener.mpcHandle = int(value) - self.__locks.mpcStart.set() - if(self.callbacks.onConnected): - thread.start_new_thread(self.callbacks.onConnected, ()) - - elif(cmd == self.CMD_STATE): - self.loadState = int(value) - fileNotReady = self.loadState == self.__MPC_LOADSTATE.MLS_CLOSING or self.loadState == self.__MPC_LOADSTATE.MLS_LOADING - if(fileNotReady): - self.playState = None - self.__locks.fileReady.clear() - else: - self.__locks.fileReady.set() - if(self.callbacks.onFileStateChange): - thread.start_new_thread(self.callbacks.onFileStateChange, (self.loadState,)) - - elif(cmd == self.CMD_PLAYMODE): - self.playState = int(value) - if(self.callbacks.onUpdatePlaystate): - thread.start_new_thread(self.callbacks.onUpdatePlaystate, (self.playState,)) - - elif(cmd == self.CMD_NOWPLAYING): - value = re.split(r'(? float(value)): #Notify seek is sometimes sent twice - self.lastFilePosition = float(value) - if(self.callbacks.onSeek): - thread.start_new_thread(self.callbacks.onSeek, (self.lastFilePosition,)) - - elif(cmd == self.CMD_DISCONNECT): - if(self.callbacks.onMpcClosed): - thread.start_new_thread(self.callbacks.onMpcClosed, (None,)) - - elif(cmd == self.CMD_VERSION): - if(self.callbacks.onVersion): - self.version = value - thread.start_new_thread(self.callbacks.onVersion, (value,)) - - class PlayerNotReadyException(Exception): - pass - - class __Callbacks: - def __init__(self): - self.onConnected = None - self.onSeek = None - self.onUpdatePath = None - self.onUpdateFilename = None - self.onUpdateFileDuration = None - self.onGetCurrentPosition = None - self.onUpdatePlaystate = None - self.onFileStateChange = None - self.onMpcClosed = None - self.onVersion = None - - class __Locks: - def __init__(self): - self.listenerStart = threading.Event() - self.mpcStart = threading.Event() - self.fileReady = threading.Event() - - def __mpcReadyInSlaveMode(self): - while(True): - time.sleep(10) - if not win32gui.IsWindow(self.__listener.mpcHandle): - if(self.callbacks.onMpcClosed): - self.callbacks.onMpcClosed(None) - break - - CMD_CONNECT = 0x50000000 - CMD_STATE = 0x50000001 - CMD_PLAYMODE = 0x50000002 - CMD_NOWPLAYING = 0x50000003 - CMD_LISTSUBTITLETRACKS = 0x50000004 - CMD_LISTAUDIOTRACKS = 0x50000005 - CMD_CURRENTPOSITION = 0x50000007 - CMD_NOTIFYSEEK = 0x50000008 - CMD_NOTIFYENDOFSTREAM = 0x50000009 - CMD_PLAYLIST = 0x50000006 - CMD_OPENFILE = 0xA0000000 - CMD_STOP = 0xA0000001 - CMD_CLOSEFILE = 0xA0000002 - CMD_PLAYPAUSE = 0xA0000003 - CMD_ADDTOPLAYLIST = 0xA0001000 - CMD_CLEARPLAYLIST = 0xA0001001 - CMD_STARTPLAYLIST = 0xA0001002 - CMD_REMOVEFROMPLAYLIST = 0xA0001003 # TODO - CMD_SETPOSITION = 0xA0002000 - CMD_SETAUDIODELAY = 0xA0002001 - CMD_SETSUBTITLEDELAY = 0xA0002002 - CMD_SETINDEXPLAYLIST = 0xA0002003 # DOESNT WORK - CMD_SETAUDIOTRACK = 0xA0002004 - CMD_SETSUBTITLETRACK = 0xA0002005 - CMD_GETSUBTITLETRACKS = 0xA0003000 - CMD_GETCURRENTPOSITION = 0xA0003004 - CMD_JUMPOFNSECONDS = 0xA0003005 - CMD_GETAUDIOTRACKS = 0xA0003001 - CMD_GETNOWPLAYING = 0xA0003002 - CMD_GETPLAYLIST = 0xA0003003 - CMD_TOGGLEFULLSCREEN = 0xA0004000 - CMD_JUMPFORWARDMED = 0xA0004001 - CMD_JUMPBACKWARDMED = 0xA0004002 - CMD_INCREASEVOLUME = 0xA0004003 - CMD_DECREASEVOLUME = 0xA0004004 - CMD_SHADER_TOGGLE = 0xA0004005 - CMD_CLOSEAPP = 0xA0004006 - CMD_OSDSHOWMESSAGE = 0xA0005000 - CMD_VERSION = 0x5000000A - CMD_DISCONNECT = 0x5000000B - CMD_PLAY = 0xA0000004 - CMD_PAUSE = 0xA0000005 - CMD_GETVERSION = 0xA0003006 - CMD_SETSPEED = 0xA0004008 - - class __MPC_LOADSTATE: - MLS_CLOSED = 0 - MLS_LOADING = 1 - MLS_LOADED = 2 - MLS_CLOSING = 3 - - class __MPC_PLAYSTATE: - PS_PLAY = 0 - PS_PAUSE = 1 - PS_STOP = 2 - PS_UNUSED = 3 - - class __Listener(threading.Thread): - def __init__(self, mpcApi, locks): - self.__mpcApi = mpcApi - self.locks = locks - self.mpcHandle = None - self.hwnd = None - self.__PCOPYDATASTRUCT = ctypes.POINTER(self.__COPYDATASTRUCT) - threading.Thread.__init__(self, name="MPC Listener") - - def run(self): - message_map = { - win32con.WM_COPYDATA: self.OnCopyData - } - wc = win32gui.WNDCLASS() - wc.lpfnWndProc = message_map - wc.lpszClassName = 'MPCApiListener' - hinst = wc.hInstance = win32api.GetModuleHandle(None) - classAtom = win32gui.RegisterClass(wc) - self.hwnd = win32gui.CreateWindow ( - classAtom, - "ListenerGUI", - 0, - 0, - 0, - win32con.CW_USEDEFAULT, - win32con.CW_USEDEFAULT, - 0, - 0, - hinst, - None - ) - self.locks.listenerStart.set() - win32gui.PumpMessages() - - - def OnCopyData(self, hwnd, msg, wparam, lparam): - pCDS = ctypes.cast(lparam, self.__PCOPYDATASTRUCT) - #print "API:\tin>\t 0x%X\t" % int(pCDS.contents.dwData), ctypes.wstring_at(pCDS.contents.lpData) - self.__mpcApi.handleCommand(pCDS.contents.dwData, ctypes.wstring_at(pCDS.contents.lpData)) - - def SendCommand(self, cmd, message=u''): - #print "API:\t= `1.6.4`") - self.__client.stop(True) - - def __testMpcReady(self): - if(not self.__preventAsking.wait(10)): - raise Exception("Player failed opening file") - - def __makePing(self): - try: - self.__testMpcReady() - self._mpcApi.callbacks.onUpdateFilename = lambda _: self.__handleUpdatedFilename() - self.__client.initPlayer(self) - self.__handleUpdatedFilename() - self.askForStatus() - except Exception, err: - self.__client.ui.showErrorMessage(err.message) - self.__client.stop() - - def initPlayer(self, filePath): - self.__dropIfNotSufficientVersion() - if(not self._mpcApi.version): - return - self.__mpcVersion = self._mpcApi.version.split('.') - if(self.__mpcVersion[0:3] == ['1', '6', '4']): - self.__switchPauseCalls = True - if(self.__mpcVersion[0:3] >= ['1', '6', '5']): - self.speedSupported = True - if(filePath): - self._mpcApi.openFile(filePath) - - def displayMessage(self, message): - self._mpcApi.sendOsd(message) - - @retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1) - def setPaused(self, value): - if self.__switchPauseCalls: - value = not value - if value: - self._mpcApi.pause() - else: - self._mpcApi.unpause() - - @retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1) - def setPosition(self, value): - self._mpcApi.seek(value) - - def __getPosition(self): - self.__positionUpdate.clear() - self._mpcApi.askForCurrentPosition() - self.__positionUpdate.wait(constants.MPC_LOCK_WAIT_TIME) - return self._mpcApi.lastFilePosition - - @retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1) - def askForStatus(self): - if(self.__preventAsking.wait(0) and self.__fileUpdate.acquire(0)): - self.__fileUpdate.release() - position = self.__getPosition() - paused = self._mpcApi.isPaused() - position = float(position) - if(self.__preventAsking.wait(0) and self.__fileUpdate.acquire(0)): - self.__client.updatePlayerStatus(paused, position) - self.__fileUpdate.release() - return - self.__echoGlobalStatus() - - def __echoGlobalStatus(self): - self.__client.updatePlayerStatus(self.__client.getGlobalPaused(), self.__client.getGlobalPosition()) - - def __forcePause(self): - for _ in xrange(constants.MPC_MAX_RETRIES): - self.setPaused(True) - time.sleep(constants.MPC_RETRY_WAIT_TIME) - - def __refreshMpcPlayState(self): - for _ in xrange(2): - self._mpcApi.playPause() - time.sleep(constants.MPC_PAUSE_TOGGLE_DELAY) - - def _setPausedAccordinglyToServer(self): - self.__forcePause() - self.setPaused(self.__client.getGlobalPaused()) - if(self._mpcApi.isPaused() <> self.__client.getGlobalPaused()): - self.__refreshMpcPlayState() - if(self._mpcApi.isPaused() <> self.__client.getGlobalPaused()): - self.__setUpStateForNewlyOpenedFile() - - @retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1) - def __setUpStateForNewlyOpenedFile(self): - self._setPausedAccordinglyToServer() - self._mpcApi.seek(self.__client.getGlobalPosition()) - - def __handleUpdatedFilename(self): - with self.__fileUpdate: - self.__setUpStateForNewlyOpenedFile() - self.__client.updateFile(self._mpcApi.filePlaying, self._mpcApi.fileDuration, self._mpcApi.filePath) - - def __mpcError(self, err=""): - self.__client.ui.showErrorMessage(err) - self.__client.stop() - - def sendCustomCommand(self, cmd, val): - self._mpcApi.sendRawCommand(cmd, val) +#coding:utf8 +import time +import threading +import thread +import win32con, win32api, win32gui, ctypes, ctypes.wintypes #@UnresolvedImport @UnusedImport +from functools import wraps +from syncplay.players.basePlayer import BasePlayer +import re +from syncplay.utils import retry +from syncplay import constants +from syncplay.messages import getMessage + +class MpcHcApi: + def __init__(self): + self.callbacks = self.__Callbacks() + self.loadState = None + self.playState = None + self.filePlaying = None + self.fileDuration = None + self.filePath = None + self.lastFilePosition = None + self.version = None + self.__playpause_warden = False + self.__locks = self.__Locks() + self.__mpcExistenceChecking = threading.Thread(target=self.__mpcReadyInSlaveMode, name="Check MPC window") + self.__mpcExistenceChecking.setDaemon(True) + self.__listener = self.__Listener(self, self.__locks) + self.__listener.setDaemon(True) + self.__listener.start() + self.__locks.listenerStart.wait() + + def waitForFileStateReady(f): #@NoSelf + @wraps(f) + def wrapper(self, *args, **kwds): + if(not self.__locks.fileReady.wait(constants.MPC_LOCK_WAIT_TIME)): + raise self.PlayerNotReadyException() + return f(self, *args, **kwds) + return wrapper + + def startMpc(self, path, args=()): + args = "%s /slave %s" % (" ".join(args), str(self.__listener.hwnd)) + win32api.ShellExecute(0, "open", path, args, None, 1) + if(not self.__locks.mpcStart.wait(constants.MPC_OPEN_MAX_WAIT_TIME)): + raise self.NoSlaveDetectedException(getMessage("en", "mpc-slave-error")) + self.__mpcExistenceChecking.start() + + def openFile(self, filePath): + self.__listener.SendCommand(self.CMD_OPENFILE, filePath) + + def isPaused(self): + return (self.playState <> self.__MPC_PLAYSTATE.PS_PLAY and self.playState <> None) + + def askForVersion(self): + self.__listener.SendCommand(self.CMD_GETVERSION) + + @waitForFileStateReady + def pause(self): + self.__listener.SendCommand(self.CMD_PAUSE) + + @waitForFileStateReady + def playPause(self): + self.__listener.SendCommand(self.CMD_PLAYPAUSE) + + @waitForFileStateReady + def unpause(self): + self.__listener.SendCommand(self.CMD_PLAY) + + @waitForFileStateReady + def askForCurrentPosition(self): + self.__listener.SendCommand(self.CMD_GETCURRENTPOSITION) + + @waitForFileStateReady + def seek(self, position): + self.__listener.SendCommand(self.CMD_SETPOSITION, unicode(position)) + + @waitForFileStateReady + def setSpeed(self, rate): + self.__listener.SendCommand(self.CMD_SETSPEED, unicode(rate)) + + def sendOsd(self, message, MsgPos=constants.MPC_OSD_POSITION, DurationMs=constants.OSD_DURATION): + class __OSDDATASTRUCT(ctypes.Structure): + _fields_ = [ + ('nMsgPos', ctypes.c_int32), + ('nDurationMS', ctypes.c_int32), + ('strMsg', ctypes.c_wchar * (len(message) + 1)) + ] + cmessage = __OSDDATASTRUCT() + cmessage.nMsgPos = MsgPos + cmessage.nDurationMS = DurationMs + cmessage.strMsg = message + self.__listener.SendCommand(self.CMD_OSDSHOWMESSAGE, cmessage) + + def sendRawCommand(self, cmd, value): + self.__listener.SendCommand(cmd, value) + + def handleCommand(self, cmd, value): + if (cmd == self.CMD_CONNECT): + self.__listener.mpcHandle = int(value) + self.__locks.mpcStart.set() + if(self.callbacks.onConnected): + thread.start_new_thread(self.callbacks.onConnected, ()) + + elif(cmd == self.CMD_STATE): + self.loadState = int(value) + fileNotReady = self.loadState == self.__MPC_LOADSTATE.MLS_CLOSING or self.loadState == self.__MPC_LOADSTATE.MLS_LOADING + if(fileNotReady): + self.playState = None + self.__locks.fileReady.clear() + else: + self.__locks.fileReady.set() + if(self.callbacks.onFileStateChange): + thread.start_new_thread(self.callbacks.onFileStateChange, (self.loadState,)) + + elif(cmd == self.CMD_PLAYMODE): + self.playState = int(value) + if(self.callbacks.onUpdatePlaystate): + thread.start_new_thread(self.callbacks.onUpdatePlaystate, (self.playState,)) + + elif(cmd == self.CMD_NOWPLAYING): + value = re.split(r'(? float(value)): #Notify seek is sometimes sent twice + self.lastFilePosition = float(value) + if(self.callbacks.onSeek): + thread.start_new_thread(self.callbacks.onSeek, (self.lastFilePosition,)) + + elif(cmd == self.CMD_DISCONNECT): + if(self.callbacks.onMpcClosed): + thread.start_new_thread(self.callbacks.onMpcClosed, (None,)) + + elif(cmd == self.CMD_VERSION): + if(self.callbacks.onVersion): + self.version = value + thread.start_new_thread(self.callbacks.onVersion, (value,)) + + class PlayerNotReadyException(Exception): + pass + + class __Callbacks: + def __init__(self): + self.onConnected = None + self.onSeek = None + self.onUpdatePath = None + self.onUpdateFilename = None + self.onUpdateFileDuration = None + self.onGetCurrentPosition = None + self.onUpdatePlaystate = None + self.onFileStateChange = None + self.onMpcClosed = None + self.onVersion = None + + class __Locks: + def __init__(self): + self.listenerStart = threading.Event() + self.mpcStart = threading.Event() + self.fileReady = threading.Event() + + def __mpcReadyInSlaveMode(self): + while(True): + time.sleep(10) + if not win32gui.IsWindow(self.__listener.mpcHandle): + if(self.callbacks.onMpcClosed): + self.callbacks.onMpcClosed(None) + break + + CMD_CONNECT = 0x50000000 + CMD_STATE = 0x50000001 + CMD_PLAYMODE = 0x50000002 + CMD_NOWPLAYING = 0x50000003 + CMD_LISTSUBTITLETRACKS = 0x50000004 + CMD_LISTAUDIOTRACKS = 0x50000005 + CMD_CURRENTPOSITION = 0x50000007 + CMD_NOTIFYSEEK = 0x50000008 + CMD_NOTIFYENDOFSTREAM = 0x50000009 + CMD_PLAYLIST = 0x50000006 + CMD_OPENFILE = 0xA0000000 + CMD_STOP = 0xA0000001 + CMD_CLOSEFILE = 0xA0000002 + CMD_PLAYPAUSE = 0xA0000003 + CMD_ADDTOPLAYLIST = 0xA0001000 + CMD_CLEARPLAYLIST = 0xA0001001 + CMD_STARTPLAYLIST = 0xA0001002 + CMD_REMOVEFROMPLAYLIST = 0xA0001003 # TODO + CMD_SETPOSITION = 0xA0002000 + CMD_SETAUDIODELAY = 0xA0002001 + CMD_SETSUBTITLEDELAY = 0xA0002002 + CMD_SETINDEXPLAYLIST = 0xA0002003 # DOESNT WORK + CMD_SETAUDIOTRACK = 0xA0002004 + CMD_SETSUBTITLETRACK = 0xA0002005 + CMD_GETSUBTITLETRACKS = 0xA0003000 + CMD_GETCURRENTPOSITION = 0xA0003004 + CMD_JUMPOFNSECONDS = 0xA0003005 + CMD_GETAUDIOTRACKS = 0xA0003001 + CMD_GETNOWPLAYING = 0xA0003002 + CMD_GETPLAYLIST = 0xA0003003 + CMD_TOGGLEFULLSCREEN = 0xA0004000 + CMD_JUMPFORWARDMED = 0xA0004001 + CMD_JUMPBACKWARDMED = 0xA0004002 + CMD_INCREASEVOLUME = 0xA0004003 + CMD_DECREASEVOLUME = 0xA0004004 + CMD_SHADER_TOGGLE = 0xA0004005 + CMD_CLOSEAPP = 0xA0004006 + CMD_OSDSHOWMESSAGE = 0xA0005000 + CMD_VERSION = 0x5000000A + CMD_DISCONNECT = 0x5000000B + CMD_PLAY = 0xA0000004 + CMD_PAUSE = 0xA0000005 + CMD_GETVERSION = 0xA0003006 + CMD_SETSPEED = 0xA0004008 + + class __MPC_LOADSTATE: + MLS_CLOSED = 0 + MLS_LOADING = 1 + MLS_LOADED = 2 + MLS_CLOSING = 3 + + class __MPC_PLAYSTATE: + PS_PLAY = 0 + PS_PAUSE = 1 + PS_STOP = 2 + PS_UNUSED = 3 + + class __Listener(threading.Thread): + def __init__(self, mpcApi, locks): + self.__mpcApi = mpcApi + self.locks = locks + self.mpcHandle = None + self.hwnd = None + self.__PCOPYDATASTRUCT = ctypes.POINTER(self.__COPYDATASTRUCT) + threading.Thread.__init__(self, name="MPC Listener") + + def run(self): + message_map = { + win32con.WM_COPYDATA: self.OnCopyData + } + wc = win32gui.WNDCLASS() + wc.lpfnWndProc = message_map + wc.lpszClassName = 'MPCApiListener' + hinst = wc.hInstance = win32api.GetModuleHandle(None) + classAtom = win32gui.RegisterClass(wc) + self.hwnd = win32gui.CreateWindow ( + classAtom, + "ListenerGUI", + 0, + 0, + 0, + win32con.CW_USEDEFAULT, + win32con.CW_USEDEFAULT, + 0, + 0, + hinst, + None + ) + self.locks.listenerStart.set() + win32gui.PumpMessages() + + + def OnCopyData(self, hwnd, msg, wparam, lparam): + pCDS = ctypes.cast(lparam, self.__PCOPYDATASTRUCT) + #print "API:\tin>\t 0x%X\t" % int(pCDS.contents.dwData), ctypes.wstring_at(pCDS.contents.lpData) + self.__mpcApi.handleCommand(pCDS.contents.dwData, ctypes.wstring_at(pCDS.contents.lpData)) + + def SendCommand(self, cmd, message=u''): + #print "API:\t= ['1', '6', '5']): + self.speedSupported = True + if(filePath): + self._mpcApi.openFile(filePath) + + def displayMessage(self, message): + self._mpcApi.sendOsd(message) + + @retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1) + def setPaused(self, value): + if self.__switchPauseCalls: + value = not value + if value: + self._mpcApi.pause() + else: + self._mpcApi.unpause() + + @retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1) + def setPosition(self, value): + self._mpcApi.seek(value) + + def __getPosition(self): + self.__positionUpdate.clear() + self._mpcApi.askForCurrentPosition() + self.__positionUpdate.wait(constants.MPC_LOCK_WAIT_TIME) + return self._mpcApi.lastFilePosition + + @retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1) + def askForStatus(self): + if(self.__preventAsking.wait(0) and self.__fileUpdate.acquire(0)): + self.__fileUpdate.release() + position = self.__getPosition() + paused = self._mpcApi.isPaused() + position = float(position) + if(self.__preventAsking.wait(0) and self.__fileUpdate.acquire(0)): + self.__client.updatePlayerStatus(paused, position) + self.__fileUpdate.release() + return + self.__echoGlobalStatus() + + def __echoGlobalStatus(self): + self.__client.updatePlayerStatus(self.__client.getGlobalPaused(), self.__client.getGlobalPosition()) + + def __forcePause(self): + for _ in xrange(constants.MPC_MAX_RETRIES): + self.setPaused(True) + time.sleep(constants.MPC_RETRY_WAIT_TIME) + + def __refreshMpcPlayState(self): + for _ in xrange(2): + self._mpcApi.playPause() + time.sleep(constants.MPC_PAUSE_TOGGLE_DELAY) + + def _setPausedAccordinglyToServer(self): + self.__forcePause() + self.setPaused(self.__client.getGlobalPaused()) + if(self._mpcApi.isPaused() <> self.__client.getGlobalPaused()): + self.__refreshMpcPlayState() + if(self._mpcApi.isPaused() <> self.__client.getGlobalPaused()): + self.__setUpStateForNewlyOpenedFile() + + @retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1) + def __setUpStateForNewlyOpenedFile(self): + self._setPausedAccordinglyToServer() + self._mpcApi.seek(self.__client.getGlobalPosition()) + + def __handleUpdatedFilename(self): + with self.__fileUpdate: + self.__setUpStateForNewlyOpenedFile() + self.__client.updateFile(self._mpcApi.filePlaying, self._mpcApi.fileDuration, self._mpcApi.filePath) + + def __mpcError(self, err=""): + self.__client.ui.showErrorMessage(err) + self.__client.stop() + + def sendCustomCommand(self, cmd, val): + self._mpcApi.sendRawCommand(cmd, val) diff --git a/syncplay/players/mplayer.py b/syncplay/players/mplayer.py index 8c3a082..19d80c1 100644 --- a/syncplay/players/mplayer.py +++ b/syncplay/players/mplayer.py @@ -1,170 +1,171 @@ -import subprocess -import re -import threading -from syncplay.players.basePlayer import BasePlayer -from syncplay import constants - -class MplayerPlayer(BasePlayer): - speedSupported = True - RE_ANSWER = re.compile(constants.MPLAYER_ANSWER_REGEX) - def __init__(self, client, playerPath, filePath, args): - self._client = client - self._paused = None - self._duration = None - self._filename = None - self._filepath = None - try: - self._listener = self.__Listener(self, playerPath, filePath, args) - except ValueError: - self._client.ui.showMessage("Syncplay using mplayer requires you to provide file when starting") - self._client.ui.showMessage("Usage example: syncplay [options] [url|path/]filename") - self._client.stop(True) - return - self._listener.setDaemon(True) - self._listener.start() - - self._durationAsk = threading.Event() - self._filenameAsk = threading.Event() - self._pathAsk = threading.Event() - - self._positionAsk = threading.Event() - self._pausedAsk = threading.Event() - - self._preparePlayer() - - def _fileUpdateClearEvents(self): - self._durationAsk.clear() - self._filenameAsk.clear() - self._pathAsk.clear() - - def _fileUpdateWaitEvents(self): - self._durationAsk.wait() - self._filenameAsk.wait() - self._pathAsk.wait() - - def _onFileUpdate(self): - self._fileUpdateClearEvents() - self._getFilename() - self._getLength() - self._getFilepath() - self._fileUpdateWaitEvents() - self._client.updateFile(self._filename, self._duration, self._filepath) - - def _preparePlayer(self): - self.setPaused(self._client.getGlobalPaused()) - self.setPosition(self._client.getGlobalPosition()) - self._client.initPlayer(self) - self._onFileUpdate() - - def askForStatus(self): - self._positionAsk.clear() - self._pausedAsk.clear() - self._getPaused() - self._getPosition() - self._positionAsk.wait() - self._pausedAsk.wait() - self._client.updatePlayerStatus(self._paused, self._position) - - def _setProperty(self, property_, value): - self._listener.sendLine("set_property {} {}".format(property_, value)) - - def _getProperty(self, property_): - self._listener.sendLine("get_property {}".format(property_)) - - def displayMessage(self, message): - self._listener.sendLine('osd_show_text "{!s}" {} {}'.format(message, constants.OSD_DURATION, constants.MPLAYER_OSD_LEVEL)) - - def setSpeed(self, value): - self._setProperty('speed', "{:.2f}".format(value)) - - def setPosition(self, value): - self._position = value - self._setProperty('time_pos', "{}".format(value)) - - def setPaused(self, value): - self._paused = value - self._setProperty('pause', 'yes' if value else 'no') - - def _getFilename(self): - self._getProperty('filename') - - def _getLength(self): - self._getProperty('length') - - def _getFilepath(self): - self._getProperty('path') - - def _getPaused(self): - self._getProperty('pause') - - def _getPosition(self): - self._getProperty('time_pos') - - def lineReceived(self, line): - match = self.RE_ANSWER.match(line) - if not match: - return - name, value = match.group(1).lower(), match.group(2) - - if(name == "time_pos"): - self._position = float(value) - self._positionAsk.set() - elif(name == "pause"): - self._paused = bool(value == 'yes') - self._pausedAsk.set() - elif(name == "length"): - self._duration = float(value) - self._durationAsk.set() - elif(name == "path"): - self._filepath = value - self._pathAsk.set() - elif(name == "filename"): - self._filename = value - self._filenameAsk.set() - - @staticmethod - def run(client, playerPath, filePath, args): - mplayer = MplayerPlayer(client, playerPath, filePath, args) - return mplayer - - def drop(self): - self._listener.sendLine('quit') - self._durationAsk.set() - self._filenameAsk.set() - self._pathAsk.set() - self._positionAsk.set() - self._pausedAsk.set() - self._client.stop(False) - for line in self._listener.readStderrLine(): - self._client.ui.showMessage(line, True, True) - - class __Listener(threading.Thread): - def __init__(self, playerController, playerPath, filePath, args): - self.__playerController = playerController - if(not filePath): - raise ValueError - call = [playerPath, filePath] - call.extend(constants.MPLAYER_SLAVE_ARGS) - if(args): - call.extend(args) - self.__process = subprocess.Popen(call, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) - threading.Thread.__init__(self, name="MPlayer Listener") - - def run(self): - while(self.__process.poll() is None): - line = self.__process.stdout.readline() - line = line.rstrip("\r\n") - self.__playerController.lineReceived(line) - self.__playerController.drop() - - def sendLine(self, line): - try: - self.__process.stdin.write(line + "\n") - except IOError: - pass - - def readStderrLine(self): - for line in self.__process.stderr.readlines(): - yield line - - +import subprocess +import re +import threading +from syncplay.players.basePlayer import BasePlayer +from syncplay import constants +from syncplay.messages import getMessage + +class MplayerPlayer(BasePlayer): + speedSupported = True + RE_ANSWER = re.compile(constants.MPLAYER_ANSWER_REGEX) + def __init__(self, client, playerPath, filePath, args): + self._client = client + self._paused = None + self._duration = None + self._filename = None + self._filepath = None + try: + self._listener = self.__Listener(self, playerPath, filePath, args) + except ValueError: + self._client.ui.showMessage(getMessage("en", "mplayer-file-required-notification")) + self._client.ui.showMessage(getMessage("en", "mplayer-file-required-notification/example")) + self._client.stop(True) + return + self._listener.setDaemon(True) + self._listener.start() + + self._durationAsk = threading.Event() + self._filenameAsk = threading.Event() + self._pathAsk = threading.Event() + + self._positionAsk = threading.Event() + self._pausedAsk = threading.Event() + + self._preparePlayer() + + def _fileUpdateClearEvents(self): + self._durationAsk.clear() + self._filenameAsk.clear() + self._pathAsk.clear() + + def _fileUpdateWaitEvents(self): + self._durationAsk.wait() + self._filenameAsk.wait() + self._pathAsk.wait() + + def _onFileUpdate(self): + self._fileUpdateClearEvents() + self._getFilename() + self._getLength() + self._getFilepath() + self._fileUpdateWaitEvents() + self._client.updateFile(self._filename, self._duration, self._filepath) + + def _preparePlayer(self): + self.setPaused(self._client.getGlobalPaused()) + self.setPosition(self._client.getGlobalPosition()) + self._client.initPlayer(self) + self._onFileUpdate() + + def askForStatus(self): + self._positionAsk.clear() + self._pausedAsk.clear() + self._getPaused() + self._getPosition() + self._positionAsk.wait() + self._pausedAsk.wait() + self._client.updatePlayerStatus(self._paused, self._position) + + def _setProperty(self, property_, value): + self._listener.sendLine("set_property {} {}".format(property_, value)) + + def _getProperty(self, property_): + self._listener.sendLine("get_property {}".format(property_)) + + def displayMessage(self, message): + self._listener.sendLine('osd_show_text "{!s}" {} {}'.format(message, constants.OSD_DURATION, constants.MPLAYER_OSD_LEVEL)) + + def setSpeed(self, value): + self._setProperty('speed', "{:.2f}".format(value)) + + def setPosition(self, value): + self._position = value + self._setProperty('time_pos', "{}".format(value)) + + def setPaused(self, value): + self._paused = value + self._setProperty('pause', 'yes' if value else 'no') + + def _getFilename(self): + self._getProperty('filename') + + def _getLength(self): + self._getProperty('length') + + def _getFilepath(self): + self._getProperty('path') + + def _getPaused(self): + self._getProperty('pause') + + def _getPosition(self): + self._getProperty('time_pos') + + def lineReceived(self, line): + match = self.RE_ANSWER.match(line) + if not match: + return + name, value = match.group(1).lower(), match.group(2) + + if(name == "time_pos"): + self._position = float(value) + self._positionAsk.set() + elif(name == "pause"): + self._paused = bool(value == 'yes') + self._pausedAsk.set() + elif(name == "length"): + self._duration = float(value) + self._durationAsk.set() + elif(name == "path"): + self._filepath = value + self._pathAsk.set() + elif(name == "filename"): + self._filename = value + self._filenameAsk.set() + + @staticmethod + def run(client, playerPath, filePath, args): + mplayer = MplayerPlayer(client, playerPath, filePath, args) + return mplayer + + def drop(self): + self._listener.sendLine('quit') + self._durationAsk.set() + self._filenameAsk.set() + self._pathAsk.set() + self._positionAsk.set() + self._pausedAsk.set() + self._client.stop(False) + for line in self._listener.readStderrLine(): + self._client.ui.showMessage(line, True, True) + + class __Listener(threading.Thread): + def __init__(self, playerController, playerPath, filePath, args): + self.__playerController = playerController + if(not filePath): + raise ValueError + call = [playerPath, filePath] + call.extend(constants.MPLAYER_SLAVE_ARGS) + if(args): + call.extend(args) + self.__process = subprocess.Popen(call, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + threading.Thread.__init__(self, name="MPlayer Listener") + + def run(self): + while(self.__process.poll() is None): + line = self.__process.stdout.readline() + line = line.rstrip("\r\n") + self.__playerController.lineReceived(line) + self.__playerController.drop() + + def sendLine(self, line): + try: + self.__process.stdin.write(line + "\n") + except IOError: + pass + + def readStderrLine(self): + for line in self.__process.stderr.readlines(): + yield line + + diff --git a/syncplay/protocols.py b/syncplay/protocols.py index 704a5e1..305759f 100644 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -1,395 +1,396 @@ -#coding:utf8 -from twisted.protocols.basic import LineReceiver -import json -import syncplay -from functools import wraps -import time - -class JSONCommandProtocol(LineReceiver): - def handleMessages(self, messages): - for message in messages.iteritems(): - command = message[0] - if command == "Hello": - self.handleHello(message[1]) - elif command == "Set": - self.handleSet(message[1]) - elif command == "List": - self.handleList(message[1]) - elif command == "State": - self.handleState(message[1]) - elif command == "Error": - self.handleError(message[1]) - else: - self.dropWithError("Unknown Command\n" + message[1]) #TODO: log, not drop - - def printReceived(self, line): #TODO: remove - #print ">>i", line - pass - - def printSent(self, line): - #print "o<<", line - pass - - def lineReceived(self, line): - line = line.strip() - if not line: - return - self.printReceived(line) - try: - messages = json.loads(line) - except: - self.dropWithError("Not a json encoded string\n" + line) - return - self.handleMessages(messages) - - def sendMessage(self, dict_): - line = json.dumps(dict_) - self.printSent(line) - self.sendLine(line) - - def drop(self): - self.transport.loseConnection() - - def dropWithError(self, error): - raise NotImplementedError() - -class SyncClientProtocol(JSONCommandProtocol): - def __init__(self, client): - self._client = client - self.clientIgnoringOnTheFly = 0 - self.serverIgnoringOnTheFly = 0 - self.logged = False - - def connectionMade(self): - self._client.initProtocol(self) - self.sendHello() - - def connectionLost(self, reason): - self._client.destroyProtocol() - - def dropWithError(self, error): - self._client.ui.showErrorMessage(error) - self._client.protocolFactory.stopRetrying() - self.drop() - - def _extractHelloArguments(self, hello): - 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 - return username, roomName, version - - def handleHello(self, hello): - username, roomName, version = self._extractHelloArguments(hello) - if(not username or not roomName or not version): - self.dropWithError("Not enough Hello arguments\n" + hello) - elif(version.split(".")[0:2] != syncplay.version.split(".")[0:2]): - self.dropWithError("Mismatch between versions of client and server\n" + hello) - else: - self._client.setUsername(username) - self._client.setRoom(roomName) - self.logged = True - self._client.ui.showMessage("Successfully connected to server") - self._client.sendFile() - - def sendHello(self): - hello = {} - hello["username"] = self._client.getUsername() - password = self._client.getPassword() - if(password): hello["password"] = password - room = self._client.getRoom() - if(room): hello["room"] = {"name" :room} - hello["version"] = syncplay.version - self.sendMessage({"Hello": hello}) - - def _SetUser(self, users): - for user in users.iteritems(): - username = user[0] - settings = user[1] - room = settings["room"]["name"] if settings.has_key("room") else None - file_ = settings["file"] if settings.has_key("file") else None - if(settings.has_key("event")): - if(settings["event"].has_key("joined")): - self._client.userlist.addUser(username, room, file_) - elif(settings["event"].has_key("left")): - self._client.userlist.removeUser(username) - else: - self._client.userlist.modUser(username, room, file_) - - def handleSet(self, settings): - for set_ in settings.iteritems(): - command = set_[0] - if command == "room": - roomName = set_[1]["name"] if set_[1].has_key("name") else None - self._client.setRoom(roomName) - elif command == "user": - self._SetUser(set_[1]) - - def sendSet(self, setting): - self.sendMessage({"Set": setting}) - - def sendRoomSetting(self, roomName, password=None): - setting = {} - setting["name"] = roomName - if(password): setting["password"] = password - self.sendSet({"room": setting}) - - def sendFileSetting(self, file_): - self.sendSet({"file": file_}) - self.sendList() - - def handleList(self, userList): - self._client.userlist.clearList() - for room in userList.iteritems(): - roomName = room[0] - for user in room[1].iteritems(): - userName = user[0] - file_ = user[1]['file'] if user[1]['file'] <> {} else None - position = user[1]['position'] - self._client.userlist.addUser(userName, roomName, file_, position, noMessage=True) - self._client.userlist.showUserList() - - def sendList(self): - self.sendMessage({"List": None}) - - def _extractStatePlaystateArguments(self, state): - position = state["playstate"]["position"] if state["playstate"].has_key("position") else 0 - paused = state["playstate"]["paused"] if state["playstate"].has_key("paused") else None - doSeek = state["playstate"]["doSeek"] if state["playstate"].has_key("doSeek") else None - setBy = state["playstate"]["setBy"] if state["playstate"].has_key("setBy") else None - return position, paused, doSeek, setBy - - def _handleStatePing(self, state): - yourLatency = state["ping"]["yourLatency"] if state["ping"].has_key("yourLatency") else 0 - senderLatency = state["ping"]["senderLatency"] if state["ping"].has_key("senderLatency") else 0 - if (state["ping"].has_key("latencyCalculation")): - latencyCalculation = state["ping"]["latencyCalculation"] - return yourLatency, senderLatency, latencyCalculation - - def handleState(self, state): - position, paused, doSeek, setBy = None, None, None, None - yourLatency, senderLatency = 0, 0 - if(state.has_key("ignoringOnTheFly")): - ignore = state["ignoringOnTheFly"] - if(ignore.has_key("server")): - self.serverIgnoringOnTheFly = ignore["server"] - self.clientIgnoringOnTheFly = 0 - elif(ignore.has_key("client")): - if(ignore['client']) == self.clientIgnoringOnTheFly: - self.clientIgnoringOnTheFly = 0 - if(state.has_key("playstate")): - position, paused, doSeek, setBy = self._extractStatePlaystateArguments(state) - if(state.has_key("ping")): - yourLatency, senderLatency, latencyCalculation = self._handleStatePing(state) - if(position is not None and paused is not None and not self.clientIgnoringOnTheFly): - latency = yourLatency + senderLatency - self._client.updateGlobalState(position, paused, doSeek, setBy, latency) - position, paused, doSeek, stateChange = self._client.getLocalState() - self.sendState(position, paused, doSeek, latencyCalculation, stateChange) - - def sendState(self, position, paused, doSeek, latencyCalculation, stateChange = False): - state = {} - positionAndPausedIsSet = position is not None and paused is not None - clientIgnoreIsNotSet = self.clientIgnoringOnTheFly == 0 or self.serverIgnoringOnTheFly != 0 - if(clientIgnoreIsNotSet and positionAndPausedIsSet): - state["playstate"] = {} - state["playstate"]["position"] = position - state["playstate"]["paused"] = paused - if(doSeek): state["playstate"]["doSeek"] = doSeek - if(latencyCalculation): - state["ping"] = {"latencyCalculation": latencyCalculation} - if(stateChange): - self.clientIgnoringOnTheFly += 1 - if(self.serverIgnoringOnTheFly or self.clientIgnoringOnTheFly): - state["ignoringOnTheFly"] = {} - if(self.serverIgnoringOnTheFly): - state["ignoringOnTheFly"]["server"] = self.serverIgnoringOnTheFly - self.serverIgnoringOnTheFly = 0 - if(self.clientIgnoringOnTheFly): - state["ignoringOnTheFly"]["client"] = self.clientIgnoringOnTheFly - self.sendMessage({"State": state}) - - def handleError(self, error): - self.dropWithError(error["message"]) #TODO: more processing and fallbacking - - def sendError(self, message): - self.sendMessage({"Error": {"message": message}}) - - -class SyncServerProtocol(JSONCommandProtocol): - def __init__(self, factory): - self._factory = factory - self._logged = False - self.clientIgnoringOnTheFly = 0 - self.serverIgnoringOnTheFly = 0 - - def __hash__(self): - return hash('|'.join(( - self.transport.getPeer().host, - str(id(self)), - ))) - - def requireLogged(f): #@NoSelf - @wraps(f) - def wrapper(self, *args, **kwds): - if(not self._logged): - self.dropWithError("You must be known to server before sending this command") - return f(self, *args, **kwds) - return wrapper - - def dropWithError(self, error): - print "Client drop: %s -- %s" % (self.transport.getPeer().host, error) - self.sendError(error) - self.drop() - - def connectionLost(self, reason): - self._factory.removeWatcher(self) - - def _extractHelloArguments(self, hello): - roomName, roomPassword = None, None - username = hello["username"] if hello.has_key("username") else None - username = username.strip() - serverPassword = hello["password"] if hello.has_key("password") else None - room = hello["room"] if hello.has_key("room") else None - 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 - - def _checkPassword(self, serverPassword): - if(self._factory.password): - if(not serverPassword): - self.dropWithError("Password required") - return False - if(serverPassword != self._factory.password): - self.dropWithError("Wrong password supplied") - return False - return True - - def handleHello(self, hello): - username, serverPassword, roomName, roomPassword, version = self._extractHelloArguments(hello) - if(not username or not roomName or not version): - self.dropWithError("Not enough Hello arguments") - elif(version.split(".")[0:2] != syncplay.version.split(".")[0:2]): - self.dropWithError("Mismatch between versions of client and server") - else: - if(not self._checkPassword(serverPassword)): - return - self._factory.addWatcher(self, username, roomName, roomPassword) - self._logged = True - self.sendHello() - - def sendHello(self): - hello = {} - hello["username"] = self._factory.watcherGetUsername(self) - room = self._factory.watcherGetRoom(self) - if(room): hello["room"] = {"name": room} - hello["version"] = syncplay.version - self.sendMessage({"Hello": hello}) - - @requireLogged - def handleSet(self, settings): - for set_ in settings.iteritems(): - command = set_[0] - if command == "room": - roomName = set_[1]["name"] if set_[1].has_key("name") else None - self._factory.watcherSetRoom(self, roomName) - elif command == "file": - self._factory.watcherSetFile(self, set_[1]) - - def sendSet(self, setting): - self.sendMessage({"Set": setting}) - - def sendRoomSetting(self, roomName): - self.sendSet({"room": {"name": roomName}}) - - def sendUserSetting(self, username, roomName, file_, event): - room = {"name": roomName} - user = {} - user[username] = {} - user[username]["room"] = room - if(file_): - user[username]["file"] = file_ - if(event): - user[username]["event"] = event - self.sendSet({"user": user}) - - def _addUserOnList(self, userlist, roomPositions, watcher): - if (not userlist.has_key(watcher.room)): - userlist[watcher.room] = {} - roomPositions[watcher.room] = watcher.getRoomPosition() - userlist[watcher.room][watcher.name] = { - "file": watcher.file if watcher.file else {}, - "position": roomPositions[watcher.room] if roomPositions[watcher.room] else 0 - } - def sendList(self): - userlist = {} - roomPositions = {} - watchers = self._factory.getAllWatchers(self) - for watcher in watchers.itervalues(): - self._addUserOnList(userlist, roomPositions, watcher) - self.sendMessage({"List": userlist}) - - @requireLogged - def handleList(self, _): - self.sendList() - - def sendState(self, position, paused, doSeek, setBy, senderLatency, watcherLatency, forced = False): - playstate = { - "position": position, - "paused": paused, - "doSeek": doSeek, - "setBy": setBy - } - ping = { - "yourLatency": watcherLatency, - "senderLatency": senderLatency, - "latencyCalculation": time.time() - } - state = { - "ping": ping, - "playstate": playstate, - } - if(forced): - self.serverIgnoringOnTheFly += 1 - if(self.serverIgnoringOnTheFly or self.clientIgnoringOnTheFly): - state["ignoringOnTheFly"] = {} - if(self.serverIgnoringOnTheFly): - state["ignoringOnTheFly"]["server"] = self.serverIgnoringOnTheFly - if(self.clientIgnoringOnTheFly): - state["ignoringOnTheFly"]["client"] = self.clientIgnoringOnTheFly - self.clientIgnoringOnTheFly = 0 - if(self.serverIgnoringOnTheFly == 0 or forced): - self.sendMessage({"State": state}) - - - def _extractStatePlaystateArguments(self, state): - position = state["playstate"]["position"] if state["playstate"].has_key("position") else 0 - paused = state["playstate"]["paused"] if state["playstate"].has_key("paused") else None - doSeek = state["playstate"]["doSeek"] if state["playstate"].has_key("doSeek") else None - return position, paused, doSeek - - @requireLogged - def handleState(self, state): - position, paused, doSeek, latencyCalculation = None, None, None, None - if(state.has_key("ignoringOnTheFly")): - ignore = state["ignoringOnTheFly"] - if(ignore.has_key("server")): - if(self.serverIgnoringOnTheFly == ignore["server"]): - self.serverIgnoringOnTheFly = 0 - if(ignore.has_key("client")): - self.clientIgnoringOnTheFly = ignore["client"] - if(state.has_key("playstate")): - position, paused, doSeek = self._extractStatePlaystateArguments(state) - if(state.has_key("ping")): - latencyCalculation = state["ping"]["latencyCalculation"] if state["ping"].has_key("latencyCalculation") else None - if(self.serverIgnoringOnTheFly == 0): - self._factory.updateWatcherState(self, position, paused, doSeek, latencyCalculation) - - def handleError(self, error): - self.dropWithError(error["message"]) #TODO: more processing and fallbacking - - def sendError(self, message): - self.sendMessage({"Error": {"message": message}}) - +#coding:utf8 +from twisted.protocols.basic import LineReceiver +import json +import syncplay +from functools import wraps +import time +from syncplay.messages import getMessage + +class JSONCommandProtocol(LineReceiver): + def handleMessages(self, messages): + for message in messages.iteritems(): + command = message[0] + if command == "Hello": + self.handleHello(message[1]) + elif command == "Set": + self.handleSet(message[1]) + elif command == "List": + self.handleList(message[1]) + elif command == "State": + self.handleState(message[1]) + elif command == "Error": + self.handleError(message[1]) + else: + self.dropWithError("Unknown Command\n" + message[1]) #TODO: log, not drop + + def printReceived(self, line): #TODO: remove + #print ">>i", line + pass + + def printSent(self, line): + #print "o<<", line + pass + + def lineReceived(self, line): + line = line.strip() + if not line: + return + self.printReceived(line) + try: + messages = json.loads(line) + except: + self.dropWithError(getMessage("en", "not-json-error") + line) + return + self.handleMessages(messages) + + def sendMessage(self, dict_): + line = json.dumps(dict_) + self.printSent(line) + self.sendLine(line) + + def drop(self): + self.transport.loseConnection() + + def dropWithError(self, error): + raise NotImplementedError() + +class SyncClientProtocol(JSONCommandProtocol): + def __init__(self, client): + self._client = client + self.clientIgnoringOnTheFly = 0 + self.serverIgnoringOnTheFly = 0 + self.logged = False + + def connectionMade(self): + self._client.initProtocol(self) + self.sendHello() + + def connectionLost(self, reason): + self._client.destroyProtocol() + + def dropWithError(self, error): + self._client.ui.showErrorMessage(error) + self._client.protocolFactory.stopRetrying() + self.drop() + + def _extractHelloArguments(self, hello): + 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 + return username, roomName, version + + def handleHello(self, hello): + username, roomName, version = self._extractHelloArguments(hello) + if(not username or not roomName or not version): + self.dropWithError(getMessage("en", "hello-arguments-error") + hello) + elif(version.split(".")[0:2] != syncplay.version.split(".")[0:2]): + self.dropWithError(getMessage("en", "version-mismatch-error") + hello) + else: + self._client.setUsername(username) + self._client.setRoom(roomName) + self.logged = True + self._client.ui.showMessage(getMessage("en", "connected-successful-notification")) + self._client.sendFile() + + def sendHello(self): + hello = {} + hello["username"] = self._client.getUsername() + password = self._client.getPassword() + if(password): hello["password"] = password + room = self._client.getRoom() + if(room): hello["room"] = {"name" :room} + hello["version"] = syncplay.version + self.sendMessage({"Hello": hello}) + + def _SetUser(self, users): + for user in users.iteritems(): + username = user[0] + settings = user[1] + room = settings["room"]["name"] if settings.has_key("room") else None + file_ = settings["file"] if settings.has_key("file") else None + if(settings.has_key("event")): + if(settings["event"].has_key("joined")): + self._client.userlist.addUser(username, room, file_) + elif(settings["event"].has_key("left")): + self._client.userlist.removeUser(username) + else: + self._client.userlist.modUser(username, room, file_) + + def handleSet(self, settings): + for set_ in settings.iteritems(): + command = set_[0] + if command == "room": + roomName = set_[1]["name"] if set_[1].has_key("name") else None + self._client.setRoom(roomName) + elif command == "user": + self._SetUser(set_[1]) + + def sendSet(self, setting): + self.sendMessage({"Set": setting}) + + def sendRoomSetting(self, roomName, password=None): + setting = {} + setting["name"] = roomName + if(password): setting["password"] = password + self.sendSet({"room": setting}) + + def sendFileSetting(self, file_): + self.sendSet({"file": file_}) + self.sendList() + + def handleList(self, userList): + self._client.userlist.clearList() + for room in userList.iteritems(): + roomName = room[0] + for user in room[1].iteritems(): + userName = user[0] + file_ = user[1]['file'] if user[1]['file'] <> {} else None + position = user[1]['position'] + self._client.userlist.addUser(userName, roomName, file_, position, noMessage=True) + self._client.userlist.showUserList() + + def sendList(self): + self.sendMessage({"List": None}) + + def _extractStatePlaystateArguments(self, state): + position = state["playstate"]["position"] if state["playstate"].has_key("position") else 0 + paused = state["playstate"]["paused"] if state["playstate"].has_key("paused") else None + doSeek = state["playstate"]["doSeek"] if state["playstate"].has_key("doSeek") else None + setBy = state["playstate"]["setBy"] if state["playstate"].has_key("setBy") else None + return position, paused, doSeek, setBy + + def _handleStatePing(self, state): + yourLatency = state["ping"]["yourLatency"] if state["ping"].has_key("yourLatency") else 0 + senderLatency = state["ping"]["senderLatency"] if state["ping"].has_key("senderLatency") else 0 + if (state["ping"].has_key("latencyCalculation")): + latencyCalculation = state["ping"]["latencyCalculation"] + return yourLatency, senderLatency, latencyCalculation + + def handleState(self, state): + position, paused, doSeek, setBy = None, None, None, None + yourLatency, senderLatency = 0, 0 + if(state.has_key("ignoringOnTheFly")): + ignore = state["ignoringOnTheFly"] + if(ignore.has_key("server")): + self.serverIgnoringOnTheFly = ignore["server"] + self.clientIgnoringOnTheFly = 0 + elif(ignore.has_key("client")): + if(ignore['client']) == self.clientIgnoringOnTheFly: + self.clientIgnoringOnTheFly = 0 + if(state.has_key("playstate")): + position, paused, doSeek, setBy = self._extractStatePlaystateArguments(state) + if(state.has_key("ping")): + yourLatency, senderLatency, latencyCalculation = self._handleStatePing(state) + if(position is not None and paused is not None and not self.clientIgnoringOnTheFly): + latency = yourLatency + senderLatency + self._client.updateGlobalState(position, paused, doSeek, setBy, latency) + position, paused, doSeek, stateChange = self._client.getLocalState() + self.sendState(position, paused, doSeek, latencyCalculation, stateChange) + + def sendState(self, position, paused, doSeek, latencyCalculation, stateChange = False): + state = {} + positionAndPausedIsSet = position is not None and paused is not None + clientIgnoreIsNotSet = self.clientIgnoringOnTheFly == 0 or self.serverIgnoringOnTheFly != 0 + if(clientIgnoreIsNotSet and positionAndPausedIsSet): + state["playstate"] = {} + state["playstate"]["position"] = position + state["playstate"]["paused"] = paused + if(doSeek): state["playstate"]["doSeek"] = doSeek + if(latencyCalculation): + state["ping"] = {"latencyCalculation": latencyCalculation} + if(stateChange): + self.clientIgnoringOnTheFly += 1 + if(self.serverIgnoringOnTheFly or self.clientIgnoringOnTheFly): + state["ignoringOnTheFly"] = {} + if(self.serverIgnoringOnTheFly): + state["ignoringOnTheFly"]["server"] = self.serverIgnoringOnTheFly + self.serverIgnoringOnTheFly = 0 + if(self.clientIgnoringOnTheFly): + state["ignoringOnTheFly"]["client"] = self.clientIgnoringOnTheFly + self.sendMessage({"State": state}) + + def handleError(self, error): + self.dropWithError(error["message"]) #TODO: more processing and fallbacking + + def sendError(self, message): + self.sendMessage({"Error": {"message": message}}) + + +class SyncServerProtocol(JSONCommandProtocol): + def __init__(self, factory): + self._factory = factory + self._logged = False + self.clientIgnoringOnTheFly = 0 + self.serverIgnoringOnTheFly = 0 + + def __hash__(self): + return hash('|'.join(( + self.transport.getPeer().host, + str(id(self)), + ))) + + def requireLogged(f): #@NoSelf + @wraps(f) + def wrapper(self, *args, **kwds): + if(not self._logged): + self.dropWithError(getMessage("en", "not-known-server-error")) + return f(self, *args, **kwds) + return wrapper + + def dropWithError(self, error): + print getMessage("en", "client-drop-server-error") % (self.transport.getPeer().host, error) + self.sendError(error) + self.drop() + + def connectionLost(self, reason): + self._factory.removeWatcher(self) + + def _extractHelloArguments(self, hello): + roomName, roomPassword = None, None + username = hello["username"] if hello.has_key("username") else None + username = username.strip() + serverPassword = hello["password"] if hello.has_key("password") else None + room = hello["room"] if hello.has_key("room") else None + 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 + + def _checkPassword(self, serverPassword): + if(self._factory.password): + if(not serverPassword): + self.dropWithError(getMessage("en", "password-required-server-error")) + return False + if(serverPassword != self._factory.password): + self.dropWithError(getMessage("en", "wrong-password-server-error")) + return False + return True + + def handleHello(self, hello): + username, serverPassword, roomName, roomPassword, version = self._extractHelloArguments(hello) + if(not username or not roomName or not version): + self.dropWithError(getMessage("en", "hello-server-error")) + elif(version.split(".")[0:2] != syncplay.version.split(".")[0:2]): + self.dropWithError(getMessage("en", "version-mismatch-server-error")) + else: + if(not self._checkPassword(serverPassword)): + return + self._factory.addWatcher(self, username, roomName, roomPassword) + self._logged = True + self.sendHello() + + def sendHello(self): + hello = {} + hello["username"] = self._factory.watcherGetUsername(self) + room = self._factory.watcherGetRoom(self) + if(room): hello["room"] = {"name": room} + hello["version"] = syncplay.version + self.sendMessage({"Hello": hello}) + + @requireLogged + def handleSet(self, settings): + for set_ in settings.iteritems(): + command = set_[0] + if command == "room": + roomName = set_[1]["name"] if set_[1].has_key("name") else None + self._factory.watcherSetRoom(self, roomName) + elif command == "file": + self._factory.watcherSetFile(self, set_[1]) + + def sendSet(self, setting): + self.sendMessage({"Set": setting}) + + def sendRoomSetting(self, roomName): + self.sendSet({"room": {"name": roomName}}) + + def sendUserSetting(self, username, roomName, file_, event): + room = {"name": roomName} + user = {} + user[username] = {} + user[username]["room"] = room + if(file_): + user[username]["file"] = file_ + if(event): + user[username]["event"] = event + self.sendSet({"user": user}) + + def _addUserOnList(self, userlist, roomPositions, watcher): + if (not userlist.has_key(watcher.room)): + userlist[watcher.room] = {} + roomPositions[watcher.room] = watcher.getRoomPosition() + userlist[watcher.room][watcher.name] = { + "file": watcher.file if watcher.file else {}, + "position": roomPositions[watcher.room] if roomPositions[watcher.room] else 0 + } + def sendList(self): + userlist = {} + roomPositions = {} + watchers = self._factory.getAllWatchers(self) + for watcher in watchers.itervalues(): + self._addUserOnList(userlist, roomPositions, watcher) + self.sendMessage({"List": userlist}) + + @requireLogged + def handleList(self, _): + self.sendList() + + def sendState(self, position, paused, doSeek, setBy, senderLatency, watcherLatency, forced = False): + playstate = { + "position": position, + "paused": paused, + "doSeek": doSeek, + "setBy": setBy + } + ping = { + "yourLatency": watcherLatency, + "senderLatency": senderLatency, + "latencyCalculation": time.time() + } + state = { + "ping": ping, + "playstate": playstate, + } + if(forced): + self.serverIgnoringOnTheFly += 1 + if(self.serverIgnoringOnTheFly or self.clientIgnoringOnTheFly): + state["ignoringOnTheFly"] = {} + if(self.serverIgnoringOnTheFly): + state["ignoringOnTheFly"]["server"] = self.serverIgnoringOnTheFly + if(self.clientIgnoringOnTheFly): + state["ignoringOnTheFly"]["client"] = self.clientIgnoringOnTheFly + self.clientIgnoringOnTheFly = 0 + if(self.serverIgnoringOnTheFly == 0 or forced): + self.sendMessage({"State": state}) + + + def _extractStatePlaystateArguments(self, state): + position = state["playstate"]["position"] if state["playstate"].has_key("position") else 0 + paused = state["playstate"]["paused"] if state["playstate"].has_key("paused") else None + doSeek = state["playstate"]["doSeek"] if state["playstate"].has_key("doSeek") else None + return position, paused, doSeek + + @requireLogged + def handleState(self, state): + position, paused, doSeek, latencyCalculation = None, None, None, None + if(state.has_key("ignoringOnTheFly")): + ignore = state["ignoringOnTheFly"] + if(ignore.has_key("server")): + if(self.serverIgnoringOnTheFly == ignore["server"]): + self.serverIgnoringOnTheFly = 0 + if(ignore.has_key("client")): + self.clientIgnoringOnTheFly = ignore["client"] + if(state.has_key("playstate")): + position, paused, doSeek = self._extractStatePlaystateArguments(state) + if(state.has_key("ping")): + latencyCalculation = state["ping"]["latencyCalculation"] if state["ping"].has_key("latencyCalculation") else None + if(self.serverIgnoringOnTheFly == 0): + self._factory.updateWatcherState(self, position, paused, doSeek, latencyCalculation) + + def handleError(self, error): + self.dropWithError(error["message"]) #TODO: more processing and fallbacking + + def sendError(self, message): + self.sendMessage({"Error": {"message": message}}) + diff --git a/syncplay/server.py b/syncplay/server.py index 522ff7b..e2bbf8d 100644 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -1,256 +1,257 @@ -#coding:utf8 -#TODO: #12, #13, #8; -import hashlib -from twisted.internet import task, reactor -from twisted.internet.protocol import Factory -import syncplay -from syncplay.protocols import SyncServerProtocol -import time -from syncplay import constants -import threading - -class SyncFactory(Factory): - def __init__(self, password = ''): - print "Welcome to Syncplay server, ver. {0}".format(syncplay.version) - if(password): - password = hashlib.md5(password).hexdigest() - self.password = password - self._rooms = {} - self._roomStates = {} - self._roomUpdate = threading.RLock() - - def buildProtocol(self, addr): - return SyncServerProtocol(self) - - def _createRoomIfDoesntExist(self, roomName): - if (not self._rooms.has_key(roomName)): - with self._roomUpdate: - self._rooms[roomName] = {} - self._roomStates[roomName] = { - "position": 0.0, - "paused": True, - "setBy": None, - "lastUpdate": time.time() - } - - def addWatcher(self, watcherProtocol, username, roomName, roomPassword): - allnames = [] - for room in self._rooms.itervalues(): - for watcher in room.itervalues(): - allnames.append(watcher.name.lower()) - while username.lower() in allnames: - username += '_' - self._createRoomIfDoesntExist(roomName) - watcher = Watcher(self, watcherProtocol, username, roomName) - with self._roomUpdate: - self._rooms[roomName][watcherProtocol] = watcher - print "{0}({2}) connected to room '{1}'".format(username, roomName, watcherProtocol.transport.getPeer().host) - reactor.callLater(0.1, watcher.scheduleSendState) - l = lambda w: w.sendUserSetting(username, roomName, None, {"joined": True}) - self.broadcast(watcherProtocol, l) - - def getWatcher(self, watcherProtocol): - for room in self._rooms.itervalues(): - if(room.has_key(watcherProtocol)): - return room[watcherProtocol] - - def getAllWatchers(self, watcherProtocol): #TODO: Optimize me - watchers = {} - for room in self._rooms.itervalues(): - for watcher in room.itervalues(): - watchers[watcher.watcherProtocol] = watcher - return watchers - - def _removeWatcherFromTheRoom(self, watcherProtocol): - for room in self._rooms.itervalues(): - with self._roomUpdate: - watcher = room.pop(watcherProtocol, None) - if(watcher): - return watcher - - def _deleteRoomIfEmpty(self, room): - if (self._rooms[room] == {}): - with self._roomUpdate: - self._rooms.pop(room) - self._roomStates.pop(room) - - def getRoomPausedAndPosition(self, room): - position = self._roomStates[room]["position"] - paused = self._roomStates[room]["paused"] - if (not paused): - timePassedSinceSet = time.time() - self._roomStates[room]["lastUpdate"] - position += timePassedSinceSet - return paused, position - - def sendState(self, watcherProtocol, doSeek = False, senderLatency = 0, forcedUpdate = False): - watcher = self.getWatcher(watcherProtocol) - if(not watcher): - return - room = watcher.room - paused, position = self.getRoomPausedAndPosition(room) - setBy = self._roomStates[room]["setBy"] - watcher.paused = paused - watcher.position = position - watcherProtocol.sendState(position, paused, doSeek, setBy, senderLatency, watcher.latency, forcedUpdate) - if(time.time() - watcher.lastUpdate > constants.PROTOCOL_TIMEOUT): - watcherProtocol.drop() - self.removeWatcher(watcherProtocol) - - def __updateWatcherPing(self, latencyCalculation, watcher): - if (latencyCalculation): - ping = (time.time() - latencyCalculation) / 2 - if (watcher.latency): - watcher.latency = watcher.latency * (constants.PING_MOVING_AVERAGE_WEIGHT) + ping * (1-constants.PING_MOVING_AVERAGE_WEIGHT) #Exponential moving average - else: - watcher.latency = ping - - def __shouldServerForceUpdateOnRoom(self, pauseChanged, doSeek): - return doSeek or pauseChanged - - def __updatePausedState(self, paused, watcher): - watcher.paused = paused - if(self._roomStates[watcher.room]["paused"] <> paused): - self._roomStates[watcher.room]["setBy"] = watcher.name - self._roomStates[watcher.room]["paused"] = paused - self._roomStates[watcher.room]["lastUpdate"] = time.time() - return True - - def __updatePositionState(self, position, doSeek, watcher): - watcher.position = position - if (doSeek): - self._roomStates[watcher.room]["position"] = position - self._roomStates[watcher.room]["setBy"] = watcher.name - self._roomStates[watcher.room]["lastUpdate"] = time.time() - else: - setter = min(self._rooms[watcher.room].values()) - self._roomStates[watcher.room]["position"] = setter.position - self._roomStates[watcher.room]["setBy"] = setter.name - self._roomStates[watcher.room]["lastUpdate"] = setter.lastUpdate - - def updateWatcherState(self, watcherProtocol, position, paused, doSeek, latencyCalculation): - watcher = self.getWatcher(watcherProtocol) - self.__updateWatcherPing(latencyCalculation, watcher) - watcher.lastUpdate = time.time() - if(watcher.file): - if(position is not None): - self.__updatePositionState(position, doSeek, watcher) - pauseChanged = False - if(paused is not None): - pauseChanged = self.__updatePausedState(paused, watcher) - forceUpdate = self.__shouldServerForceUpdateOnRoom(pauseChanged, doSeek) - if(forceUpdate): - l = lambda w: self.sendState(w, doSeek, watcher.latency, forceUpdate) - self.broadcastRoom(watcher.watcherProtocol, l) - - def removeWatcher(self, watcherProtocol): - watcher = self.getWatcher(watcherProtocol) - if(not watcher): - return - l = lambda w: w.sendUserSetting(watcher.name, watcher.room, None, {"left": True}) - self.broadcast(watcherProtocol, l) - self._removeWatcherFromTheRoom(watcherProtocol) - watcher.deactivate() - self._deleteRoomIfEmpty(watcher.room) - print "{0} left server".format(watcher.name) - - def watcherGetUsername(self, watcherProtocol): - return self.getWatcher(watcherProtocol).name - - def watcherGetRoom(self, watcherProtocol): - return self.getWatcher(watcherProtocol).room - - def watcherSetRoom(self, watcherProtocol, room): - watcher = self._removeWatcherFromTheRoom(watcherProtocol) - if(not watcher): - return - watcher.resetStateTimer() - oldRoom = watcher.room - self._createRoomIfDoesntExist(room) - with self._roomUpdate: - self._rooms[room][watcherProtocol] = watcher - self._roomStates[room]["position"] = watcher.position - self._roomStates[room]["setBy"] = watcher.name - self._roomStates[room]["lastUpdate"] = time.time() - self._deleteRoomIfEmpty(oldRoom) - watcher.room = room - l = lambda w: w.sendUserSetting(watcher.name, watcher.room, watcher.file, None) - self.broadcast(watcherProtocol, l) - - def watcherSetFile(self, watcherProtocol, file_): - watcher = self.getWatcher(watcherProtocol) - watcher.file = file_ - l = lambda w: w.sendUserSetting(watcher.name, watcher.room, watcher.file, None) - self.broadcast(watcherProtocol, l) - - def broadcastRoom(self, sender, what): - room = self._rooms[self.watcherGetRoom(sender)] - if(room): - with self._roomUpdate: - for receiver in room: - what(receiver) - - def broadcast(self, sender, what): - with self._roomUpdate: - for room in self._rooms.itervalues(): - for receiver in room: - what(receiver) - -class SyncIsolatedFactory(SyncFactory): - def broadcast(self, sender, what): - self.broadcastRoom(sender, what) - - def getAllWatchers(self, watcherProtocol): - room = self.getWatcher(watcherProtocol).room - if(self._rooms.has_key(room)): - return self._rooms[room] - else: - return {} - - def watcherSetRoom(self, watcherProtocol, room): - watcher = self.getWatcher(watcherProtocol) - oldRoom = watcher.room - l = lambda w: w.sendUserSetting(watcher.name, oldRoom, None, {"left": True}) - self.broadcast(watcherProtocol, l) - SyncFactory.watcherSetRoom(self, watcherProtocol, room) - - -class Watcher(object): - def __init__(self, factory, watcherProtocol, name, room): - self.factory = factory - self.watcherProtocol = watcherProtocol - self.name = name - self.room = room - self.file = None - self._sendStateTimer = None - self.position = None - self.latency = 0 - self.lastUpdate = time.time() - - def __lt__(self, b): - if(self.position is None): - return False - elif(b.position is None): - return True - else: - return self.position < b.position - - def getRoomPosition(self): - _, position = self.factory.getRoomPausedAndPosition(self.room) - return position - - def scheduleSendState(self): - self._sendStateTimer = task.LoopingCall(self.sendState) - self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL, True) - - def sendState(self): - self.factory.sendState(self.watcherProtocol) - - def resetStateTimer(self): - if(self._sendStateTimer): - self._sendStateTimer.stop() - self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL) - - def deactivate(self): - if(self._sendStateTimer): - self._sendStateTimer.stop() - +#coding:utf8 +#TODO: #12, #13, #8; +import hashlib +from twisted.internet import task, reactor +from twisted.internet.protocol import Factory +import syncplay +from syncplay.protocols import SyncServerProtocol +import time +from syncplay import constants +import threading +from syncplay.messages import getMessage + +class SyncFactory(Factory): + def __init__(self, password = ''): + print getMessage("en", "welcome-server-notification").format(syncplay.version) + if(password): + password = hashlib.md5(password).hexdigest() + self.password = password + self._rooms = {} + self._roomStates = {} + self._roomUpdate = threading.RLock() + + def buildProtocol(self, addr): + return SyncServerProtocol(self) + + def _createRoomIfDoesntExist(self, roomName): + if (not self._rooms.has_key(roomName)): + with self._roomUpdate: + self._rooms[roomName] = {} + self._roomStates[roomName] = { + "position": 0.0, + "paused": True, + "setBy": None, + "lastUpdate": time.time() + } + + def addWatcher(self, watcherProtocol, username, roomName, roomPassword): + allnames = [] + for room in self._rooms.itervalues(): + for watcher in room.itervalues(): + allnames.append(watcher.name.lower()) + while username.lower() in allnames: + username += '_' + self._createRoomIfDoesntExist(roomName) + watcher = Watcher(self, watcherProtocol, username, roomName) + with self._roomUpdate: + self._rooms[roomName][watcherProtocol] = watcher + print getMessage("en", "client-connected-room-server-notification").format(username, roomName, watcherProtocol.transport.getPeer().host) + reactor.callLater(0.1, watcher.scheduleSendState) + l = lambda w: w.sendUserSetting(username, roomName, None, {"joined": True}) + self.broadcast(watcherProtocol, l) + + def getWatcher(self, watcherProtocol): + for room in self._rooms.itervalues(): + if(room.has_key(watcherProtocol)): + return room[watcherProtocol] + + def getAllWatchers(self, watcherProtocol): #TODO: Optimize me + watchers = {} + for room in self._rooms.itervalues(): + for watcher in room.itervalues(): + watchers[watcher.watcherProtocol] = watcher + return watchers + + def _removeWatcherFromTheRoom(self, watcherProtocol): + for room in self._rooms.itervalues(): + with self._roomUpdate: + watcher = room.pop(watcherProtocol, None) + if(watcher): + return watcher + + def _deleteRoomIfEmpty(self, room): + if (self._rooms[room] == {}): + with self._roomUpdate: + self._rooms.pop(room) + self._roomStates.pop(room) + + def getRoomPausedAndPosition(self, room): + position = self._roomStates[room]["position"] + paused = self._roomStates[room]["paused"] + if (not paused): + timePassedSinceSet = time.time() - self._roomStates[room]["lastUpdate"] + position += timePassedSinceSet + return paused, position + + def sendState(self, watcherProtocol, doSeek = False, senderLatency = 0, forcedUpdate = False): + watcher = self.getWatcher(watcherProtocol) + if(not watcher): + return + room = watcher.room + paused, position = self.getRoomPausedAndPosition(room) + setBy = self._roomStates[room]["setBy"] + watcher.paused = paused + watcher.position = position + watcherProtocol.sendState(position, paused, doSeek, setBy, senderLatency, watcher.latency, forcedUpdate) + if(time.time() - watcher.lastUpdate > constants.PROTOCOL_TIMEOUT): + watcherProtocol.drop() + self.removeWatcher(watcherProtocol) + + def __updateWatcherPing(self, latencyCalculation, watcher): + if (latencyCalculation): + ping = (time.time() - latencyCalculation) / 2 + if (watcher.latency): + watcher.latency = watcher.latency * (constants.PING_MOVING_AVERAGE_WEIGHT) + ping * (1-constants.PING_MOVING_AVERAGE_WEIGHT) #Exponential moving average + else: + watcher.latency = ping + + def __shouldServerForceUpdateOnRoom(self, pauseChanged, doSeek): + return doSeek or pauseChanged + + def __updatePausedState(self, paused, watcher): + watcher.paused = paused + if(self._roomStates[watcher.room]["paused"] <> paused): + self._roomStates[watcher.room]["setBy"] = watcher.name + self._roomStates[watcher.room]["paused"] = paused + self._roomStates[watcher.room]["lastUpdate"] = time.time() + return True + + def __updatePositionState(self, position, doSeek, watcher): + watcher.position = position + if (doSeek): + self._roomStates[watcher.room]["position"] = position + self._roomStates[watcher.room]["setBy"] = watcher.name + self._roomStates[watcher.room]["lastUpdate"] = time.time() + else: + setter = min(self._rooms[watcher.room].values()) + self._roomStates[watcher.room]["position"] = setter.position + self._roomStates[watcher.room]["setBy"] = setter.name + self._roomStates[watcher.room]["lastUpdate"] = setter.lastUpdate + + def updateWatcherState(self, watcherProtocol, position, paused, doSeek, latencyCalculation): + watcher = self.getWatcher(watcherProtocol) + self.__updateWatcherPing(latencyCalculation, watcher) + watcher.lastUpdate = time.time() + if(watcher.file): + if(position is not None): + self.__updatePositionState(position, doSeek, watcher) + pauseChanged = False + if(paused is not None): + pauseChanged = self.__updatePausedState(paused, watcher) + forceUpdate = self.__shouldServerForceUpdateOnRoom(pauseChanged, doSeek) + if(forceUpdate): + l = lambda w: self.sendState(w, doSeek, watcher.latency, forceUpdate) + self.broadcastRoom(watcher.watcherProtocol, l) + + def removeWatcher(self, watcherProtocol): + watcher = self.getWatcher(watcherProtocol) + if(not watcher): + return + l = lambda w: w.sendUserSetting(watcher.name, watcher.room, None, {"left": True}) + self.broadcast(watcherProtocol, l) + self._removeWatcherFromTheRoom(watcherProtocol) + watcher.deactivate() + self._deleteRoomIfEmpty(watcher.room) + print getMessage("en", "client-left-server-notification").format(watcher.name) + + def watcherGetUsername(self, watcherProtocol): + return self.getWatcher(watcherProtocol).name + + def watcherGetRoom(self, watcherProtocol): + return self.getWatcher(watcherProtocol).room + + def watcherSetRoom(self, watcherProtocol, room): + watcher = self._removeWatcherFromTheRoom(watcherProtocol) + if(not watcher): + return + watcher.resetStateTimer() + oldRoom = watcher.room + self._createRoomIfDoesntExist(room) + with self._roomUpdate: + self._rooms[room][watcherProtocol] = watcher + self._roomStates[room]["position"] = watcher.position + self._roomStates[room]["setBy"] = watcher.name + self._roomStates[room]["lastUpdate"] = time.time() + self._deleteRoomIfEmpty(oldRoom) + watcher.room = room + l = lambda w: w.sendUserSetting(watcher.name, watcher.room, watcher.file, None) + self.broadcast(watcherProtocol, l) + + def watcherSetFile(self, watcherProtocol, file_): + watcher = self.getWatcher(watcherProtocol) + watcher.file = file_ + l = lambda w: w.sendUserSetting(watcher.name, watcher.room, watcher.file, None) + self.broadcast(watcherProtocol, l) + + def broadcastRoom(self, sender, what): + room = self._rooms[self.watcherGetRoom(sender)] + if(room): + with self._roomUpdate: + for receiver in room: + what(receiver) + + def broadcast(self, sender, what): + with self._roomUpdate: + for room in self._rooms.itervalues(): + for receiver in room: + what(receiver) + +class SyncIsolatedFactory(SyncFactory): + def broadcast(self, sender, what): + self.broadcastRoom(sender, what) + + def getAllWatchers(self, watcherProtocol): + room = self.getWatcher(watcherProtocol).room + if(self._rooms.has_key(room)): + return self._rooms[room] + else: + return {} + + def watcherSetRoom(self, watcherProtocol, room): + watcher = self.getWatcher(watcherProtocol) + oldRoom = watcher.room + l = lambda w: w.sendUserSetting(watcher.name, oldRoom, None, {"left": True}) + self.broadcast(watcherProtocol, l) + SyncFactory.watcherSetRoom(self, watcherProtocol, room) + + +class Watcher(object): + def __init__(self, factory, watcherProtocol, name, room): + self.factory = factory + self.watcherProtocol = watcherProtocol + self.name = name + self.room = room + self.file = None + self._sendStateTimer = None + self.position = None + self.latency = 0 + self.lastUpdate = time.time() + + def __lt__(self, b): + if(self.position is None): + return False + elif(b.position is None): + return True + else: + return self.position < b.position + + def getRoomPosition(self): + _, position = self.factory.getRoomPausedAndPosition(self.room) + return position + + def scheduleSendState(self): + self._sendStateTimer = task.LoopingCall(self.sendState) + self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL, True) + + def sendState(self): + self.factory.sendState(self.watcherProtocol) + + def resetStateTimer(self): + if(self._sendStateTimer): + self._sendStateTimer.stop() + self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL) + + def deactivate(self): + if(self._sendStateTimer): + self._sendStateTimer.stop() + diff --git a/syncplay/ui/ConfigurationGetter.py b/syncplay/ui/ConfigurationGetter.py index 3599f52..ef2e8f5 100644 --- a/syncplay/ui/ConfigurationGetter.py +++ b/syncplay/ui/ConfigurationGetter.py @@ -1,235 +1,236 @@ -from ConfigParser import SafeConfigParser -import argparse -import os -import sys -from syncplay import constants -try: - from syncplay.ui.GuiConfiguration import GuiConfiguration -except ImportError: - GuiConfiguration = None - -class InvalidConfigValue(Exception): - def __init__(self, message): - Exception.__init__(self, message) - -class ConfigurationGetter(object): - def __init__(self): - self._config = { - "host": None, - "port": constants.DEFAULT_PORT, - "name": None, - "debug": False, - "forceGuiPrompt": False, - "noGui": False, - "noStore": False, - "room": "", - "password": None, - "playerPath": None, - "file": None, - "playerArgs": [], - "playerType": None, - } - - # - #Custom validation in self._validateArguments - # - self._required = [ - "host", - "port", - "name", - "playerPath", - "playerType", - ] - - self._iniStructure = { - "server_data": ["host", "port", "password"], - "client_settings": ["name", "room", "playerPath"] - } - - # - #Watch out for the method self._overrideConfigWithArgs when you're adding custom multi-word command line arguments - # - self._argparser = argparse.ArgumentParser(description='Solution to synchronize playback of multiple MPlayer and MPC-HC instances over the network.', - epilog='If no options supplied values from configuration file will be used') - self._argparser.add_argument('--no-gui', action='store_true', help='show no GUI') - self._argparser.add_argument('-a', '--host', metavar='hostname', type=str, help='server\'s address') - self._argparser.add_argument('-n', '--name', metavar='username', type=str, help='desired username') - self._argparser.add_argument('-d', '--debug', action='store_true', help='debug mode') - self._argparser.add_argument('-g', '--force-gui-prompt', action='store_true', help='make configuration prompt appear') - self._argparser.add_argument('--no-store', action='store_true', help='don\'t store values in .syncplay') - self._argparser.add_argument('-r', '--room', metavar='room', type=str, nargs='?', help='default room') - self._argparser.add_argument('-p', '--password', metavar='password', type=str, nargs='?', help='server password') - self._argparser.add_argument('--player-path', metavar='path', type=str, help='path to your player executable') - self._argparser.add_argument('file', metavar='file', type=str, nargs='?', help='file to play') - self._argparser.add_argument('_args', metavar='options', type=str, nargs='*', help='player options, if you need to pass options starting with - prepend them with single \'--\' argument') - - def _validateArguments(self): - for key in self._required: - if(key == "playerPath"): - if(self._isPlayerMPCAndValid(self._config["playerPath"])): - self._config["playerType"] = "mpc" - self.__addSpecialMPCFlags() - elif(self._isMplayerPathAndValid(self._config["playerPath"])): - self._config["playerType"] = "mplayer" - else: - raise InvalidConfigValue("Player path is not set properly") - elif(key == "host"): - self._config["host"], self._config["port"] = self._splitPortAndHost(self._config["host"]) - hostNotValid = (self._config["host"] == "" or self._config["host"] is None) - portNotValid = (self._config["port"] == "" or self._config["port"] is None) - if(hostNotValid or portNotValid): - raise InvalidConfigValue("Hostname can't be empty") - elif(self._config[key] == "" or self._config[key] is None): - raise InvalidConfigValue("{} can't be empty".format(key)) - - def _overrideConfigWithArgs(self, args): - for key, val in vars(args).items(): - if(val): - if(key == "force_gui_prompt"): - key = "forceGuiPrompt" - if(key == "no_store"): - key = "noStore" - if(key == "player_path"): - key = "playerPath" - if(key == "_args"): - key = "playerArgs" - if(key == "no_gui"): - key = "noGui" - self._config[key] = val - - def _isPlayerMPCAndValid(self, path): - if(os.path.isfile(path)): - if(path[-10:] == 'mpc-hc.exe' or path[-12:] == 'mpc-hc64.exe'): - return True - if(os.path.isfile(path + "\\mpc-hc.exe")): - path += "\\mpc-hc.exe" - return True - if(os.path.isfile(path + "\\mpc-hc64.exe")): - path += "\\mpc-hc64.exe" - return True - return False - - def __addSpecialMPCFlags(self): - self._config['playerArgs'].extend(['/open', '/new']) - - def _isMplayerPathAndValid(self, playerPath): - if("mplayer" in playerPath): - if os.access(playerPath, os.X_OK): - return True - for path in os.environ['PATH'].split(':'): - path = os.path.join(os.path.realpath(path), playerPath) - if os.access(path, os.X_OK): - self._config['playerPath'] = path - return True - return False - - def _splitPortAndHost(self, host): - port = constants.DEFAULT_PORT if not self._config["port"] else self._config["port"] - if(host): - if ':' in host: - host, port = host.split(':', 1) - return host, int(port) - - def _findWorkingDir(self): - frozen = getattr(sys, 'frozen', '') - if not frozen: - path = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) - elif frozen in ('dll', 'console_exe', 'windows_exe'): - path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))) - else: - path = "" - return path - - def _checkForPortableFile(self): - path = self._findWorkingDir() - if(os.path.isfile(os.path.join(path, constants.DEFAULT_CONFIG_NAME))): - return os.path.join(path, constants.DEFAULT_CONFIG_NAME) - - def _getConfigurationFilePath(self): - configFile = self._checkForPortableFile() - if(not configFile): - if(os.name <> 'nt'): - configFile = os.path.join(os.getenv('HOME', '.'), constants.DEFAULT_CONFIG_NAME) - else: - configFile = os.path.join(os.getenv('APPDATA', '.'), constants.DEFAULT_CONFIG_NAME) - return configFile - - def _parseConfigFile(self, iniPath): - parser = SafeConfigParser() - parser.read(iniPath) - for section, options in self._iniStructure.items(): - if(parser.has_section(section)): - for option in options: - if(parser.has_option(section, option)): - self._config[option] = parser.get(section, option) - - def _checkConfig(self): - try: - self._validateArguments() - except InvalidConfigValue: - try: - for key, value in self._promptForMissingArguments().items(): - self._config[key] = value - self._checkConfig() - except: - sys.exit() - - def _promptForMissingArguments(self): - if(self._config['noGui']): - print "Some necessary arguments are missing, refer to --help" - sys.exit() - elif(GuiConfiguration): - return GuiConfiguration(self._config).getProcessedConfiguration() - - def __wasOptionChanged(self, parser, section, option): - if (parser.has_option(section, option)): - if (parser.get(section, option) != str(self._config[option])): - return True - else: - return True - - def _saveConfig(self, iniPath): - changed = False - if(self._config['noStore']): - return - parser = SafeConfigParser() - parser.read(iniPath) - for section, options in self._iniStructure.items(): - if(not parser.has_section(section)): - parser.add_section(section) - changed = True - for option in options: - if(self.__wasOptionChanged(parser, section, option)): - changed = True - parser.set(section, option, str(self._config[option])) - if(changed): - parser.write(file(iniPath, "w")) - - def getConfiguration(self): - iniPath = self._getConfigurationFilePath() - self._parseConfigFile(iniPath) - args = self._argparser.parse_args() - self._overrideConfigWithArgs(args) - if(self._config['forceGuiPrompt']): - try: - self._promptForMissingArguments() - except: - sys.exit() - self._checkConfig() - self._saveConfig(iniPath) - return self._config - -class ServerConfigurationGetter(object): - def getConfiguration(self): - self._prepareArgParser() - self._args = self._argparser.parse_args() - if(self._args.port == None): - self._args.port = constants.DEFAULT_PORT - return self._args - - def _prepareArgParser(self): - self._argparser = argparse.ArgumentParser(description='Solution to synchronize playback of multiple MPlayer and MPC-HC instances over the network. Server instance', - epilog='If no options supplied _config values will be used') - self._argparser.add_argument('--port', metavar='port', type=str, nargs='?', help='server TCP port') - self._argparser.add_argument('--password', metavar='password', type=str, nargs='?', help='server password') - self._argparser.add_argument('--isolate-rooms', action='store_true', help='should rooms be isolated?') +from ConfigParser import SafeConfigParser +import argparse +import os +import sys +from syncplay import constants +from syncplay.messages import getMessage +try: + from syncplay.ui.GuiConfiguration import GuiConfiguration +except ImportError: + GuiConfiguration = None + +class InvalidConfigValue(Exception): + def __init__(self, message): + Exception.__init__(self, message) + +class ConfigurationGetter(object): + def __init__(self): + self._config = { + "host": None, + "port": constants.DEFAULT_PORT, + "name": None, + "debug": False, + "forceGuiPrompt": False, + "noGui": False, + "noStore": False, + "room": "", + "password": None, + "playerPath": None, + "file": None, + "playerArgs": [], + "playerType": None, + } + + # + #Custom validation in self._validateArguments + # + self._required = [ + "host", + "port", + "name", + "playerPath", + "playerType", + ] + + self._iniStructure = { + "server_data": ["host", "port", "password"], + "client_settings": ["name", "room", "playerPath"] + } + + # + #Watch out for the method self._overrideConfigWithArgs when you're adding custom multi-word command line arguments + # + self._argparser = argparse.ArgumentParser(description=getMessage("en", "argument-description"), + epilog='If no options supplied values from configuration file will be used') + self._argparser.add_argument('--no-gui', action='store_true', help=getMessage("en", "nogui-argument")) + self._argparser.add_argument('-a', '--host', metavar='hostname', type=str, help=getMessage("en", "host-argument")) + self._argparser.add_argument('-n', '--name', metavar='username', type=str, help=getMessage("en", "name-argument")) + self._argparser.add_argument('-d', '--debug', action='store_true', help=getMessage("en", "debug-argument")) + self._argparser.add_argument('-g', '--force-gui-prompt', action='store_true', help=getMessage("en", "force-gui-prompt-argument")) + self._argparser.add_argument('--no-store', action='store_true', help=getMessage("en", "no-store-argument")) + self._argparser.add_argument('-r', '--room', metavar='room', type=str, nargs='?', help=getMessage("en", "room-argument")) + self._argparser.add_argument('-p', '--password', metavar='password', type=str, nargs='?', help=getMessage("en", "password-argument")) + self._argparser.add_argument('--player-path', metavar='path', type=str, help=getMessage("en", "player-path-argument")) + self._argparser.add_argument('file', metavar='file', type=str, nargs='?', help=getMessage("en", "file-argument")) + self._argparser.add_argument('_args', metavar='options', type=str, nargs='*', help=getMessage("en", "args-argument")) + + def _validateArguments(self): + for key in self._required: + if(key == "playerPath"): + if(self._isPlayerMPCAndValid(self._config["playerPath"])): + self._config["playerType"] = "mpc" + self.__addSpecialMPCFlags() + elif(self._isMplayerPathAndValid(self._config["playerPath"])): + self._config["playerType"] = "mplayer" + else: + raise InvalidConfigValue(getMessage("en", "player-path-error")) + elif(key == "host"): + self._config["host"], self._config["port"] = self._splitPortAndHost(self._config["host"]) + hostNotValid = (self._config["host"] == "" or self._config["host"] is None) + portNotValid = (self._config["port"] == "" or self._config["port"] is None) + if(hostNotValid or portNotValid): + raise InvalidConfigValue(getMessage("en", "hostname-empty-error")) + elif(self._config[key] == "" or self._config[key] is None): + raise InvalidConfigValue(getMessage("en", "empty-error").format(key)) + + def _overrideConfigWithArgs(self, args): + for key, val in vars(args).items(): + if(val): + if(key == "force_gui_prompt"): + key = "forceGuiPrompt" + if(key == "no_store"): + key = "noStore" + if(key == "player_path"): + key = "playerPath" + if(key == "_args"): + key = "playerArgs" + if(key == "no_gui"): + key = "noGui" + self._config[key] = val + + def _isPlayerMPCAndValid(self, path): + if(os.path.isfile(path)): + if(path[-10:] == 'mpc-hc.exe' or path[-12:] == 'mpc-hc64.exe'): + return True + if(os.path.isfile(path + "\\mpc-hc.exe")): + path += "\\mpc-hc.exe" + return True + if(os.path.isfile(path + "\\mpc-hc64.exe")): + path += "\\mpc-hc64.exe" + return True + return False + + def __addSpecialMPCFlags(self): + self._config['playerArgs'].extend(['/open', '/new']) + + def _isMplayerPathAndValid(self, playerPath): + if("mplayer" in playerPath): + if os.access(playerPath, os.X_OK): + return True + for path in os.environ['PATH'].split(':'): + path = os.path.join(os.path.realpath(path), playerPath) + if os.access(path, os.X_OK): + self._config['playerPath'] = path + return True + return False + + def _splitPortAndHost(self, host): + port = constants.DEFAULT_PORT if not self._config["port"] else self._config["port"] + if(host): + if ':' in host: + host, port = host.split(':', 1) + return host, int(port) + + def _findWorkingDir(self): + frozen = getattr(sys, 'frozen', '') + if not frozen: + path = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + elif frozen in ('dll', 'console_exe', 'windows_exe'): + path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))) + else: + path = "" + return path + + def _checkForPortableFile(self): + path = self._findWorkingDir() + if(os.path.isfile(os.path.join(path, constants.DEFAULT_CONFIG_NAME))): + return os.path.join(path, constants.DEFAULT_CONFIG_NAME) + + def _getConfigurationFilePath(self): + configFile = self._checkForPortableFile() + if(not configFile): + if(os.name <> 'nt'): + configFile = os.path.join(os.getenv('HOME', '.'), constants.DEFAULT_CONFIG_NAME) + else: + configFile = os.path.join(os.getenv('APPDATA', '.'), constants.DEFAULT_CONFIG_NAME) + return configFile + + def _parseConfigFile(self, iniPath): + parser = SafeConfigParser() + parser.read(iniPath) + for section, options in self._iniStructure.items(): + if(parser.has_section(section)): + for option in options: + if(parser.has_option(section, option)): + self._config[option] = parser.get(section, option) + + def _checkConfig(self): + try: + self._validateArguments() + except InvalidConfigValue: + try: + for key, value in self._promptForMissingArguments().items(): + self._config[key] = value + self._checkConfig() + except: + sys.exit() + + def _promptForMissingArguments(self): + if(self._config['noGui']): + print getMessage("en", "arguments-missing-error") + sys.exit() + elif(GuiConfiguration): + return GuiConfiguration(self._config).getProcessedConfiguration() + + def __wasOptionChanged(self, parser, section, option): + if (parser.has_option(section, option)): + if (parser.get(section, option) != str(self._config[option])): + return True + else: + return True + + def _saveConfig(self, iniPath): + changed = False + if(self._config['noStore']): + return + parser = SafeConfigParser() + parser.read(iniPath) + for section, options in self._iniStructure.items(): + if(not parser.has_section(section)): + parser.add_section(section) + changed = True + for option in options: + if(self.__wasOptionChanged(parser, section, option)): + changed = True + parser.set(section, option, str(self._config[option])) + if(changed): + parser.write(file(iniPath, "w")) + + def getConfiguration(self): + iniPath = self._getConfigurationFilePath() + self._parseConfigFile(iniPath) + args = self._argparser.parse_args() + self._overrideConfigWithArgs(args) + if(self._config['forceGuiPrompt']): + try: + self._promptForMissingArguments() + except: + sys.exit() + self._checkConfig() + self._saveConfig(iniPath) + return self._config + +class ServerConfigurationGetter(object): + def getConfiguration(self): + self._prepareArgParser() + self._args = self._argparser.parse_args() + if(self._args.port == None): + self._args.port = constants.DEFAULT_PORT + return self._args + + def _prepareArgParser(self): + self._argparser = argparse.ArgumentParser(description=getMessage("en", "server-argument-description"), + epilog=getMessage("en", "argument-epilog")) + self._argparser.add_argument('--port', metavar='port', type=str, nargs='?', help=getMessage("en", "server-port-argument")) + self._argparser.add_argument('--password', metavar='password', type=str, nargs='?', help=getMessage("en", "password-argument")) + self._argparser.add_argument('--isolate-rooms', action='store_true', help=getMessage("en", "server-isolate-room-argument")) diff --git a/syncplay/ui/GuiConfiguration.py b/syncplay/ui/GuiConfiguration.py index c006ec0..263f57d 100644 --- a/syncplay/ui/GuiConfiguration.py +++ b/syncplay/ui/GuiConfiguration.py @@ -1,100 +1,101 @@ -import pygtk -import os -from syncplay import constants -pygtk.require('2.0') -import gtk -gtk.set_interactive(False) -import cairo, gio, pango, atk, pangocairo, gobject #@UnusedImport - -class GuiConfiguration: - def __init__(self, config): - self.config = config - self.closedAndNotSaved = False - self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) - self.window.set_title("Syncplay Configuration") - self.window.connect("delete_event", lambda w, e: self._windowClosed()) - vbox = gtk.VBox(False, 0) - self.window.add(vbox) - vbox.show() - self._addLabeledEntries(config, vbox) - - self.hostEntry.select_region(0, len(self.hostEntry.get_text())) - button = gtk.Button(stock=gtk.STOCK_SAVE) - button.connect("clicked", lambda w: self._saveDataAndLeave()) - vbox.pack_start(button, True, True, 0) - button.set_flags(gtk.CAN_DEFAULT) - button.grab_default() - button.show() - self.window.show() - gtk.main() - - def _windowClosed(self): - self.window.destroy() - gtk.main_quit() - self.closedAndNotSaved = True - - def _addLabeledEntries(self, config, vbox): - if(config['host'] == None): - host = "" - elif(":" in config['host']): - host = config['host'] - else: - host = config['host']+":"+str(config['port']) - - self.hostEntry = self._addLabeledEntryToVbox('Host: ', host, vbox, lambda __, _: self._saveDataAndLeave()) - self.userEntry = self._addLabeledEntryToVbox('Username: ', config['name'], vbox, lambda __, _: self._saveDataAndLeave()) - self.roomEntry = self._addLabeledEntryToVbox('Default room (optional): ', config['room'], vbox, lambda __, _: self._saveDataAndLeave()) - self.passEntry = self._addLabeledEntryToVbox('Server password (optional): ', config['password'], vbox, lambda __, _: self._saveDataAndLeave()) - self.mpcEntry = self._addLabeledEntryToVbox('Path to player executable: ', self._tryToFillUpMpcPath(config['playerPath']), vbox, lambda __, _: self._saveDataAndLeave()) - - def _tryToFillUpMpcPath(self, playerPath): - if(playerPath == None): - for path in constants.MPC_PATHS: - if(os.path.isfile(path)): - return path - return playerPath - - def getProcessedConfiguration(self): - if(self.closedAndNotSaved): - raise self.WindowClosed - return self.config - - def _saveDataAndLeave(self): - self.config['host'] = self.hostEntry.get_text() - self.config['name'] = self.userEntry.get_text() - self.config['room'] = self.roomEntry.get_text() - self.config['password'] = self.passEntry.get_text() - self.config['playerPath'] = self.mpcEntry.get_text() - self.window.destroy() - gtk.main_quit() - - def _addLabeledEntryToVbox(self, label, initialEntryValue, vbox, callback): - hbox = gtk.HBox(False, 0) - hbox.set_border_width(3) - vbox.pack_start(hbox, False, False, 0) - hbox.show() - label_ = gtk.Label() - label_.set_text(label) - label_.set_alignment(xalign=0, yalign=0.5) - hbox.pack_start(label_, False, False, 0) - label_.show() - entry = gtk.Entry() - entry.connect("activate", callback, entry) - if(initialEntryValue == None): - initialEntryValue = "" - entry.set_text(initialEntryValue) - hbox.pack_end(entry, False, False, 0) - entry.set_usize(200, -1) - entry.show() - hbox = gtk.HBox(False, 0) - vbox.add(hbox) - hbox.show() - return entry - - class WindowClosed(Exception): - def __init__(self): - Exception.__init__(self) - - - - +import pygtk +import os +from syncplay import constants +from syncplay.messages import getMessage +pygtk.require('2.0') +import gtk +gtk.set_interactive(False) +import cairo, gio, pango, atk, pangocairo, gobject #@UnusedImport + +class GuiConfiguration: + def __init__(self, config): + self.config = config + self.closedAndNotSaved = False + self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) + self.window.set_title("Syncplay Configuration") + self.window.connect("delete_event", lambda w, e: self._windowClosed()) + vbox = gtk.VBox(False, 0) + self.window.add(vbox) + vbox.show() + self._addLabeledEntries(config, vbox) + + self.hostEntry.select_region(0, len(self.hostEntry.get_text())) + button = gtk.Button(stock=gtk.STOCK_SAVE) + button.connect("clicked", lambda w: self._saveDataAndLeave()) + vbox.pack_start(button, True, True, 0) + button.set_flags(gtk.CAN_DEFAULT) + button.grab_default() + button.show() + self.window.show() + gtk.main() + + def _windowClosed(self): + self.window.destroy() + gtk.main_quit() + self.closedAndNotSaved = True + + def _addLabeledEntries(self, config, vbox): + if(config['host'] == None): + host = "" + elif(":" in config['host']): + host = config['host'] + else: + host = config['host']+":"+str(config['port']) + + self.hostEntry = self._addLabeledEntryToVbox(getMessage("en", "host-label"), host, vbox, lambda __, _: self._saveDataAndLeave()) + self.userEntry = self._addLabeledEntryToVbox(getMessage("en", "username-label"), config['name'], vbox, lambda __, _: self._saveDataAndLeave()) + self.roomEntry = self._addLabeledEntryToVbox(getMessage("en", "room-label"), config['room'], vbox, lambda __, _: self._saveDataAndLeave()) + self.passEntry = self._addLabeledEntryToVbox(getMessage("en", "password-label"), config['password'], vbox, lambda __, _: self._saveDataAndLeave()) + self.mpcEntry = self._addLabeledEntryToVbox(getMessage("en", "path-label"), self._tryToFillUpMpcPath(config['playerPath']), vbox, lambda __, _: self._saveDataAndLeave()) + + def _tryToFillUpMpcPath(self, playerPath): + if(playerPath == None): + for path in constants.MPC_PATHS: + if(os.path.isfile(path)): + return path + return playerPath + + def getProcessedConfiguration(self): + if(self.closedAndNotSaved): + raise self.WindowClosed + return self.config + + def _saveDataAndLeave(self): + self.config['host'] = self.hostEntry.get_text() + self.config['name'] = self.userEntry.get_text() + self.config['room'] = self.roomEntry.get_text() + self.config['password'] = self.passEntry.get_text() + self.config['playerPath'] = self.mpcEntry.get_text() + self.window.destroy() + gtk.main_quit() + + def _addLabeledEntryToVbox(self, label, initialEntryValue, vbox, callback): + hbox = gtk.HBox(False, 0) + hbox.set_border_width(3) + vbox.pack_start(hbox, False, False, 0) + hbox.show() + label_ = gtk.Label() + label_.set_text(label) + label_.set_alignment(xalign=0, yalign=0.5) + hbox.pack_start(label_, False, False, 0) + label_.show() + entry = gtk.Entry() + entry.connect("activate", callback, entry) + if(initialEntryValue == None): + initialEntryValue = "" + entry.set_text(initialEntryValue) + hbox.pack_end(entry, False, False, 0) + entry.set_usize(200, -1) + entry.show() + hbox = gtk.HBox(False, 0) + vbox.add(hbox) + hbox.show() + return entry + + class WindowClosed(Exception): + def __init__(self): + Exception.__init__(self) + + + + diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index aa75f21..551b46c 100644 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -1,122 +1,123 @@ -from __future__ import print_function -import threading -import time -import syncplay -import os -import re -from syncplay import utils -from syncplay import constants -class ConsoleUI(threading.Thread): - def __init__(self): - self.promptMode = threading.Event() - self.PromptResult = "" - self.promptMode.set() - self._syncplayClient = None - threading.Thread.__init__(self, name="ConsoleUI") - - def addClient(self, client): - self._syncplayClient = client - - def run(self): - while True: - data = raw_input() - data = data.rstrip('\n\r') - if(not self.promptMode.isSet()): - self.PromptResult = data - self.promptMode.set() - elif(self._syncplayClient): - self._executeCommand(data) - - def promptFor(self, prompt=">", message=""): - if message <> "": - print(message) - self.promptMode.clear() - print(prompt, end='') - self.promptMode.wait() - return self.PromptResult - - def showMessage(self, message, noTimestamp=False): - if(os.name == "nt"): - message = message.encode('ascii', 'replace') - if(noTimestamp): - print(message) - else: - print(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message) - - def showDebugMessage(self, message): - print(message) - - def showErrorMessage(self, message): - print("ERROR:\t" + message) - - def _extractSign(self, m): - if(m): - if(m == "-"): - return -1 - else: - return 1 - else: - return None - - def _tryAdvancedCommands(self, data): - o = re.match(constants.UI_OFFSET_REGEX, data) - s = re.match(constants.UI_SEEK_REGEX, data) - if(o): - sign = self._extractSign(o.group('sign')) - t = utils.parseTime(o.group('time')) - if(t is None): - return - if (o.group('sign') == "/"): - t = self._syncplayClient.getPlayerPosition() - t - elif(sign): - t = self._syncplayClient.getUserOffset() + sign * t - self._syncplayClient.setUserOffset(t) - return True - elif s: - sign = self._extractSign(s.group('sign')) - t = utils.parseTime(s.group('time')) - if(t is None): - return - if(sign): - t = self._syncplayClient.getGlobalPosition() + sign * t - self._syncplayClient.setPosition(t) - return True - return False - - def _executeCommand(self, data): - command = re.match(constants.UI_COMMAND_REGEX, data) - if(not command): - return - if(command.group('command') in constants.COMMANDS_UNDO): - tmp_pos = self._syncplayClient.getPlayerPosition() - self._syncplayClient.setPosition(self._syncplayClient.playerPositionBeforeLastSeek) - self._syncplayClient.playerPositionBeforeLastSeek = tmp_pos - elif (command.group('command') in constants.COMMANDS_LIST): - self._syncplayClient.getUserList() - elif (command.group('command') in constants.COMMANDS_PAUSE): - self._syncplayClient.setPaused(not self._syncplayClient.getPlayerPaused()) - elif (command.group('command') in constants.COMMANDS_ROOM): - room = command.group('parameter') - if room == None: - if self._syncplayClient.userlist.currentUser.file: - room = self._syncplayClient.userlist.currentUser.file["name"] - else: - room = self._syncplayClient.defaultRoom - - self._syncplayClient.setRoom(room) - self._syncplayClient.sendRoom() - else: - if(self._tryAdvancedCommands(data)): - return - if (command.group('command') not in constants.COMMANDS_HELP): - self.showMessage("Unrecognized command") - self.showMessage("Available commands:", True) - self.showMessage("\tr [name] - change room", True) - self.showMessage("\tl - show user list", True) - self.showMessage("\tu - undo last seek", True) - self.showMessage("\tp - toggle pause", True) - self.showMessage("\t[s][+-]time - seek to the given value of time, if + or - is not specified it's absolute time in seconds or min:sec", True) - self.showMessage("\th - this help", True) - self.showMessage("Syncplay version: {}".format(syncplay.version), True) - self.showMessage("More info available at: {}".format(syncplay.projectURL), True) - +from __future__ import print_function +import threading +import time +import syncplay +import os +import re +from syncplay import utils +from syncplay import constants +from syncplay.messages import getMessage +class ConsoleUI(threading.Thread): + def __init__(self): + self.promptMode = threading.Event() + self.PromptResult = "" + self.promptMode.set() + self._syncplayClient = None + threading.Thread.__init__(self, name="ConsoleUI") + + def addClient(self, client): + self._syncplayClient = client + + def run(self): + while True: + data = raw_input() + data = data.rstrip('\n\r') + if(not self.promptMode.isSet()): + self.PromptResult = data + self.promptMode.set() + elif(self._syncplayClient): + self._executeCommand(data) + + def promptFor(self, prompt=">", message=""): + if message <> "": + print(message) + self.promptMode.clear() + print(prompt, end='') + self.promptMode.wait() + return self.PromptResult + + def showMessage(self, message, noTimestamp=False): + if(os.name == "nt"): + message = message.encode('ascii', 'replace') + if(noTimestamp): + print(message) + else: + print(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message) + + def showDebugMessage(self, message): + print(message) + + def showErrorMessage(self, message): + print("ERROR:\t" + message) + + def _extractSign(self, m): + if(m): + if(m == "-"): + return -1 + else: + return 1 + else: + return None + + def _tryAdvancedCommands(self, data): + o = re.match(constants.UI_OFFSET_REGEX, data) + s = re.match(constants.UI_SEEK_REGEX, data) + if(o): + sign = self._extractSign(o.group('sign')) + t = utils.parseTime(o.group('time')) + if(t is None): + return + if (o.group('sign') == "/"): + t = self._syncplayClient.getPlayerPosition() - t + elif(sign): + t = self._syncplayClient.getUserOffset() + sign * t + self._syncplayClient.setUserOffset(t) + return True + elif s: + sign = self._extractSign(s.group('sign')) + t = utils.parseTime(s.group('time')) + if(t is None): + return + if(sign): + t = self._syncplayClient.getGlobalPosition() + sign * t + self._syncplayClient.setPosition(t) + return True + return False + + def _executeCommand(self, data): + command = re.match(constants.UI_COMMAND_REGEX, data) + if(not command): + return + if(command.group('command') in constants.COMMANDS_UNDO): + tmp_pos = self._syncplayClient.getPlayerPosition() + self._syncplayClient.setPosition(self._syncplayClient.playerPositionBeforeLastSeek) + self._syncplayClient.playerPositionBeforeLastSeek = tmp_pos + elif (command.group('command') in constants.COMMANDS_LIST): + self._syncplayClient.getUserList() + elif (command.group('command') in constants.COMMANDS_PAUSE): + self._syncplayClient.setPaused(not self._syncplayClient.getPlayerPaused()) + elif (command.group('command') in constants.COMMANDS_ROOM): + room = command.group('parameter') + if room == None: + if self._syncplayClient.userlist.currentUser.file: + room = self._syncplayClient.userlist.currentUser.file["name"] + else: + room = self._syncplayClient.defaultRoom + + self._syncplayClient.setRoom(room) + self._syncplayClient.sendRoom() + else: + if(self._tryAdvancedCommands(data)): + return + if (command.group('command') not in constants.COMMANDS_HELP): + self.showMessage(getMessage("en", "unrecognized-command-notification")) + self.showMessage(getMessage("en", "commandlist-notification"), True) + self.showMessage(getMessage("en", "commandlist-notification/room"), True) + self.showMessage(getMessage("en", "commandlist-notification/list"), True) + self.showMessage(getMessage("en", "commandlist-notification/undo"), True) + self.showMessage(getMessage("en", "commandlist-notification/pause"), True) + self.showMessage(getMessage("en", "commandlist-notification/seek"), True) + self.showMessage(getMessage("en", "commandlist-notification/help"), True) + self.showMessage("Syncplay version: {}".format(syncplay.version), True) + self.showMessage("More info available at: {}".format(syncplay.projectURL), True) + diff --git a/syncplay/utils.py b/syncplay/utils.py index b5ccebf..e399524 100644 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -1,76 +1,77 @@ -import time -import re -import datetime -from syncplay import constants - -def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None): - """Retry calling the decorated function using an exponential backoff. - - http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/ - original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry - - :param ExceptionToCheck: the exception to check. may be a tuple of - excpetions to check - :type ExceptionToCheck: Exception or tuple - :param tries: number of times to try (not retry) before giving up - :type tries: int - :param delay: initial delay between retries in seconds - :type delay: int - :param backoff: backoff multiplier e.g. value of 2 will double the delay - each retry - :type backoff: int - :param logger: logger to use. If None, print - :type logger: logging.Logger instance - """ - def deco_retry(f): - def f_retry(*args, **kwargs): - mtries, mdelay = tries, delay - try_one_last_time = True - while mtries > 1: - try: - return f(*args, **kwargs) - try_one_last_time = False - break - except ExceptionToCheck, e: - if logger: - msg = "%s, Retrying in %d seconds..." % (str(e), mdelay) - logger.warning(msg) - time.sleep(mdelay) - mtries -= 1 - mdelay *= backoff - if try_one_last_time: - return f(*args, **kwargs) - return - return f_retry # true decorator - return deco_retry - -def parseTime(timeStr): - regex = re.compile(constants.PARSE_TIME_REGEX) - parts = regex.match(timeStr) - if not parts: - return - parts = parts.groupdict() - time_params = {} - for (name, param) in parts.iteritems(): - if param: - if(name == "miliseconds"): - time_params["microseconds"] = int(param) * 1000 - else: - time_params[name] = int(param) - return datetime.timedelta(**time_params).total_seconds() - -def formatTime(timeInSeconds): - timeInSeconds = round(timeInSeconds) - weeks = timeInSeconds // 604800 - days = (timeInSeconds % 604800) // 86400 - hours = (timeInSeconds % 86400) // 3600 - minutes = (timeInSeconds % 3600) // 60 - seconds = timeInSeconds % 60 - if(weeks > 0): - return '{0:.0f}w, {1:.0f}d, {2:02.0f}:{3:02.0f}:{4:02.0f}'.format(weeks, days, hours, minutes, seconds) - elif(days > 0): - return '{0:.0f}d, {1:02.0f}:{2:02.0f}:{3:02.0f}'.format(days, hours, minutes, seconds) - elif(hours > 0): - return '{0:02.0f}:{1:02.0f}:{2:02.0f}'.format(hours, minutes, seconds) - else: - return '{0:02.0f}:{1:02.0f}'.format(minutes, seconds) +import time +import re +import datetime +from syncplay import constants +from syncplay.messages import getMessage + +def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None): + """Retry calling the decorated function using an exponential backoff. + + http://www.saltycrane.com/blog/2009/11/trying-out-retry-decorator-python/ + original from: http://wiki.python.org/moin/PythonDecoratorLibrary#Retry + + :param ExceptionToCheck: the exception to check. may be a tuple of + excpetions to check + :type ExceptionToCheck: Exception or tuple + :param tries: number of times to try (not retry) before giving up + :type tries: int + :param delay: initial delay between retries in seconds + :type delay: int + :param backoff: backoff multiplier e.g. value of 2 will double the delay + each retry + :type backoff: int + :param logger: logger to use. If None, print + :type logger: logging.Logger instance + """ + def deco_retry(f): + def f_retry(*args, **kwargs): + mtries, mdelay = tries, delay + try_one_last_time = True + while mtries > 1: + try: + return f(*args, **kwargs) + try_one_last_time = False + break + except ExceptionToCheck, e: + if logger: + msg = getMessage("en", "retrying-notification") % (str(e), mdelay) + logger.warning(msg) + time.sleep(mdelay) + mtries -= 1 + mdelay *= backoff + if try_one_last_time: + return f(*args, **kwargs) + return + return f_retry # true decorator + return deco_retry + +def parseTime(timeStr): + regex = re.compile(constants.PARSE_TIME_REGEX) + parts = regex.match(timeStr) + if not parts: + return + parts = parts.groupdict() + time_params = {} + for (name, param) in parts.iteritems(): + if param: + if(name == "miliseconds"): + time_params["microseconds"] = int(param) * 1000 + else: + time_params[name] = int(param) + return datetime.timedelta(**time_params).total_seconds() + +def formatTime(timeInSeconds): + timeInSeconds = round(timeInSeconds) + weeks = timeInSeconds // 604800 + days = (timeInSeconds % 604800) // 86400 + hours = (timeInSeconds % 86400) // 3600 + minutes = (timeInSeconds % 3600) // 60 + seconds = timeInSeconds % 60 + if(weeks > 0): + return '{0:.0f}w, {1:.0f}d, {2:02.0f}:{3:02.0f}:{4:02.0f}'.format(weeks, days, hours, minutes, seconds) + elif(days > 0): + return '{0:.0f}d, {1:02.0f}:{2:02.0f}:{3:02.0f}'.format(days, hours, minutes, seconds) + elif(hours > 0): + return '{0:02.0f}:{1:02.0f}:{2:02.0f}'.format(hours, minutes, seconds) + else: + return '{0:02.0f}:{1:02.0f}'.format(minutes, seconds)