diff --git a/syncplay/constants.py b/syncplay/constants.py index 9cad6c0..3b04d2d 100755 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -53,6 +53,7 @@ DIFFERENT_DURATION_THRESHOLD = 2.5 PROTOCOL_TIMEOUT = 12.5 RECONNECT_RETRIES = 999 SERVER_STATE_INTERVAL = 1 +SERVER_STATS_SNAPSHOT_INTERVAL = 3600 WARNING_OSD_MESSAGES_LOOP_INTERVAL = 1 AUTOPLAY_DELAY = 3.0 DO_NOT_RESET_POSITION_THRESHOLD = 1.0 diff --git a/syncplay/messages_de.py b/syncplay/messages_de.py index 1dcab6d..318f0a3 100755 --- a/syncplay/messages_de.py +++ b/syncplay/messages_de.py @@ -434,7 +434,8 @@ de = { "server-motd-argument": "Pfad zur Datei, von der die Nachricht des Tages geladen wird", "server-chat-argument": "Should chat be disabled?", # TODO: Translate "server-chat-maxchars-argument": "Maximum number of characters in a chat message (default is {})", # TODO: Translate - "server-maxusernamelength-argument": "Maximum number of charactrs in a username (default is {})", # TODO: Translate + "server-maxusernamelength-argument": "Maximum number of characters in a username (default is {})", # TODO: Translate + "server-stats-db-file-argument": "Enable server stats using the SQLite db file provided", # TODO: Translate "server-messed-up-motd-unescaped-placeholders": "Die Nachricht des Tages hat unmaskierte Platzhalter. Alle $-Zeichen sollten verdoppelt werden ($$).", "server-messed-up-motd-too-long": "Die Nachricht des Tages ist zu lang - Maximal {} Zeichen, aktuell {}.", diff --git a/syncplay/messages_en.py b/syncplay/messages_en.py index f22015b..267cbce 100755 --- a/syncplay/messages_en.py +++ b/syncplay/messages_en.py @@ -437,8 +437,9 @@ en = { "server-disable-ready-argument": "disable readiness feature", "server-motd-argument": "path to file from which motd will be fetched", "server-chat-argument": "Should chat be disabled?", - "server-chat-maxchars-argument": "Maximum number of characters in a chat message (default is {})", # Default number of characters - "server-maxusernamelength-argument": "Maximum number of charactrs in a username (default is {})", + "server-chat-maxchars-argument": "Maximum number of characters in a chat message (default is {})", # Default number of characters + "server-maxusernamelength-argument": "Maximum number of characters in a username (default is {})", + "server-stats-db-file-argument": "Enable server stats using the SQLite db file provided", "server-messed-up-motd-unescaped-placeholders": "Message of the Day has unescaped placeholders. All $ signs should be doubled ($$).", "server-messed-up-motd-too-long": "Message of the Day is too long - maximum of {} chars, {} given.", diff --git a/syncplay/messages_it.py b/syncplay/messages_it.py index 8904733..0b804aa 100755 --- a/syncplay/messages_it.py +++ b/syncplay/messages_it.py @@ -437,8 +437,9 @@ it = { "server-disable-ready-argument": "disabilita la funzionalità \"pronto\"", "server-motd-argument": "percorso del file da cui verrà letto il messaggio del giorno", "server-chat-argument": "abilita o disabilita la chat", - "server-chat-maxchars-argument": "Numero massimo di caratteri in un messaggio di chat (default è {})", # Default number of characters - "server-maxusernamelength-argument": "Maximum number of charactrs in a username (default is {})", # TODO: Translate + "server-chat-maxchars-argument": "Numero massimo di caratteri in un messaggio di chat (default è {})", # Default number of characters + "server-maxusernamelength-argument": "Numero massimo di caratteri in un nome utente (default è {})", + "server-stats-db-file-argument": "Abilita la raccolta dei dati statistici nel file SQLite indicato", "server-messed-up-motd-unescaped-placeholders": "Il messaggio del giorno ha dei caratteri non 'escaped'. Tutti i simboli $ devono essere doppi ($$).", "server-messed-up-motd-too-long": "Il messaggio del giorno è troppo lungo - numero massimo di caratteri è {}, {} trovati.", diff --git a/syncplay/messages_ru.py b/syncplay/messages_ru.py index 9daecb6..6c31d62 100755 --- a/syncplay/messages_ru.py +++ b/syncplay/messages_ru.py @@ -440,9 +440,10 @@ ru = { "server-motd-argument": "путь к файлу, из которого будет извлекаться MOTD-сообщение", "server-chat-argument": "Should chat be disabled?", # TODO: Translate "server-chat-maxchars-argument": "Maximum number of characters in a chat message (default is {})", # TODO: Translate - "server-maxusernamelength-argument": "Maximum number of charactrs in a username (default is {})", # TODO: Translate - "server-messed-up-motd-unescaped-placeholders": "MOTD-сообщение содержит неэкранированные спец.символы. Все знаки $ должны быть продублированы ($$).", - "server-messed-up-motd-too-long": "MOTD-сообщение слишком длинное: максимальная длина - {} символ(ов), текущая длина - {} символ(ов).", + "server-maxusernamelength-argument": "Maximum number of characters in a username (default is {})", # TODO: Translate + "server-stats-db-file-argument": "Enable server stats using the SQLite db file provided", # TODO: Translate + "server-messed-up-motd-unescaped-placeholders" : "MOTD-сообщение содержит неэкранированные спец.символы. Все знаки $ должны быть продублированы ($$).", + "server-messed-up-motd-too-long" : "MOTD-сообщение слишком длинное: максимальная длина - {} символ(ов), текущая длина - {} символ(ов).", # Server errors "unknown-command-server-error": "Неизвестная команда: {}", # message diff --git a/syncplay/server.py b/syncplay/server.py index 4b8cdaa..af06a50 100755 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -1,4 +1,3 @@ - import argparse import codecs import hashlib @@ -7,6 +6,7 @@ import random import time from string import Template +from twisted.enterprise import adbapi from twisted.internet import task, reactor from twisted.internet.protocol import Factory @@ -18,11 +18,12 @@ from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomString class SyncFactory(Factory): - def __init__(self, password='', motdFilePath=None, isolateRooms=False, salt=None, + def __init__(self, port='', password='', motdFilePath=None, isolateRooms=False, salt=None, disableReady=False, disableChat=False, maxChatMessageLength=constants.MAX_CHAT_MESSAGE_LENGTH, - maxUsernameLength=constants.MAX_USERNAME_LENGTH): + maxUsernameLength=constants.MAX_USERNAME_LENGTH, statsDbFile=None): self.isolateRooms = isolateRooms print(getMessage("welcome-server-notification").format(syncplay.version)) + self.port = port if password: password = hashlib.md5(password).hexdigest() self.password = password @@ -39,6 +40,13 @@ class SyncFactory(Factory): self._roomManager = RoomManager() else: self._roomManager = PublicRoomManager() + if statsDbFile is not None: + self._statsDbHandle = DBManager(statsDbFile) + self._statsRecorder = StatsRecorder(self._statsDbHandle, self._roomManager) + statsDelay = 5*(int(self.port)%10 + 1) + self._statsRecorder.startRecorder(statsDelay) + else: + self._statsDbHandle = None def buildProtocol(self, addr): return SyncServerProtocol(self) @@ -186,6 +194,55 @@ class SyncFactory(Factory): watcher.setPlaylistIndex(room.getName(), room.getPlaylistIndex()) +class StatsRecorder(object): + def __init__(self, dbHandle, roomManager): + self._dbHandle = dbHandle + self._roomManagerHandle = roomManager + + def startRecorder(self, delay): + try: + self._dbHandle.connect() + reactor.callLater(delay, self._scheduleClientSnapshot) + except: + print("--- Error in initializing the stats database. Server Stats not enabled. ---") + + def _scheduleClientSnapshot(self): + self._clientSnapshotTimer = task.LoopingCall(self._runClientSnapshot) + self._clientSnapshotTimer.start(constants.SERVER_STATS_SNAPSHOT_INTERVAL) + + def _runClientSnapshot(self): + try: + snapshotTime = int(time.time()) + rooms = self._roomManagerHandle.exportRooms() + for room in rooms.values(): + for watcher in room.getWatchers(): + self._dbHandle.addVersionLog(snapshotTime, watcher.getVersion()) + except: + pass + + +class DBManager(object): + def __init__(self, dbpath): + self._dbPath = dbpath + self._connection = None + + def __del__(self): + if self._connection is not None: + self._connection.close() + + def connect(self): + self._connection = adbapi.ConnectionPool("sqlite3", self._dbPath, check_same_thread=False) + self._createSchema() + + def _createSchema(self): + initQuery = 'create table if not exists clients_snapshots (snapshot_time integer, version string)' + self._connection.runQuery(initQuery) + + def addVersionLog(self, timestamp, version): + content = (timestamp, version, ) + self._connection.runQuery("INSERT INTO clients_snapshots VALUES (?, ?)", content) + + class RoomManager(object): def __init__(self): self._rooms = {} @@ -244,6 +301,9 @@ class RoomManager(object): while username.lower() in allnames: username += '_' return username + + def exportRooms(self): + return self._rooms class PublicRoomManager(RoomManager): @@ -562,3 +622,4 @@ class ConfigurationGetter(object): self._argparser.add_argument('--motd-file', metavar='file', type=str, nargs='?', help=getMessage("server-motd-argument")) self._argparser.add_argument('--max-chat-message-length', metavar='maxChatMessageLength', type=int, nargs='?', help=getMessage("server-chat-maxchars-argument").format(constants.MAX_CHAT_MESSAGE_LENGTH)) self._argparser.add_argument('--max-username-length', metavar='maxUsernameLength', type=int, nargs='?', help=getMessage("server-maxusernamelength-argument").format(constants.MAX_USERNAME_LENGTH)) + self._argparser.add_argument('--stats-db-file', metavar='file', type=str, nargs='?', help=getMessage("server-stats-db-file-argument")) diff --git a/syncplayServer.py b/syncplayServer.py index 3aa9866..28cfd1b 100755 --- a/syncplayServer.py +++ b/syncplayServer.py @@ -22,11 +22,14 @@ if __name__ == '__main__': reactor.listenTCP( int(args.port), SyncFactory( + args.port, args.password, args.motd_file, args.isolate_rooms, args.salt, args.disable_ready, args.disable_chat, - args.max_chat_message_length)) + args.max_chat_message_length, + args.max_username_length, + args.stats_db_file)) reactor.run()