diff --git a/syncplay/client.py b/syncplay/client.py index 134e85f..82f9611 100644 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -5,6 +5,7 @@ import time from twisted.internet.protocol import ClientFactory from twisted.internet import reactor, task from syncplay.protocols import SyncClientProtocol +from syncplay import utils class SyncClientFactory(ClientFactory): def __init__(self, client, retry = 10): @@ -78,6 +79,7 @@ class SyncplayClient(object): self._lastGlobalUpdate = None self._globalPosition = 0.0 self._globalPaused = 0.0 + self._userOffset = 0.0 self._speedChanged = False def initProtocol(self, protocol): @@ -119,7 +121,8 @@ class SyncplayClient(object): return pauseChange, seeked def updatePlayerStatus(self, paused, position): - pauseChange, seeked = self._determinePlayerStateChange(paused, position) + position -= self.getUserOffset() + pauseChange, seeked = self._determinePlayerStateChange(paused, position) self._playerPosition = position self._playerPaused = paused if(self._lastGlobalUpdate): @@ -139,13 +142,13 @@ class SyncplayClient(object): return None, None, None, None def _initPlayerState(self, position, paused): - self._player.setPosition(position) + self.setPosition(position) self._player.setPaused(paused) madeChangeOnPlayer = True return madeChangeOnPlayer def _rewindPlayerDueToTimeDifference(self, position, setBy): - self._player.setPosition(position) + self.setPosition(position) message = "Rewinded due to time difference with <{}>".format(setBy) self.ui.showMessage(message) madeChangeOnPlayer = True @@ -160,7 +163,7 @@ class SyncplayClient(object): def _serverPaused(self, setBy, diff): if (diff > 0): - self._player.setPosition(self.getGlobalPosition()) + self.setPosition(self.getGlobalPosition()) self._player.setPaused(True) madeChangeOnPlayer = True message = '<{}> paused'.format(setBy) @@ -170,11 +173,11 @@ class SyncplayClient(object): def _serverSeeked(self, position, setBy): if(self.getUsername() <> setBy): self.playerPositionBeforeLastSeek = self.getPlayerPosition() - self._player.setPosition(position) + self.setPosition(position) madeChangeOnPlayer = True else: madeChangeOnPlayer = False - message = '<{}> jumped from {} to {}'.format(setBy, self.ui.formatTime(self.playerPositionBeforeLastSeek), self.ui.formatTime(position)) + message = '<{}> jumped from {} to {}'.format(setBy, utils.formatTime(self.playerPositionBeforeLastSeek), utils.formatTime(position)) self.ui.showMessage(message) return madeChangeOnPlayer @@ -214,12 +217,22 @@ class SyncplayClient(object): self.__getUserlistOnLogon = False self.getUserList() madeChangeOnPlayer = False + if(not paused): position += latency if(self._player): madeChangeOnPlayer = self._changePlayerStateAccordingToGlobalState(position, paused, doSeek, setBy) if(madeChangeOnPlayer): self.askPlayer() + + def getUserOffset(self): + return self._userOffset + + def setUserOffset(self, time): + self._userOffset = time + message = "Current offset: {} seconds".format(self._userOffset) + self.setPosition(self.getGlobalPosition()) + self.ui.showMessage(message) def getPlayerPosition(self): if(not self._lastPlayerUpdate): @@ -291,7 +304,10 @@ class SyncplayClient(object): return self._serverPassword def setPosition(self, position): + position += self.getUserOffset() if(self._player): + if(position < 0): + position = 0 self._player.setPosition(position) def setPaused(self, paused): @@ -360,7 +376,7 @@ class SyncplayUserlist(object): message = "<{}> has joined the room: '{}'".format(username, room) self.ui.showMessage(message) elif (room and file_ and username != self.currentUser.username): - duration = self.ui.formatTime(file_['duration']) + duration = utils.formatTime(file_['duration']) message = "<{}> is playing '{}' ({})".format(username, file_['name'], duration) if(self.currentUser.room <> room or self.currentUser.username == username): message += " in room: '{}'".format(room) @@ -411,8 +427,8 @@ class SyncplayUserlist(object): self.addUser(username, room, file_) def __addUserWithFileToList(self, rooms, user): - currentPosition = self.ui.formatTime(user.lastPosition) - file_key = '\'{}\' ({}/{})'.format(user.file['name'], currentPosition, self.ui.formatTime(user.file['duration'])) + currentPosition = utils.formatTime(user.lastPosition) + file_key = '\'{}\' ({}/{})'.format(user.file['name'], currentPosition, utils.formatTime(user.file['duration'])) if (not rooms[user.room].has_key(file_key)): rooms[user.room][file_key] = {} rooms[user.room][file_key][user.username] = user @@ -492,19 +508,3 @@ class UiManager(object): def promptFor(self, prompt): return self.__ui.promptFor(prompt) - def formatTime(self, 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) - diff --git a/syncplay/players/mpc.py b/syncplay/players/mpc.py index 6a11a9e..7c819e3 100644 --- a/syncplay/players/mpc.py +++ b/syncplay/players/mpc.py @@ -6,7 +6,7 @@ import win32con, win32api, win32gui, ctypes, ctypes.wintypes #@UnresolvedImport from functools import wraps from syncplay.players.basePlayer import BasePlayer import re -from syncplay.util import retry +from syncplay.utils import retry class MpcHcApi: diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index a00931c..fea0353 100644 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -4,6 +4,7 @@ import time import syncplay import os import re +from syncplay import utils class ConsoleUI(threading.Thread): def __init__(self): @@ -48,26 +49,52 @@ class ConsoleUI(threading.Thread): def showErrorMessage(self, message): print("ERROR:\t" + message) - def __doSeek(self, m): - if (m.group(4)): - t = int(m.group(5)) * 60 + int(m.group(6)) - else: - t = int(m.group(2)) + def _extractRegexSign(self, m): if(m.group(1)): if(m.group(1) == "-"): - sign = -1 + return -1 else: - sign = 1 - t = self._syncplayClient.getGlobalPosition() + sign * t - self._syncplayClient.setPosition(t) + return 1 + else: + return None + def _tryAdvancedCommands(self, data): + o = re.match(r"^(?:o|offset)\ ([+-])?\ ?(.+)$", data) + s = re.match(r"^(?:s|seek)?\ ?([+-])?\ ?(.+)$", data) #careful! s will match o as well + if(o): + sign = self._extractRegexSign(o) + t = utils.parseTime(o.group(2)) + if(not t): + return + if(sign): + t = self._syncplayClient.getUserOffset() + sign * t + self._syncplayClient.setUserOffset(t) + return True + elif s: + sign = self._extractRegexSign(s) + t = utils.parseTime(s.group(2)) + 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): - m = re.match(r"^s? ?([+-])? ?((\d+)|((\d+)\D(\d+)))$", data) - r = re.match(r"^(r|room)( (.+))?$", data) - if(m): - self.__doSeek(m) - elif r: - room = r.group(3) + command = re.match(r"^(.+)(?:\ (.+))?", data) + if(not command): + return + if(command.group(1) in ["u", "undo", "revert"]): + tmp_pos = self._syncplayClient.getPlayerPosition() + self._syncplayClient.setPosition(self._syncplayClient.playerPositionBeforeLastSeek) + self._syncplayClient.playerPositionBeforeLastSeek = tmp_pos + elif (command.group(1) in ["l", "list", "users"]): + self._syncplayClient.getUserList() + elif (command.group(1) in ["p", "play", "pause"]): + self._syncplayClient.setPaused(not self._syncplayClient.getPlayerPaused()) + elif (command.group(1) in ["r", "room"]): + room = command.group(1) if room == None: if self._syncplayClient.userlist.currentUser.file: room = self._syncplayClient.userlist.currentUser.file["name"] @@ -75,16 +102,10 @@ class ConsoleUI(threading.Thread): room = self._syncplayClient.defaultRoom self._syncplayClient.setRoom(room) self._syncplayClient.sendRoom() - elif data == "u": - tmp_pos = self._syncplayClient.getPlayerPosition() - self._syncplayClient.setPosition(self._syncplayClient.playerPositionBeforeLastSeek) - self._syncplayClient.playerPositionBeforeLastSeek = tmp_pos - elif data == "l": - self._syncplayClient.getUserList() - elif data == "p": - self._syncplayClient.setPaused(not self._syncplayClient.getPlayerPaused()) else: - if data not in ['help', 'h', '?', '/?', '\?']: + if(self._tryAdvancedCommands(data)): + return + if (command.group(1) not in ['help', 'h', '?', '/?', '\?']): self.showMessage("Unrecognized command") self.showMessage("Available commands:", True) self.showMessage("\tr [name] - change room", True) @@ -95,4 +116,4 @@ class ConsoleUI(threading.Thread): self.showMessage("\th - this help", True) self.showMessage("Syncplay version: {}".format(syncplay.version), True) self.showMessage("More info available at: {}".format(syncplay.projectURL), True) - + diff --git a/syncplay/util.py b/syncplay/utils.py similarity index 54% rename from syncplay/util.py rename to syncplay/utils.py index 34deb96..9fab2bb 100644 --- a/syncplay/util.py +++ b/syncplay/utils.py @@ -1,4 +1,6 @@ import time +import re +import datetime def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None): """Retry calling the decorated function using an exponential backoff. @@ -39,4 +41,35 @@ def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None): return f(*args, **kwargs) return return f_retry # true decorator - return deco_retry \ No newline at end of file + return deco_retry + +def parseTime(timeStr): + regex = re.compile(r'(:?(?:(?P\d+?)[^\d\.])?(?:(?P\d+?))?[^\d\.])?(?P\d+?)(?:\.(?P\d+?))?$') + 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)