syncplay/syncplay/server.py
2014-04-21 21:27:35 +02:00

327 lines
12 KiB
Python

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
import codecs
import os
from string import Template
import argparse
from pprint import pprint
class SyncFactory(Factory):
def __init__(self, password='', motdFilePath=None, isolateRooms=False):
print getMessage("en", "welcome-server-notification").format(syncplay.version)
if(password):
password = hashlib.md5(password).hexdigest()
self.password = password
self._motdFilePath = motdFilePath
if(not isolateRooms):
self._roomManager = RoomManager()
else:
self._roomManager = PublicRoomManager()
def buildProtocol(self, addr):
return SyncServerProtocol(self)
def sendState(self, watcher, doSeek=False, forcedUpdate=False):
room = watcher.getRoom()
if room:
paused, position = room.isPaused(), room.getPosition()
setBy = room.getSetBy()
watcher.sendState(position, paused, doSeek, setBy, forcedUpdate)
def getMotd(self, userIp, username, room, clientVersion):
oldClient = False
if constants.WARN_OLD_CLIENTS:
if int(clientVersion.replace(".", "")) < int(constants.RECENT_CLIENT_THRESHOLD.replace(".", "")):
oldClient = True
if(self._motdFilePath and os.path.isfile(self._motdFilePath)):
tmpl = codecs.open(self._motdFilePath, "r", "utf-8-sig").read()
args = dict(version=syncplay.version, userIp=userIp, username=username, room=room)
try:
motd = Template(tmpl).substitute(args)
if oldClient:
motdwarning = getMessage("en", "new-syncplay-available-motd-message").format(clientVersion)
motd = "{}\n{}".format(motdwarning, motd)
return motd if len(motd) < constants.SERVER_MAX_TEMPLATE_LENGTH else getMessage("en", "server-messed-up-motd-too-long").format(constants.SERVER_MAX_TEMPLATE_LENGTH, len(motd))
except ValueError:
return getMessage("en", "server-messed-up-motd-unescaped-placeholders")
elif oldClient:
return getMessage("en", "new-syncplay-available-motd-message").format(clientVersion)
else:
return ""
def addWatcher(self, watcherProtocol, username, roomName, roomPassword):
username = self._roomManager.findFreeUsername(username)
watcher = Watcher(self, watcherProtocol, username)
self.setWatcherRoom(watcher, roomName)
def setWatcherRoom(self, watcher, roomName):
self._roomManager.moveWatcher(watcher, roomName)
self.sendJoinMessage(watcher)
def removeWatcher(self, watcher):
self.sendLeftMessage(watcher)
self._roomManager.removeWatcher(watcher)
def sendLeftMessage(self, watcher):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"left": True})
self._roomManager.broadcast(watcher, l)
def sendJoinMessage(self, watcher):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"joined": True})
self._roomManager.broadcast(watcher, l)
def sendFileUpdate(self, watcher, file_):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), watcher.getFile(), None)
self._roomManager.broadcast(watcher, l)
def forcePositionUpdate(self, room, watcher, doSeek):
room = watcher.getRoom()
paused, position = room.isPaused(), watcher.getPosition()
setBy = watcher
l = lambda w: w.sendState(position, paused, doSeek, setBy, True)
self._roomManager.broadcastRoom(watcher, l)
def getAllWatchersForUser(self, forUser):
return self._roomManager.getAllWatchersForUser(forUser)
class RoomManager(object):
def __init__(self):
self._rooms = {}
def broadcastRoom(self, sender, whatLambda):
room = sender.getRoom()
if room and room.getName() in self._rooms:
for receiver in room.getWatchers():
whatLambda(receiver)
def broadcast(self, sender, whatLambda):
for room in self._rooms.itervalues():
for receiver in room.getWatchers():
whatLambda(receiver)
def getAllWatchersForUser(self, sender):
watchers = []
for room in self._rooms.itervalues():
for watcher in room.getWatchers():
watchers.append(watcher)
return watchers
def moveWatcher(self, watcher, roomName):
self.removeWatcher(watcher)
room = self._getRoom(roomName)
room.addWatcher(watcher)
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None)
self.broadcast(watcher, l)
def removeWatcher(self, watcher):
oldRoom = watcher.getRoom()
if(oldRoom):
oldRoom.removeWatcher(watcher)
self._deleteRoomIfEmpty(oldRoom)
def _getRoom(self, roomName):
if roomName in self._rooms:
return self._rooms[roomName]
else:
room = Room(roomName)
self._rooms[roomName] = room
return room
def _deleteRoomIfEmpty(self, room):
if room.isEmpty() and room.getName() in self._rooms:
del self._rooms[room.getName()]
def findFreeUsername(self, username):
allnames = []
for room in self._rooms.itervalues():
for watcher in room.getWatchers():
allnames.append(watcher.getName().lower())
while username.lower() in allnames:
username += '_'
return username
class PublicRoomManager(RoomManager):
def broadcast(self, sender, what):
self.broadcastRoom(sender, what)
def getAllWatchersForUser(self, sender):
room = sender.getRoom().getWatchers()
def moveWatcher(self, watcher, room):
oldRoom = watcher.room
l = lambda w: w.sendSetting(watcher.getName(), oldRoom, None, {"left": True})
self.broadcast(watcher, l)
RoomManager.watcherSetRoom(self, watcher, room)
watcher.setFile(watcher.getFile())
class Room(object):
STATE_PAUSED = 0
STATE_PLAYING = 1
def __init__(self, name):
self._name = name
self._watchers = {}
self._playState = self.STATE_PAUSED
self._setBy = None
def __str__(self, *args, **kwargs):
return self.getName()
def getName(self):
return self._name
def getPosition(self):
watcher = min(self._watchers.values())
self._setBy = watcher
return watcher.getPosition()
def setPaused(self, paused=STATE_PAUSED, setBy=None):
self._playState = paused
self._setBy = setBy
def isPlaying(self):
return self._playState == self.STATE_PLAYING
def isPaused(self):
return self._playState == self.STATE_PAUSED
def getWatchers(self):
return self._watchers.values()
def addWatcher(self, watcher):
self._watchers[watcher.getName()] = watcher
watcher.setRoom(self)
def removeWatcher(self, watcher):
if(watcher.getName() not in self._watchers):
return
del self._watchers[watcher.getName()]
watcher.setRoom(None)
def isEmpty(self):
return not bool(self._watchers)
def getSetBy(self):
return self._setBy
class Watcher(object):
def __init__(self, server, connector, name):
self._server = server
self._connector = connector
self._name = name
self._room = None
self._file = None
self._position = None
self._lastUpdatedOn = time.time()
self._sendStateTimer = None
self._connector.setWatcher(self)
reactor.callLater(0.1, self._scheduleSendState)
def setFile(self, file):
self._file = file
self._server.sendFileUpdate(self, file)
def setRoom(self, room):
self._room = room
if room is None:
self._deactivateStateTimer()
else:
self._resetStateTimer()
self._askForStateUpdate(True, True)
def getRoom(self):
return self._room
def getName(self):
return self._name
def getFile(self):
return self._file
def getPosition(self):
if self._position is None:
return None
if self._room.isPlaying():
timePassedSinceSet = time.time() - self._lastUpdatedOn
else:
timePassedSinceSet = 0
return self._position + timePassedSinceSet
def sendSetting(self, username, roomName, file_, event):
self._connector.sendUserSetting(username, roomName, file_, event)
def __lt__(self, b):
if self.getPosition() is None:
return False
if b.getPosition is None:
return True
return self.getPosition() < b.getPosition()
def _scheduleSendState(self):
self._sendStateTimer = task.LoopingCall(self._askForStateUpdate)
self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL, True)
def _askForStateUpdate(self, doSeek=False, forcedUpdate=False):
self._server.sendState(self, doSeek, forcedUpdate)
def _resetStateTimer(self):
if self._sendStateTimer:
if self._sendStateTimer.running:
self._sendStateTimer.stop()
self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL)
def _deactivateStateTimer(self):
if(self._sendStateTimer and self._sendStateTimer.running):
self._sendStateTimer.stop()
def sendState(self, position, paused, doSeek, setBy, forcedUpdate):
self._connector.sendState(position, paused, doSeek, setBy, forcedUpdate)
if time.time() - self._lastUpdatedOn > constants.PROTOCOL_TIMEOUT:
self._server.removeWatcher(self)
self._connector.drop()
def __hasPauseChanged(self, paused):
return self._room.isPaused() and not paused or not self._room.isPaused() and paused
def updateState(self, position, paused, doSeek, messageAge):
if(self._file):
oldPosition = self.getPosition()
pauseChanged = False
if(paused is not None):
pauseChanged = self.__hasPauseChanged(paused)
if pauseChanged:
self.getRoom().setPaused(Room.STATE_PAUSED if paused else Room.STATE_PLAYING, self)
if(position is not None):
if(not paused):
position += messageAge
self._position = position
self._lastUpdatedOn = time.time()
if(doSeek or pauseChanged):
self._server.forcePositionUpdate(self._room, self, doSeek)
class ConfigurationGetter(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", "server-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", "server-password-argument"))
self._argparser.add_argument('--isolate-rooms', action='store_true', help=getMessage("en", "server-isolate-room-argument"))
self._argparser.add_argument('--motd-file', metavar='file', type=str, nargs='?', help=getMessage("en", "server-motd-argument"))