#coding:utf8 import hashlib import os.path import time from twisted.internet.protocol import ClientFactory from twisted.internet import reactor, task from syncplay.protocols import SyncClientProtocol from syncplay import utils, constants class SyncClientFactory(ClientFactory): def __init__(self, client, retry = constants.RECONNECT_RETRIES): self._client = client self.retry = retry self._timesTried = 0 self.reconnecting = False def buildProtocol(self, addr): return SyncClientProtocol(self._client) def startedConnecting(self, connector): destination = connector.getDestination() self._client.ui.showMessage('Attempting to connect to {}:{}'.format(destination.host, destination.port)) def clientConnectionLost(self, connector, reason): if self._timesTried < self.retry: self._timesTried += 1 message = 'Connection with server lost, attempting to reconnect' self._client.ui.showMessage(message) self.reconnecting = True reactor.callLater(0.1*(2**self._timesTried), connector.connect) else: message = 'Disconnected from server' self._client.ui.showMessage(message) def clientConnectionFailed(self, connector, reason): if not self.reconnecting: message = 'Connection with server failed' self._client.ui.showMessage(message) self._client.stop(True) else: self.clientConnectionLost(connector, reason) def resetRetrying(self): self._timesTried = 0 def stopRetrying(self): self._timesTried = self.retry class SyncplayClient(object): def __init__(self, playerClass, ui, args): self.protocolFactory = SyncClientFactory(self) self.ui = UiManager(self, ui) self.userlist = SyncplayUserlist(self.ui, self) self._protocol = None if(args.room == None or args.room == ''): args.room = constants.DEFAULT_ROOM self.defaultRoom = args.room self.playerPositionBeforeLastSeek = 0.0 self.setUsername(args.name) self.setRoom(args.room) if(args.password): args.password = hashlib.md5(args.password).hexdigest() self._serverPassword = args.password if(not args.file): self.__getUserlistOnLogon = True else: self.__getUserlistOnLogon = False self._player = None self._playerClass = playerClass self._startupArgs = args self._running = False self._askPlayerTimer = None self._lastPlayerUpdate = None self._playerPosition = 0.0 self._playerPaused = True self._lastGlobalUpdate = None self._globalPosition = 0.0 self._globalPaused = 0.0 self._userOffset = 0.0 self._speedChanged = False def initProtocol(self, protocol): self._protocol = protocol def destroyProtocol(self): if(self._protocol): self._protocol.drop() self._protocol = None def initPlayer(self, player): self._player = player self.scheduleAskPlayer() def scheduleAskPlayer(self, when=constants.PLAYER_ASK_DELAY): self._askPlayerTimer = task.LoopingCall(self.askPlayer) self._askPlayerTimer.start(when) def askPlayer(self): if(not self._running): return if(self._player): self._player.askForStatus() self.checkIfConnected() def checkIfConnected(self): if(self._lastGlobalUpdate and self._protocol and time.time() - self._lastGlobalUpdate > constants.PROTOCOL_TIMEOUT): self._lastGlobalUpdate = None self.ui.showErrorMessage("Connection with server timed out") self._protocol.drop() return False return True def _determinePlayerStateChange(self, paused, position): pauseChange = self.getPlayerPaused() != paused and self.getGlobalPaused() != paused _playerDiff = abs(self.getPlayerPosition() - position) _globalDiff = abs(self.getGlobalPosition() - position) seeked = _playerDiff > constants.SEEK_BOUNDARY and _globalDiff > constants.SEEK_BOUNDARY return pauseChange, seeked def updatePlayerStatus(self, paused, position): position -= self.getUserOffset() pauseChange, seeked = self._determinePlayerStateChange(paused, position) self._playerPosition = position self._playerPaused = paused if(self._lastGlobalUpdate): self._lastPlayerUpdate = time.time() if((pauseChange or seeked) and self._protocol): if(seeked): self.playerPositionBeforeLastSeek = self.getGlobalPosition() self._protocol.sendState(self.getPlayerPosition(), self.getPlayerPaused(), seeked, None, True) def getLocalState(self): paused = self.getPlayerPaused() position = self.getPlayerPosition() pauseChange, _ = self._determinePlayerStateChange(paused, position) if(self._lastGlobalUpdate): return position, paused, _, pauseChange else: return None, None, None, None def _initPlayerState(self, position, paused): self.setPosition(position) self._player.setPaused(paused) madeChangeOnPlayer = True return madeChangeOnPlayer def _rewindPlayerDueToTimeDifference(self, position, setBy): self.setPosition(position) message = "Rewinded due to time difference with <{}>".format(setBy) self.ui.showMessage(message) madeChangeOnPlayer = True return madeChangeOnPlayer def _serverUnpaused(self, setBy): self._player.setPaused(False) madeChangeOnPlayer = True message = '<{}> unpaused'.format(setBy) self.ui.showMessage(message) return madeChangeOnPlayer def _serverPaused(self, setBy, diff): if (diff > 0): self.setPosition(self.getGlobalPosition()) self._player.setPaused(True) madeChangeOnPlayer = True message = '<{}> paused'.format(setBy) self.ui.showMessage(message) return madeChangeOnPlayer def _serverSeeked(self, position, setBy): if(self.getUsername() <> setBy): self.playerPositionBeforeLastSeek = self.getPlayerPosition() self.setPosition(position) madeChangeOnPlayer = True else: madeChangeOnPlayer = False message = '<{}> jumped from {} to {}'.format(setBy, utils.formatTime(self.playerPositionBeforeLastSeek), utils.formatTime(position)) self.ui.showMessage(message) return madeChangeOnPlayer def _slowDownToCoverTimeDifference(self, diff, setBy): if(constants.SLOWDOWN_KICKIN_BOUNDARY < diff and not self._speedChanged): self._player.setSpeed(constants.SLOWDOWN_RATE) self._speedChanged = True message = "Slowing down due to time difference with <{}>".format(setBy) self.ui.showMessage(message) elif(self._speedChanged and diff < constants.SLOWDOWN_RESET_BOUNDARY): self._player.setSpeed(1.00) self._speedChanged = False message = "Reverting speed back to normal" self.ui.showMessage(message) madeChangeOnPlayer = True return madeChangeOnPlayer def _changePlayerStateAccordingToGlobalState(self, position, paused, doSeek, setBy): madeChangeOnPlayer = False pauseChanged = paused != self.getGlobalPaused() diff = self.getPlayerPosition() - position if(self._lastGlobalUpdate is None): madeChangeOnPlayer = self._initPlayerState(position, paused) self._globalPaused = paused self._globalPosition = position self._lastGlobalUpdate = time.time() if (doSeek): madeChangeOnPlayer = self._serverSeeked(position, setBy) if (diff > 4 and not doSeek): madeChangeOnPlayer = self._rewindPlayerDueToTimeDifference(position, setBy) if (self._player.speedSupported and not doSeek and not paused): madeChangeOnPlayer = self._slowDownToCoverTimeDifference(diff, setBy) if (paused == False and pauseChanged): madeChangeOnPlayer = self._serverUnpaused(setBy) elif (paused == True and pauseChanged): madeChangeOnPlayer = self._serverPaused(setBy, diff) return madeChangeOnPlayer def updateGlobalState(self, position, paused, doSeek, setBy, latency): if(self.__getUserlistOnLogon): 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): if(self._lastGlobalUpdate): return self.getGlobalPosition() else: return 0.0 position = self._playerPosition if(not self._playerPaused): diff = time.time() - self._lastPlayerUpdate if diff < 0.5: position += diff return position def getPlayerPaused(self): if(not self._lastPlayerUpdate): if(self._lastGlobalUpdate): return self.getGlobalPaused() else: return True return self._playerPaused def getGlobalPosition(self): if not self._lastGlobalUpdate: return 0.0 position = self._globalPosition if not self._globalPaused: position += time.time() - self._lastGlobalUpdate return position def getGlobalPaused(self): if(not self._lastGlobalUpdate): return True return self._globalPaused def updateFile(self, filename, duration, path): size = os.path.getsize(path) self.userlist.currentUser.setFile(filename, duration, size) self.sendFile() def sendFile(self): file_ = self.userlist.currentUser.file if(self._protocol and self._protocol.logged and file_): self._protocol.sendFileSetting(file_) def setUsername(self, username): self.userlist.currentUser.username = username def getUsername(self): return self.userlist.currentUser.username def setRoom(self, roomName): self.userlist.currentUser.room = roomName self.getUserList() def sendRoom(self): room = self.userlist.currentUser.room if(self._protocol and self._protocol.logged and room): self._protocol.sendRoomSetting(room) def getRoom(self): return self.userlist.currentUser.room def getUserList(self): if(self._protocol and self._protocol.logged): self._protocol.sendList() def getPassword(self): return self._serverPassword def setPosition(self, position): position += self.getUserOffset() if(self._player): if(position < 0): position = 0 self._protocol.sendState(self.getPlayerPosition(), self.getPlayerPaused(), True, None, True) self._player.setPosition(position) def setPaused(self, paused): if(self._player): self._player.setPaused(paused) def start(self, host, port): if self._running: return self._running = True if self._playerClass: self._playerClass.run(self, self._startupArgs.player_path, self._startupArgs.file, self._startupArgs._args) self._playerClass = None self.protocolFactory = SyncClientFactory(self) reactor.connectTCP(host, port, self.protocolFactory) reactor.run() def stop(self, promptForAction = False): if not self._running: return self._running = False if self.protocolFactory: self.protocolFactory.stopRetrying() self.destroyProtocol() if self._player: self._player.drop() reactor.callLater(0.1, reactor.stop) if(promptForAction): self.ui.promptFor("Press enter to exit\n") class SyncplayUser(object): def __init__(self, username = None, room = None, file_ = None, position = 0): self.username = username self.room = room self.file = file_ self.lastPosition = position def setFile(self, filename, duration, size): file_ = { "name": filename, "duration": duration, "size":size } self.file = file_ def isFileSame(self, file_): if(not self.file): return False sameName = self.file['name'] == file_['name'] sameSize = self.file['size'] == file_['size'] sameDuration = int(self.file['duration']) - int(file_['duration']) < constants.DIFFFERENT_DURATION_BOUNDARY return sameName and sameSize and sameDuration def __lt__(self, other): return self.username < other.username class SyncplayUserlist(object): def __init__(self, ui, client): self.currentUser = SyncplayUser() self._users = {} self.ui = ui self._client = client def __showUserChangeMessage(self, username, room, file_): if (room and not file_): message = "<{}> has joined the room: '{}'".format(username, room) self.ui.showMessage(message) elif (room and file_ and username != self.currentUser.username): 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) self.ui.showMessage(message) if(self.currentUser.file and not self.currentUser.isFileSame(file_) and self.currentUser.room == room): message = "File you are playing appears to be different from <{}>'s".format(username) self.ui.showMessage(message) differences = [] if(self.currentUser.file['name'] <> file_['name']): differences.append("filename") if(self.currentUser.file['size'] <> file_['size']): differences.append("size") if(self.currentUser.file['duration'] <> file_['duration']): differences.append("duration") message = "Your file differs in the following way(s): " + ", ".join(differences) self.ui.showMessage(message) def addUser(self, username, room, file_, position = 0, noMessage = False): if(username == self.currentUser.username): self.currentUser.lastPosition = position return user = SyncplayUser(username, room, file_, position) self._users[username] = user if(not noMessage): self.__showUserChangeMessage(username, room, file_) def removeUser(self, username): if(self._users.has_key(username)): self._users.pop(username) message = "<{}> has left".format(username) self.ui.showMessage(message) def __displayModUserMessage(self, username, room, file_, user): if (file_ and not user.isFileSame(file_)): self.__showUserChangeMessage(username, room, file_) elif (room and room != user.room): self.__showUserChangeMessage(username, room, None) def modUser(self, username, room, file_): if(self._users.has_key(username)): user = self._users[username] self.__displayModUserMessage(username, room, file_, user) user.room = room user.file = file_ elif(username == self.currentUser.username): self.__showUserChangeMessage(username, room, file_) else: self.addUser(username, room, file_) def __addUserWithFileToList(self, rooms, user): 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 def __addUserWithoutFileToList(self, rooms, user): if (not rooms[user.room].has_key("__noFile__")): rooms[user.room]["__noFile__"] = {} rooms[user.room]["__noFile__"][user.username] = user def __createListOfPeople(self, rooms): if(not rooms.has_key(self.currentUser.room)): rooms[self.currentUser.room] = {} for user in self._users.itervalues(): if (not rooms.has_key(user.room)): rooms[user.room] = {} if(user.file): self.__addUserWithFileToList(rooms, user) else: self.__addUserWithoutFileToList(rooms, user) if(self.currentUser.file): self.__addUserWithFileToList(rooms, self.currentUser) else: self.__addUserWithoutFileToList(rooms, self.currentUser) return rooms def __addDifferentFileMessageIfNecessary(self, user, message): if(self.currentUser.file): fileHasSameSizeAsYour = user.file['size'] == self.currentUser.file['size'] fileHasSameNameYour = user.file['name'] == self.currentUser.file['name'] differentFileMessage = " (their file size is different from yours!)" message += differentFileMessage if not fileHasSameSizeAsYour and fileHasSameNameYour else "" return message def __displayFileWatchersInRoomList(self, key, users): self.ui.showMessage("File: {} is being played by:".format(key), True, True) for user in sorted(users.itervalues()): message = "<"+user.username+">" if(self.currentUser.username == user.username): message = "*" + message + "*" message = self.__addDifferentFileMessageIfNecessary(user, message) self.ui.showMessage("\t" + message, True, True) def __displayPeopleInRoomWithNoFile(self, noFileList): if (noFileList): self.ui.showMessage("People who are not playing any file:", True, True) for user in sorted(noFileList.itervalues()): self.ui.showMessage("\t<" + user.username + ">", True, True) def __displayListOfPeople(self, rooms): for roomName in sorted(rooms.iterkeys()): self.ui.showMessage("In room '{}':".format(roomName), True, False) noFileList = rooms[roomName].pop("__noFile__") if (rooms[roomName].has_key("__noFile__")) else None for key in sorted(rooms[roomName].iterkeys()): self.__displayFileWatchersInRoomList(key, rooms[roomName][key]) self.__displayPeopleInRoomWithNoFile(noFileList) def showUserList(self): rooms = {} self.__createListOfPeople(rooms) self.__displayListOfPeople(rooms) def clearList(self): self._users = {} class UiManager(object): def __init__(self, client, ui): self._client = client self.__ui = ui def showMessage(self, message, noPlayer = False, noTimestamp = False): if(self._client._player and not noPlayer): self._client._player.displayMessage(message) self.__ui.showMessage(message, noTimestamp) def showErrorMessage(self, message): self.__ui.showErrorMessage(message) def promptFor(self, prompt): return self.__ui.promptFor(prompt)