Re-work room persistence (#487)

* Initial server-side room persistence implementation (#434)

* Added room permanence option to server

* Fixed error if roomsDirPath is None

* Sanitized filenames

* Delete room file on empty playlist

* Fixed position not saving when leaving and seeking, and position not loading after a restart

* Decoupled permanence check

* Added --rooms-timer option that limits the max lifespan of persistent rooms

* Assigned filename to variable to deduplicate calculation

* Freed up room when loading unwanted room from file

Co-authored-by: Assistant <assistant.moetron@gmail.com>

* Use sqlite for persistent/permanent rooms (#434)

* Add -temp rooms and persistent room notices

* Use system loanguage for servers

* Make room temp check case-insensitive

* Improve temp room check

* Fix controlled rooms

* Refactor how non-macOS/frozen initialLanguage is fixed

* Fix persistent room list

* Don't send dummy users to new console clients (#434)

* Allow hiding of empty persistent rooms (#434)

* List current rooms in join list

Co-authored-by: Assistant <assistant.moetron@gmail.com>
This commit is contained in:
Etoh 2021-12-09 16:23:57 +00:00 committed by GitHub
parent 148198b212
commit c0e8652c8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 321 additions and 99 deletions

View File

@ -78,12 +78,13 @@ Random string used to generate managed room passwords.
Path to a file from which motd (Message Of The Day) will be read. Path to a file from which motd (Message Of The Day) will be read.
.TP .TP
.B \-\-rooms\-dir [directory] .B \-\-rooms\-db-file [directory]
Path to a directory from where room data will be written to and read from. This will enable rooms to persist without watchers and through restarts. Will not work if using \fB\-\-isolate\-rooms\fP. Enables room persistence. Path is to where a database file should be loaded/create where room data will be written to and read from. This will enable rooms to persist without watchers and through restarts. Will not work if using \fB\-\-isolate\-rooms\fP.
.TP .TP
.B \-\-rooms\-timer [directory] .B \-\-permanent\-rooms-file [directory]
Requires \fB\-\-rooms\-timer\fP. Time in seconds that rooms will persist without users. \fB0\fP disables the timer, meaning rooms persist permanently. Specifies a list of rooms that will still be listed even if their playlist is empty. Path is to where a text file with one room per line. This will require persistent rooms to be enabled.
.TP .TP
.B \-\-max\-chat\-message\-length [maxChatMessageLength] .B \-\-max\-chat\-message\-length [maxChatMessageLength]

View File

@ -640,6 +640,7 @@ class SyncplayClient(object):
"chat": utils.meetsMinVersion(self.serverVersion, constants.CHAT_MIN_VERSION), "chat": utils.meetsMinVersion(self.serverVersion, constants.CHAT_MIN_VERSION),
"readiness": utils.meetsMinVersion(self.serverVersion, constants.USER_READY_MIN_VERSION), "readiness": utils.meetsMinVersion(self.serverVersion, constants.USER_READY_MIN_VERSION),
"managedRooms": utils.meetsMinVersion(self.serverVersion, constants.CONTROLLED_ROOMS_MIN_VERSION), "managedRooms": utils.meetsMinVersion(self.serverVersion, constants.CONTROLLED_ROOMS_MIN_VERSION),
"persistentRooms": False,
"maxChatMessageLength": constants.FALLBACK_MAX_CHAT_MESSAGE_LENGTH, "maxChatMessageLength": constants.FALLBACK_MAX_CHAT_MESSAGE_LENGTH,
"maxUsernameLength": constants.FALLBACK_MAX_USERNAME_LENGTH, "maxUsernameLength": constants.FALLBACK_MAX_USERNAME_LENGTH,
"maxRoomNameLength": constants.FALLBACK_MAX_ROOM_NAME_LENGTH, "maxRoomNameLength": constants.FALLBACK_MAX_ROOM_NAME_LENGTH,
@ -706,11 +707,13 @@ class SyncplayClient(object):
# Can change during runtime: # Can change during runtime:
features["sharedPlaylists"] = self.sharedPlaylistIsEnabled() # Can change during runtime features["sharedPlaylists"] = self.sharedPlaylistIsEnabled() # Can change during runtime
features["chat"] = self.chatIsEnabled() # Can change during runtime features["chat"] = self.chatIsEnabled() # Can change during runtime
features["uiMode"] = self.ui.getUIMode()
# Static for this version/release of Syncplay: # Static for this version/release of Syncplay:
features["featureList"] = True features["featureList"] = True
features["readiness"] = True features["readiness"] = True
features["managedRooms"] = True features["managedRooms"] = True
features["persistentRooms"] = True
return features return features
@ -1593,6 +1596,9 @@ class UiManager(object):
self.lastAlertOSDEndTime = None self.lastAlertOSDEndTime = None
self.lastError = "" self.lastError = ""
def getUIMode(self):
return self.__ui.uiMode
def addFileToPlaylist(self, newPlaylistItem): def addFileToPlaylist(self, newPlaylistItem):
self.__ui.addFileToPlaylist(newPlaylistItem) self.__ui.addFileToPlaylist(newPlaylistItem)

View File

@ -349,3 +349,8 @@ DEFAULT_TRUSTED_DOMAINS = ["youtube.com", "youtu.be"]
TRUSTABLE_WEB_PROTOCOLS = ["http", "https"] TRUSTABLE_WEB_PROTOCOLS = ["http", "https"]
PRIVATE_FILE_FIELDS = ["path"] PRIVATE_FILE_FIELDS = ["path"]
CONSOLE_UI_MODE = "CLI"
GRAPHICAL_UI_MODE = "GUI"
UNKNOWN_UI_MODE = "Unknown"
FALLBACK_ASSUMED_UI_MODE = GRAPHICAL_UI_MODE

View File

@ -34,8 +34,8 @@ def main():
args.port, args.port,
args.password, args.password,
args.motd_file, args.motd_file,
args.rooms_dir, args.rooms_db_file,
args.rooms_timer, args.permanent_rooms_file,
args.isolate_rooms, args.isolate_rooms,
args.salt, args.salt,
args.disable_ready, args.disable_ready,

View File

@ -64,7 +64,7 @@ def getInitialLanguage():
try: try:
import sys import sys
frozen = getattr(sys, 'frozen', '') frozen = getattr(sys, 'frozen', '')
if frozen in 'macosx_app': if frozen and frozen in 'macosx_app':
from PySide2.QtCore import QLocale from PySide2.QtCore import QLocale
initialLanguage = QLocale.system().uiLanguages()[0].split('-')[0] initialLanguage = QLocale.system().uiLanguages()[0].split('-')[0]
else: else:

View File

@ -288,6 +288,7 @@ de = {
"autoplay-menu-label": "Auto-Play-Knopf anzeigen", "autoplay-menu-label": "Auto-Play-Knopf anzeigen",
"autoplay-guipushbuttonlabel": "Automatisch abspielen wenn alle bereit sind", "autoplay-guipushbuttonlabel": "Automatisch abspielen wenn alle bereit sind",
"autoplay-minimum-label": "Minimum an Nutzern:", "autoplay-minimum-label": "Minimum an Nutzern:",
"hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate
"sendmessage-label": "Senden", "sendmessage-label": "Senden",
@ -458,6 +459,7 @@ de = {
# Server messages to client # Server messages to client
"new-syncplay-available-motd-message": "Du nutzt Syncplay Version {}, aber es gibt eine neuere Version auf https://syncplay.pl", # ClientVersion "new-syncplay-available-motd-message": "Du nutzt Syncplay Version {}, aber es gibt eine neuere Version auf https://syncplay.pl", # ClientVersion
"persistent-rooms-notice": "NOTICE: This server uses persistent rooms, which means that the playlist information is stored between playback sessions. If you want to create a room where information is not saved then put -temp at the end of the room name.", # TO DO: Translate - NOTE: Do not translate the word -temp
# Server notifications # Server notifications
"welcome-server-notification": "Willkommen zum Syncplay-Server, v. {0}", # version "welcome-server-notification": "Willkommen zum Syncplay-Server, v. {0}", # version
@ -474,8 +476,8 @@ de = {
"server-salt-argument": "zufällige Zeichenkette, die zur Erstellung von Passwörtern verwendet wird", "server-salt-argument": "zufällige Zeichenkette, die zur Erstellung von Passwörtern verwendet wird",
"server-disable-ready-argument": "Bereitschaftsfeature deaktivieren", "server-disable-ready-argument": "Bereitschaftsfeature deaktivieren",
"server-motd-argument": "Pfad zur Datei, von der die Nachricht des Tages geladen wird", "server-motd-argument": "Pfad zur Datei, von der die Nachricht des Tages geladen wird",
"server-rooms-argument": "path to directory to store/fetch room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate "server-rooms-argument": "path to database file to use and/or create to store persistent room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate
"server-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # TODO: Translate "server-permanent-rooms-argument": "path to file which lists permenant rooms that will be listed even if the room is empty (in the form of a text file which lists one room per line) - requires persistent rooms to be enabled", # TODO: Translate
"server-chat-argument": "Soll Chat deaktiviert werden?", "server-chat-argument": "Soll Chat deaktiviert werden?",
"server-chat-maxchars-argument": "Maximale Zeichenzahl in einer Chatnachricht (Standard ist {})", "server-chat-maxchars-argument": "Maximale Zeichenzahl in einer Chatnachricht (Standard ist {})",
"server-maxusernamelength-argument": "Maximale Zeichenzahl in einem Benutzernamen (Standard ist {})", "server-maxusernamelength-argument": "Maximale Zeichenzahl in einem Benutzernamen (Standard ist {})",

View File

@ -292,6 +292,7 @@ en = {
"autoplay-menu-label": "Show auto-play button", "autoplay-menu-label": "Show auto-play button",
"autoplay-guipushbuttonlabel": "Play when all ready", "autoplay-guipushbuttonlabel": "Play when all ready",
"autoplay-minimum-label": "Min users:", "autoplay-minimum-label": "Min users:",
"hideemptyrooms-menu-label": "Hide empty persistent rooms",
"sendmessage-label": "Send", "sendmessage-label": "Send",
@ -458,6 +459,7 @@ en = {
# Server messages to client # Server messages to client
"new-syncplay-available-motd-message": "You are using Syncplay {} but a newer version is available from https://syncplay.pl", # ClientVersion "new-syncplay-available-motd-message": "You are using Syncplay {} but a newer version is available from https://syncplay.pl", # ClientVersion
"persistent-rooms-notice": "NOTICE: This server uses persistent rooms, which means that the playlist information is stored between playback sessions. If you want to create a room where information is not saved then put -temp at the end of the room name.", # NOTE: Do not translate the word -temp
# Server notifications # Server notifications
"welcome-server-notification": "Welcome to Syncplay server, ver. {0}", # version "welcome-server-notification": "Welcome to Syncplay server, ver. {0}", # version
@ -475,8 +477,8 @@ en = {
"server-salt-argument": "random string used to generate managed room passwords", "server-salt-argument": "random string used to generate managed room passwords",
"server-disable-ready-argument": "disable readiness feature", "server-disable-ready-argument": "disable readiness feature",
"server-motd-argument": "path to file from which motd will be fetched", "server-motd-argument": "path to file from which motd will be fetched",
"server-rooms-argument": "path to directory to store/fetch room data. Enables rooms to persist without watchers and through restarts", "server-rooms-argument": "path to database file to use and/or create to store persistent room data. Enables rooms to persist without watchers and through restarts",
"server-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", "server-permanent-rooms-argument": "path to file which lists permenant rooms that will be listed even if the room is empty (in the form of a text file which lists one room per line) - requires persistent rooms to be enabled",
"server-chat-argument": "Should chat be disabled?", "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-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-maxusernamelength-argument": "Maximum number of characters in a username (default is {})",

View File

@ -291,6 +291,7 @@ es = {
"autoplay-menu-label": "Mostrar botón de auto-reproducción", "autoplay-menu-label": "Mostrar botón de auto-reproducción",
"autoplay-guipushbuttonlabel": "Reproducir cuando todos estén listos", "autoplay-guipushbuttonlabel": "Reproducir cuando todos estén listos",
"autoplay-minimum-label": "Mín. de usuarios:", "autoplay-minimum-label": "Mín. de usuarios:",
"hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate
"sendmessage-label": "Enviar", "sendmessage-label": "Enviar",
@ -457,6 +458,7 @@ es = {
# Server messages to client # Server messages to client
"new-syncplay-available-motd-message": "Estás usando Syncplay {} pero hay una versión más nueva disponible en https://syncplay.pl", # ClientVersion "new-syncplay-available-motd-message": "Estás usando Syncplay {} pero hay una versión más nueva disponible en https://syncplay.pl", # ClientVersion
"persistent-rooms-notice": "NOTICE: This server uses persistent rooms, which means that the playlist information is stored between playback sessions. If you want to create a room where information is not saved then put -temp at the end of the room name.", # TO DO: Translate - NOTE: Do not translate the word -temp
# Server notifications # Server notifications
"welcome-server-notification": "Bienvenido al servidor de Syncplay, ver. {0}", # version "welcome-server-notification": "Bienvenido al servidor de Syncplay, ver. {0}", # version
@ -474,8 +476,8 @@ es = {
"server-salt-argument": "cadena aleatoria utilizada para generar contraseñas de salas administradas", "server-salt-argument": "cadena aleatoria utilizada para generar contraseñas de salas administradas",
"server-disable-ready-argument": "deshabilitar la función de preparación", "server-disable-ready-argument": "deshabilitar la función de preparación",
"server-motd-argument": "ruta al archivo del cual se obtendrá el texto motd", "server-motd-argument": "ruta al archivo del cual se obtendrá el texto motd",
"server-rooms-argument": "path to directory to store/fetch room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate "server-rooms-argument": "path to database file to use and/or create to store persistent room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate
"server-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # TODO: Translate "server-permanent-rooms-argument": "path to file which lists permenant rooms that will be listed even if the room is empty (in the form of a text file which lists one room per line) - requires persistent rooms to be enabled", # TODO: Translate
"server-chat-argument": "¿Debería deshabilitarse el chat?", "server-chat-argument": "¿Debería deshabilitarse el chat?",
"server-chat-maxchars-argument": "Número máximo de caracteres en un mensaje de chat (el valor predeterminado es {})", # Default number of characters "server-chat-maxchars-argument": "Número máximo de caracteres en un mensaje de chat (el valor predeterminado es {})", # Default number of characters
"server-maxusernamelength-argument": "Número máximo de caracteres para el nombre de usuario (el valor predeterminado es {})", "server-maxusernamelength-argument": "Número máximo de caracteres para el nombre de usuario (el valor predeterminado es {})",

View File

@ -291,6 +291,7 @@ it = {
"autoplay-menu-label": "Mostra il tasto di riproduzione automatica", "autoplay-menu-label": "Mostra il tasto di riproduzione automatica",
"autoplay-guipushbuttonlabel": "Riproduci quando tutti sono pronti", "autoplay-guipushbuttonlabel": "Riproduci quando tutti sono pronti",
"autoplay-minimum-label": "Minimo utenti pronti:", "autoplay-minimum-label": "Minimo utenti pronti:",
"hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate
"sendmessage-label": "Invia", "sendmessage-label": "Invia",
@ -457,6 +458,7 @@ it = {
# Server messages to client # Server messages to client
"new-syncplay-available-motd-message": "Stai usando Syncplay {} ma una nuova versione è disponibile presso https://syncplay.pl", # ClientVersion "new-syncplay-available-motd-message": "Stai usando Syncplay {} ma una nuova versione è disponibile presso https://syncplay.pl", # ClientVersion
"persistent-rooms-notice": "NOTICE: This server uses persistent rooms, which means that the playlist information is stored between playback sessions. If you want to create a room where information is not saved then put -temp at the end of the room name.", # TO DO: Translate - NOTE: Do not translate the word -temp to create a room where information is not saved then put -temp at the end of the room name.", # TO DO: Translate
# Server notifications # Server notifications
"welcome-server-notification": "Benvenuto nel server Syncplay, ver. {0}", # version "welcome-server-notification": "Benvenuto nel server Syncplay, ver. {0}", # version
@ -474,8 +476,8 @@ it = {
"server-salt-argument": "usare stringhe casuali per generare le password delle stanze gestite", "server-salt-argument": "usare stringhe casuali per generare le password delle stanze gestite",
"server-disable-ready-argument": "disabilita la funzionalità \"pronto\"", "server-disable-ready-argument": "disabilita la funzionalità \"pronto\"",
"server-motd-argument": "percorso del file da cui verrà letto il messaggio del giorno", "server-motd-argument": "percorso del file da cui verrà letto il messaggio del giorno",
"server-rooms-argument": "path to directory to store/fetch room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate "server-rooms-argument": "path to database file to use and/or create to store persistent room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate
"server-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # TODO: Translate "server-permanent-rooms-argument": "path to file which lists permenant rooms that will be listed even if the room is empty (in the form of a text file which lists one room per line) - requires persistent rooms to be enabled", # TODO: Translate
"server-chat-argument": "abilita o disabilita la chat", "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-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-maxusernamelength-argument": "Numero massimo di caratteri in un nome utente (default è {})",

View File

@ -292,6 +292,7 @@ pt_BR = {
"autoplay-menu-label": "Mostrar botão de reprodução automática", "autoplay-menu-label": "Mostrar botão de reprodução automática",
"autoplay-guipushbuttonlabel": "Tocar quando todos estiverem prontos", "autoplay-guipushbuttonlabel": "Tocar quando todos estiverem prontos",
"autoplay-minimum-label": "Mín. de usuários:", "autoplay-minimum-label": "Mín. de usuários:",
"hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate
"sendmessage-label": "Enviar", "sendmessage-label": "Enviar",
@ -458,6 +459,7 @@ pt_BR = {
# Server messages to client # Server messages to client
"new-syncplay-available-motd-message": "Você está usando o Syncplay {}, mas uma versão mais nova está disponível em https://syncplay.pl", # ClientVersion "new-syncplay-available-motd-message": "Você está usando o Syncplay {}, mas uma versão mais nova está disponível em https://syncplay.pl", # ClientVersion
"persistent-rooms-notice": "NOTICE: This server uses persistent rooms, which means that the playlist information is stored between playback sessions. If you want to create a room where information is not saved then put -temp at the end of the room name.", # TO DO: Translate - NOTE: Do not translate the word -temp
# Server notifications # Server notifications
"welcome-server-notification": "Seja bem-vindo ao servidor de Syncplay, versão {0}", # version "welcome-server-notification": "Seja bem-vindo ao servidor de Syncplay, versão {0}", # version
@ -475,8 +477,8 @@ pt_BR = {
"server-salt-argument": "string aleatória utilizada para gerar senhas de salas gerenciadas", "server-salt-argument": "string aleatória utilizada para gerar senhas de salas gerenciadas",
"server-disable-ready-argument": "desativar recurso de prontidão", "server-disable-ready-argument": "desativar recurso de prontidão",
"server-motd-argument": "caminho para o arquivo o qual o motd será obtido", "server-motd-argument": "caminho para o arquivo o qual o motd será obtido",
"server-rooms-argument": "path to directory to store/fetch room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate "server-rooms-argument": "path to database file to use and/or create to store persistent room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate
"server-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # TODO: Translate "server-permanent-rooms-argument": "path to file which lists permenant rooms that will be listed even if the room is empty (in the form of a text file which lists one room per line) - requires persistent rooms to be enabled", # TODO: Translate
"server-chat-argument": "O chat deve ser desativado?", "server-chat-argument": "O chat deve ser desativado?",
"server-chat-maxchars-argument": "Número máximo de caracteres numa mensagem do chat (o padrão é {})", # Default number of characters "server-chat-maxchars-argument": "Número máximo de caracteres numa mensagem do chat (o padrão é {})", # Default number of characters
"server-maxusernamelength-argument": "Número máximos de caracteres num nome de usuário (o padrão é {})", "server-maxusernamelength-argument": "Número máximos de caracteres num nome de usuário (o padrão é {})",

View File

@ -290,6 +290,7 @@ pt_PT = {
"autoplay-menu-label": "Mostrar botão de reprodução automática", "autoplay-menu-label": "Mostrar botão de reprodução automática",
"autoplay-guipushbuttonlabel": "Tocar quando todos estiverem prontos", "autoplay-guipushbuttonlabel": "Tocar quando todos estiverem prontos",
"autoplay-minimum-label": "Mín. de utilizadores:", "autoplay-minimum-label": "Mín. de utilizadores:",
"hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate
"sendmessage-label": "Enviar", "sendmessage-label": "Enviar",
@ -457,6 +458,7 @@ pt_PT = {
# Server messages to client # Server messages to client
"new-syncplay-available-motd-message": "Você está usando o Syncplay {}, mas uma versão mais nova está disponível em https://syncplay.pl", # ClientVersion "new-syncplay-available-motd-message": "Você está usando o Syncplay {}, mas uma versão mais nova está disponível em https://syncplay.pl", # ClientVersion
"persistent-rooms-notice": "NOTICE: This server uses persistent rooms, which means that the playlist information is stored between playback sessions. If you want to create a room where information is not saved then put -temp at the end of the room name.", # TO DO: Translate - NOTE: Do not translate the word -temp
# Server notifications # Server notifications
"welcome-server-notification": "Seja bem-vindo ao servidor de Syncplay, versão {0}", # version "welcome-server-notification": "Seja bem-vindo ao servidor de Syncplay, versão {0}", # version
@ -474,8 +476,8 @@ pt_PT = {
"server-salt-argument": "string aleatória utilizada para gerar senhas de salas gerenciadas", "server-salt-argument": "string aleatória utilizada para gerar senhas de salas gerenciadas",
"server-disable-ready-argument": "desativar recurso de prontidão", "server-disable-ready-argument": "desativar recurso de prontidão",
"server-motd-argument": "caminho para o arquivo o qual o motd será obtido", "server-motd-argument": "caminho para o arquivo o qual o motd será obtido",
"server-rooms-argument": "path to directory to store/fetch room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate "server-rooms-argument": "path to database file to use and/or create to store persistent room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate
"server-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # TODO: Translate "server-permanent-rooms-argument": "path to file which lists permenant rooms that will be listed even if the room is empty (in the form of a text file which lists one room per line) - requires persistent rooms to be enabled", # TODO: Translate
"server-chat-argument": "O chat deve ser desativado?", "server-chat-argument": "O chat deve ser desativado?",
"server-chat-maxchars-argument": "Número máximo de caracteres numa mensagem do chat (o padrão é {})", # Default number of characters "server-chat-maxchars-argument": "Número máximo de caracteres numa mensagem do chat (o padrão é {})", # Default number of characters
"server-maxusernamelength-argument": "Número máximos de caracteres num nome de utilizador (o padrão é {})", "server-maxusernamelength-argument": "Número máximos de caracteres num nome de utilizador (o padrão é {})",

View File

@ -291,6 +291,7 @@ ru = {
"autoplay-menu-label": "Показывать кнопку &автовоспроизведения", "autoplay-menu-label": "Показывать кнопку &автовоспроизведения",
"autoplay-guipushbuttonlabel": "Стартовать, когда все будут готовы", "autoplay-guipushbuttonlabel": "Стартовать, когда все будут готовы",
"autoplay-minimum-label": "Минимум зрителей:", "autoplay-minimum-label": "Минимум зрителей:",
"hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate
"sendmessage-label": "Выслать", "sendmessage-label": "Выслать",
"ready-guipushbuttonlabel": "Я готов", "ready-guipushbuttonlabel": "Я готов",
@ -455,6 +456,7 @@ ru = {
# Server messages to client # Server messages to client
"new-syncplay-available-motd-message": "Вы используете Syncplay версии {}. Доступна более новая версия на https://syncplay.pl/", # ClientVersion "new-syncplay-available-motd-message": "Вы используете Syncplay версии {}. Доступна более новая версия на https://syncplay.pl/", # ClientVersion
"persistent-rooms-notice": "NOTICE: This server uses persistent rooms, which means that the playlist information is stored between playback sessions. If you want to create a room where information is not saved then put -temp at the end of the room name.", # TO DO: Translate - NOTE: Do not translate the word -temp
# Server notifications # Server notifications
"welcome-server-notification": "Добро пожаловать на сервер Syncplay версии {0}", # version "welcome-server-notification": "Добро пожаловать на сервер Syncplay версии {0}", # version
@ -471,8 +473,8 @@ ru = {
"server-salt-argument": "генерировать пароли к управляемым комнатам на основании указанной строки (соли)", "server-salt-argument": "генерировать пароли к управляемым комнатам на основании указанной строки (соли)",
"server-disable-ready-argument": "отключить статусы готов/не готов", "server-disable-ready-argument": "отключить статусы готов/не готов",
"server-motd-argument": "путь к файлу, из которого будет извлекаться MOTD-сообщение", "server-motd-argument": "путь к файлу, из которого будет извлекаться MOTD-сообщение",
"server-rooms-argument": "path to directory to store/fetch room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate "server-rooms-argument": "path to database file to use and/or create to store persistent room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate
"server-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # TODO: Translate "server-permanent-rooms-argument": "path to file which lists permenant rooms that will be listed even if the room is empty (in the form of a text file which lists one room per line) - requires persistent rooms to be enabled", # TODO: Translate
"server-chat-argument": "Должен ли чат быть отключён?", "server-chat-argument": "Должен ли чат быть отключён?",
"server-chat-maxchars-argument": "Максимальное число символов в сообщениях в чате (по умолчанию {})", "server-chat-maxchars-argument": "Максимальное число символов в сообщениях в чате (по умолчанию {})",
"server-maxusernamelength-argument": "Максимальное число символов в именах пользователей (по умолчанию {})", "server-maxusernamelength-argument": "Максимальное число символов в именах пользователей (по умолчанию {})",

View File

@ -292,6 +292,7 @@ tr = {
"autoplay-menu-label": "Otomatik oynat düğmesini göster", "autoplay-menu-label": "Otomatik oynat düğmesini göster",
"autoplay-guipushbuttonlabel": "Her şey hazır olduğunda oynat", "autoplay-guipushbuttonlabel": "Her şey hazır olduğunda oynat",
"autoplay-minimum-label": "Asgari kullanıcı:", "autoplay-minimum-label": "Asgari kullanıcı:",
"hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate
"sendmessage-label": "Gönder", "sendmessage-label": "Gönder",
@ -458,6 +459,7 @@ tr = {
# Server messages to client # Server messages to client
"new-syncplay-available-motd-message": "Syncplay {} kullanıyorsunuz ancak daha yeni bir sürüm https://syncplay.pl adresinde mevcut", # ClientVersion "new-syncplay-available-motd-message": "Syncplay {} kullanıyorsunuz ancak daha yeni bir sürüm https://syncplay.pl adresinde mevcut", # ClientVersion
"persistent-rooms-notice": "NOTICE: This server uses persistent rooms, which means that the playlist information is stored between playback sessions. If you want to create a room where information is not saved then put -temp at the end of the room name.", # TO DO: Translate - NOTE: Do not translate the word -temp
# Server notifications # Server notifications
"welcome-server-notification": "Syncplay sunucusuna hoş geldiniz, ver. {0}", # version "welcome-server-notification": "Syncplay sunucusuna hoş geldiniz, ver. {0}", # version
@ -475,8 +477,8 @@ tr = {
"server-salt-argument": "yönetilen oda şifreleri oluşturmak için kullanılan rastgele dize", "server-salt-argument": "yönetilen oda şifreleri oluşturmak için kullanılan rastgele dize",
"server-disable-ready-argument": "hazır olma özelliğini devre dışı bırak", "server-disable-ready-argument": "hazır olma özelliğini devre dışı bırak",
"server-motd-argument": "motd alınacak dosyanın yolu", "server-motd-argument": "motd alınacak dosyanın yolu",
"server-rooms-argument": "path to directory to store/fetch room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate "server-rooms-argument": "path to database file to use and/or create to store persistent room data. Enables rooms to persist without watchers and through restarts", # TODO: Translate
"server-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # TODO: Translate "server-permanent-rooms-argument": "path to file which lists permenant rooms that will be listed even if the room is empty (in the form of a text file which lists one room per line) - requires persistent rooms to be enabled", # TODO: Translate
"server-chat-argument": "Sohbet devre dışı bırakılmalı mı?", "server-chat-argument": "Sohbet devre dışı bırakılmalı mı?",
"server-chat-maxchars-argument": "Bir sohbet mesajındaki maksimum karakter sayısı (varsayılan: {})", # Default number of characters "server-chat-maxchars-argument": "Bir sohbet mesajındaki maksimum karakter sayısı (varsayılan: {})", # Default number of characters
"server-maxusernamelength-argument": "Bir kullanıcı adındaki maksimum karakter sayısı (varsayılan {})", "server-maxusernamelength-argument": "Bir kullanıcı adındaki maksimum karakter sayısı (varsayılan {})",

View File

@ -11,7 +11,7 @@ from twisted.python.versions import Version
from zope.interface.declarations import implementer from zope.interface.declarations import implementer
import syncplay import syncplay
from syncplay.constants import PING_MOVING_AVERAGE_WEIGHT, CONTROLLED_ROOMS_MIN_VERSION, USER_READY_MIN_VERSION, SHARED_PLAYLIST_MIN_VERSION, CHAT_MIN_VERSION from syncplay.constants import PING_MOVING_AVERAGE_WEIGHT, CONTROLLED_ROOMS_MIN_VERSION, USER_READY_MIN_VERSION, SHARED_PLAYLIST_MIN_VERSION, CHAT_MIN_VERSION, UNKNOWN_UI_MODE
from syncplay.messages import getMessage from syncplay.messages import getMessage
from syncplay.utils import meetsMinVersion from syncplay.utils import meetsMinVersion
@ -131,6 +131,10 @@ class SyncClientProtocol(JSONCommandProtocol):
self._client.setUsername(username) self._client.setUsername(username)
self._client.setRoom(roomName) self._client.setRoom(roomName)
self.logged = True self.logged = True
if self.persistentRoomWarning(featureList):
if len(motd) > 0:
motd += "\n\n"
motd += getMessage("persistent-rooms-notice")
if motd: if motd:
self._client.ui.showMessage(motd, True, True) self._client.ui.showMessage(motd, True, True)
self._client.ui.showMessage(getMessage("connected-successful-notification")) self._client.ui.showMessage(getMessage("connected-successful-notification"))
@ -138,6 +142,9 @@ class SyncClientProtocol(JSONCommandProtocol):
self._client.sendFile() self._client.sendFile()
self._client.setServerVersion(version, featureList) self._client.setServerVersion(version, featureList)
def persistentRoomWarning(self, serverFeatures):
return serverFeatures["persistentRooms"] if "persistentRooms" in serverFeatures else False
def sendHello(self): def sendHello(self):
hello = {} hello = {}
hello["username"] = self._client.getUsername() hello["username"] = self._client.getUsername()
@ -441,6 +448,8 @@ class SyncServerProtocol(JSONCommandProtocol):
self._features["featureList"] = False self._features["featureList"] = False
self._features["readiness"] = meetsMinVersion(self._version, USER_READY_MIN_VERSION) self._features["readiness"] = meetsMinVersion(self._version, USER_READY_MIN_VERSION)
self._features["managedRooms"] = meetsMinVersion(self._version, CONTROLLED_ROOMS_MIN_VERSION) self._features["managedRooms"] = meetsMinVersion(self._version, CONTROLLED_ROOMS_MIN_VERSION)
self._features["persistentRooms"] = False
self._features["uiMode"] = UNKNOWN_UI_MODE
return self._features return self._features
def isLogged(self): def isLogged(self):
@ -496,6 +505,11 @@ class SyncServerProtocol(JSONCommandProtocol):
self._logged = True self._logged = True
self.sendHello(version) self.sendHello(version)
def persistentRoomWarning(self, clientFeatures, serverFeatures):
serverPersistentRooms = serverFeatures["persistentRooms"]
clientPersistentRooms = clientFeatures["persistentRooms"] if "persistentRooms" in clientFeatures else False
return serverPersistentRooms and not clientPersistentRooms
@requireLogged @requireLogged
def handleChat(self, chatMessage): def handleChat(self, chatMessage):
if not self._factory.disableChat: if not self._factory.disableChat:
@ -520,8 +534,12 @@ class SyncServerProtocol(JSONCommandProtocol):
hello["room"] = {"name": room.getName()} hello["room"] = {"name": room.getName()}
hello["version"] = clientVersion # Used so 1.2.X client works on newer server hello["version"] = clientVersion # Used so 1.2.X client works on newer server
hello["realversion"] = syncplay.version hello["realversion"] = syncplay.version
hello["motd"] = self._factory.getMotd(userIp, username, room, clientVersion)
hello["features"] = self._factory.getFeatures() hello["features"] = self._factory.getFeatures()
hello["motd"] = self._factory.getMotd(userIp, username, room, clientVersion)
if self.persistentRoomWarning(clientFeatures=self._features, serverFeatures=hello["features"]):
if len(hello["motd"]) > 0:
hello["motd"] += "\n\n"
hello["motd"] += getMessage("persistent-rooms-notice")
self.sendMessage({"Hello": hello}) self.sendMessage({"Hello": hello})
@requireLogged @requireLogged
@ -617,11 +635,28 @@ class SyncServerProtocol(JSONCommandProtocol):
} }
userlist[room.getName()][watcher.getName()] = userFile userlist[room.getName()][watcher.getName()] = userFile
def _addDummyUserOnList(self, userlist, dummyRoom,dummyCount):
if dummyRoom not in userlist:
userlist[dummyRoom] = {}
dummyFile = {
"position": 0,
"file": {},
"controller": False,
"isReady": True,
"features": []
}
userlist[dummyRoom][" " * dummyCount] = dummyFile
def sendList(self): def sendList(self):
userlist = {} userlist = {}
watchers = self._factory.getAllWatchersForUser(self._watcher) watchers = self._factory.getAllWatchersForUser(self._watcher)
dummyCount = 0
for watcher in watchers: for watcher in watchers:
self._addUserOnList(userlist, watcher) self._addUserOnList(userlist, watcher)
if self._watcher.isGUIUser(self.getFeatures()):
for emptyRoom in self._factory.getEmptyPersistentRooms():
dummyCount += 1
self._addDummyUserOnList(userlist, emptyRoom, dummyCount)
self.sendMessage({"List": userlist}) self.sendMessage({"List": userlist})
@requireLogged @requireLogged

View File

@ -22,14 +22,14 @@ import syncplay
from syncplay import constants from syncplay import constants
from syncplay.messages import getMessage from syncplay.messages import getMessage
from syncplay.protocols import SyncServerProtocol from syncplay.protocols import SyncServerProtocol
from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomStringGenerator, meetsMinVersion, playlistIsValid, truncateText from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomStringGenerator, meetsMinVersion, playlistIsValid, truncateText, getListAsMultilineString, convertMultilineStringToList
class SyncFactory(Factory): class SyncFactory(Factory):
def __init__(self, port='', password='', motdFilePath=None, roomsDirPath=None, roomsTimer=31558149, isolateRooms=False, salt=None, def __init__(self, port='', password='', motdFilePath=None, roomsDbFile=None, permanentRoomsFile=None, isolateRooms=False, salt=None,
disableReady=False, disableChat=False, maxChatMessageLength=constants.MAX_CHAT_MESSAGE_LENGTH, disableReady=False, disableChat=False, maxChatMessageLength=constants.MAX_CHAT_MESSAGE_LENGTH,
maxUsernameLength=constants.MAX_USERNAME_LENGTH, statsDbFile=None, tlsCertPath=None): maxUsernameLength=constants.MAX_USERNAME_LENGTH, statsDbFile=None, tlsCertPath=None):
self.isolateRooms = isolateRooms self.isolateRooms = isolateRooms
syncplay.messages.setLanguage(syncplay.messages.getInitialLanguage())
print(getMessage("welcome-server-notification").format(syncplay.version)) print(getMessage("welcome-server-notification").format(syncplay.version))
self.port = port self.port = port
if password: if password:
@ -41,18 +41,19 @@ class SyncFactory(Factory):
print(getMessage("no-salt-notification").format(salt)) print(getMessage("no-salt-notification").format(salt))
self._salt = salt self._salt = salt
self._motdFilePath = motdFilePath self._motdFilePath = motdFilePath
self._roomsDirPath = roomsDirPath if roomsDirPath is not None and os.path.isdir(roomsDirPath) else None self.roomsDbFile = roomsDbFile
self._roomsTimer = roomsTimer if roomsDirPath is not None and isinstance(roomsTimer, int) and roomsTimer > 0 else 0
self.disableReady = disableReady self.disableReady = disableReady
self.disableChat = disableChat self.disableChat = disableChat
self.maxChatMessageLength = maxChatMessageLength if maxChatMessageLength is not None else constants.MAX_CHAT_MESSAGE_LENGTH self.maxChatMessageLength = maxChatMessageLength if maxChatMessageLength is not None else constants.MAX_CHAT_MESSAGE_LENGTH
self.maxUsernameLength = maxUsernameLength if maxUsernameLength is not None else constants.MAX_USERNAME_LENGTH self.maxUsernameLength = maxUsernameLength if maxUsernameLength is not None else constants.MAX_USERNAME_LENGTH
self.permanentRoomsFile = permanentRoomsFile if permanentRoomsFile is not None and os.path.isfile(permanentRoomsFile) else None
self.permanentRooms = self.loadListFromMultilineTextFile(self.permanentRoomsFile) if self.permanentRoomsFile is not None else []
if not isolateRooms: if not isolateRooms:
self._roomManager = RoomManager(self._roomsDirPath, self._roomsTimer) self._roomManager = RoomManager(self.roomsDbFile, self.permanentRooms)
else: else:
self._roomManager = PublicRoomManager() self._roomManager = PublicRoomManager()
if statsDbFile is not None: if statsDbFile is not None:
self._statsDbHandle = DBManager(statsDbFile) self._statsDbHandle = StatsDBManager(statsDbFile)
self._statsRecorder = StatsRecorder(self._statsDbHandle, self._roomManager) self._statsRecorder = StatsRecorder(self._statsDbHandle, self._roomManager)
statsDelay = 5*(int(self.port)%10 + 1) statsDelay = 5*(int(self.port)%10 + 1)
self._statsRecorder.startRecorder(statsDelay) self._statsRecorder.startRecorder(statsDelay)
@ -67,6 +68,16 @@ class SyncFactory(Factory):
self.options = None self.options = None
self.serverAcceptsTLS = False self.serverAcceptsTLS = False
def loadListFromMultilineTextFile(self, path):
if not os.path.isfile(path):
return []
with open(path) as f:
multiline = f.read().splitlines()
return multiline
def loadRoom(self):
rooms = self._roomsDbHandle.loadRooms()
def buildProtocol(self, addr): def buildProtocol(self, addr):
return SyncServerProtocol(self) return SyncServerProtocol(self)
@ -82,6 +93,7 @@ class SyncFactory(Factory):
features["isolateRooms"] = self.isolateRooms features["isolateRooms"] = self.isolateRooms
features["readiness"] = not self.disableReady features["readiness"] = not self.disableReady
features["managedRooms"] = True features["managedRooms"] = True
features["persistentRooms"] = self.roomsDbFile is not None
features["chat"] = not self.disableChat features["chat"] = not self.disableChat
features["maxChatMessageLength"] = self.maxChatMessageLength features["maxChatMessageLength"] = self.maxChatMessageLength
features["maxUsernameLength"] = self.maxUsernameLength features["maxUsernameLength"] = self.maxUsernameLength
@ -137,11 +149,17 @@ class SyncFactory(Factory):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None) l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None)
self._roomManager.broadcast(watcher, l) self._roomManager.broadcast(watcher, l)
self._roomManager.broadcastRoom(watcher, lambda w: w.sendSetReady(watcher.getName(), watcher.isReady(), False)) self._roomManager.broadcastRoom(watcher, lambda w: w.sendSetReady(watcher.getName(), watcher.isReady(), False))
if self.roomsDbFile:
l = lambda w: w.sendList(toGUIOnly=True)
self._roomManager.broadcast(watcher, l)
def removeWatcher(self, watcher): def removeWatcher(self, watcher):
if watcher and watcher.getRoom(): if watcher and watcher.getRoom():
self.sendLeftMessage(watcher) self.sendLeftMessage(watcher)
self._roomManager.removeWatcher(watcher) self._roomManager.removeWatcher(watcher)
if self.roomsDbFile:
l = lambda w: w.sendList(toGUIOnly=True)
self._roomManager.broadcast(watcher, l)
def sendLeftMessage(self, watcher): def sendLeftMessage(self, watcher):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"left": True}) l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"left": True})
@ -151,6 +169,9 @@ class SyncFactory(Factory):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"joined": True, "version": watcher.getVersion(), "features": watcher.getFeatures()}) if w != watcher else None l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"joined": True, "version": watcher.getVersion(), "features": watcher.getFeatures()}) if w != watcher else None
self._roomManager.broadcast(watcher, l) self._roomManager.broadcast(watcher, l)
self._roomManager.broadcastRoom(watcher, lambda w: w.sendSetReady(watcher.getName(), watcher.isReady(), False)) self._roomManager.broadcastRoom(watcher, lambda w: w.sendSetReady(watcher.getName(), watcher.isReady(), False))
if self.roomsDbFile:
l = lambda w: w.sendList(toGUIOnly=True)
self._roomManager.broadcast(watcher, l)
def sendFileUpdate(self, watcher): def sendFileUpdate(self, watcher):
if watcher.getFile(): if watcher.getFile():
@ -172,6 +193,9 @@ class SyncFactory(Factory):
def getAllWatchersForUser(self, forUser): def getAllWatchersForUser(self, forUser):
return self._roomManager.getAllWatchersForUser(forUser) return self._roomManager.getAllWatchersForUser(forUser)
def getEmptyPersistentRooms(self):
return self._roomManager.getEmptyPersistentRooms()
def authRoomController(self, watcher, password, roomBaseName=None): def authRoomController(self, watcher, password, roomBaseName=None):
room = watcher.getRoom() room = watcher.getRoom()
roomName = roomBaseName if roomBaseName else room.getName() roomName = roomBaseName if roomBaseName else room.getName()
@ -290,8 +314,33 @@ class StatsRecorder(object):
except: except:
pass pass
class RoomsRecorder(StatsRecorder):
def __init__(self, dbHandle, roomManager):
self._dbHandle = dbHandle
self._roomManagerHandle = roomManager
class DBManager(object): def startRecorder(self, delay):
try:
self._dbHandle.connect()
reactor.callLater(delay, self._scheduleClientSnapshot) # TODO: FIX THIS!
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 StatsDBManager(object):
def __init__(self, dbpath): def __init__(self, dbpath):
self._dbPath = dbpath self._dbPath = dbpath
self._connection = None self._connection = None
@ -305,31 +354,74 @@ class DBManager(object):
self._createSchema() self._createSchema()
def _createSchema(self): def _createSchema(self):
initQuery = 'create table if not exists clients_snapshots (snapshot_time integer, version string)' initQuery = 'create table if not exists clients_snapshots (snapshot_time INTEGER, version STRING)'
self._connection.runQuery(initQuery) return self._connection.runQuery(initQuery)
def addVersionLog(self, timestamp, version): def addVersionLog(self, timestamp, version):
content = (timestamp, version, ) content = (timestamp, version, )
self._connection.runQuery("INSERT INTO clients_snapshots VALUES (?, ?)", content) self._connection.runQuery("INSERT INTO clients_snapshots VALUES (?, ?)", content)
class RoomDBManager(object):
def __init__(self, dbpath, loadroomscallback):
self._dbPath = dbpath
self._connection = None
self._loadRoomsCallback = loadroomscallback
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().addCallback(self.loadRooms)
def _createSchema(self):
initQuery = 'create table if not exists persistent_rooms (name STRING PRIMARY KEY, playlist STRING, playlistIndex INTEGER, position REAL, lastSavedUpdate INTEGER)'
return self._connection.runQuery(initQuery)
def saveRoom(self, name, playlist, playlistIndex, position, lastUpdate):
content = (name, playlist, playlistIndex, position, lastUpdate)
self._connection.runQuery("INSERT OR REPLACE INTO persistent_rooms VALUES (?, ?, ?, ?, ?)", content)
def deleteRoom(self, name):
self._connection.runQuery("DELETE FROM persistent_rooms where name = ?", [name])
def loadRooms(self, result=None):
roomsQuery = "SELECT * FROM persistent_rooms"
rooms = self._connection.runQuery(roomsQuery)
rooms.addCallback(self.loadedRooms)
def loadedRooms(self, rooms):
self._loadRoomsCallback(rooms)
class RoomManager(object): class RoomManager(object):
def __init__(self, roomsDir=None, timer=0): def __init__(self, roomsdbfile=None, permanentRooms=[]):
self._roomsDir = roomsDir self._roomsDbFile = roomsdbfile
self._timer = timer
self._rooms = {} self._rooms = {}
if self._roomsDir is not None: self._permanentRooms = permanentRooms
for root, dirs, files in os.walk(self._roomsDir): if self._roomsDbFile is not None:
for file in files: self._roomsDbHandle = RoomDBManager(self._roomsDbFile, self.loadRooms)
if file.endswith(".room"): self._roomsDbHandle.connect()
room = Room('', self._roomsDir) else:
room.loadFromFile(os.path.join(root, file)) self._roomsDbHandle = None
roomName = truncateText(room.getName(), constants.MAX_ROOM_NAME_LENGTH)
if len(room.getPlaylist()) == 0 or room.isStale(self._timer): def loadRooms(self, rooms):
os.remove(os.path.join(root, file)) roomsLoaded = []
del room for roomDetails in rooms:
else: roomName = truncateText(roomDetails[0], constants.MAX_ROOM_NAME_LENGTH)
self._rooms[roomName] = room room = Room(roomDetails[0], self._roomsDbHandle)
room.loadRoom(roomDetails)
if roomName in self._permanentRooms:
room.setPermanent(True)
self._rooms[roomName] = room
roomsLoaded.append(roomName)
for roomName in self._permanentRooms:
if roomName not in roomsLoaded:
roomDetails = (roomName, "", 0, 0, 0)
room = Room(roomName, self._roomsDbHandle)
room.loadRoom(roomDetails)
room.setPermanent(True)
self._rooms[roomName] = room
def broadcastRoom(self, sender, whatLambda): def broadcastRoom(self, sender, whatLambda):
room = sender.getRoom() room = sender.getRoom()
@ -349,6 +441,20 @@ class RoomManager(object):
watchers.append(watcher) watchers.append(watcher)
return watchers return watchers
def getPersistentRooms(self, sender):
persistentRooms = []
for room in self._rooms.values():
if room.isPersistent():
persistentRooms.append(room.getName())
return persistentRooms
def getEmptyPersistentRooms(self):
emptyPersistentRooms = []
for room in self._rooms.values():
if len(room.getWatchers()) == 0:
emptyPersistentRooms.append(room.getName())
return emptyPersistentRooms
def moveWatcher(self, watcher, roomName): def moveWatcher(self, watcher, roomName):
roomName = truncateText(roomName, constants.MAX_ROOM_NAME_LENGTH) roomName = truncateText(roomName, constants.MAX_ROOM_NAME_LENGTH)
self.removeWatcher(watcher) self.removeWatcher(watcher)
@ -367,16 +473,20 @@ class RoomManager(object):
return self._rooms[roomName] return self._rooms[roomName]
else: else:
if RoomPasswordProvider.isControlledRoom(roomName): if RoomPasswordProvider.isControlledRoom(roomName):
room = ControlledRoom(roomName) room = ControlledRoom(roomName, self._roomsDbHandle)
else: else:
if roomName in self._rooms: if roomName in self._rooms:
self._deleteRoomIfEmpty(self._rooms[roomName]) self._deleteRoomIfEmpty(self._rooms[roomName])
room = Room(roomName, self._roomsDir) room = Room(roomName, self._roomsDbHandle)
self._rooms[roomName] = room self._rooms[roomName] = room
return room return room
def _deleteRoomIfEmpty(self, room): def _deleteRoomIfEmpty(self, room):
if room.isEmpty() and room.getName() in self._rooms: if room.isEmpty() and room.getName():
if self._roomsDbHandle and room.isNotPermanent():
if room.isPersistent() and not room.isPlaylistEmpty():
return
self._roomsDbHandle.deleteRoom(room.getName())
del self._rooms[room.getName()] del self._rooms[room.getName()]
def findFreeUsername(self, username): def findFreeUsername(self, username):
@ -412,9 +522,9 @@ class Room(object):
STATE_PAUSED = 0 STATE_PAUSED = 0
STATE_PLAYING = 1 STATE_PLAYING = 1
def __init__(self, name, _roomsDir=None): def __init__(self, name, roomsdbhandle):
self._name = name self._name = name
self._roomsDir = _roomsDir self._roomsDbHandle = roomsdbhandle
self._watchers = {} self._watchers = {}
self._playState = self.STATE_PAUSED self._playState = self.STATE_PAUSED
self._setBy = None self._setBy = None
@ -423,51 +533,46 @@ class Room(object):
self._lastUpdate = time.time() self._lastUpdate = time.time()
self._lastSavedUpdate = 0 self._lastSavedUpdate = 0
self._position = 0 self._position = 0
self._permanent = False
def __str__(self, *args, **kwargs): def __str__(self, *args, **kwargs):
return self.getName() return self.getName()
def roomsCanPersist(self): def roomsCanPersist(self):
return self._roomsDir is not None return self._roomsDbHandle is not None
def isPersistent(self):
return self.roomsCanPersist() and not self.isMarkedAsTemporary()
def isMarkedAsTemporary(self):
roomName = self.getName().lower()
return roomName.endswith("-temp") or "-temp:" in roomName
def isPlaylistEmpty(self):
return len(self._playlist) == 0
def isPermanent(self): def isPermanent(self):
return self.roomsCanPersist() return self._permanent
def isNotPermanent(self):
return not self.isPermanent()
def sanitizeFilename(self, filename, blacklist="<>:/\\|?*\"", placeholder="_"): def sanitizeFilename(self, filename, blacklist="<>:/\\|?*\"", placeholder="_"):
return ''.join([c if c not in blacklist and ord(c) >= 32 else placeholder for c in filename]) return ''.join([c if c not in blacklist and ord(c) >= 32 else placeholder for c in filename])
def writeToFile(self): def writeToDb(self):
if not self.isPermanent(): if not self.isPersistent():
return return
filename = os.path.join(self._roomsDir, self.sanitizeFilename(self._name)+'.room') processed_playlist = getListAsMultilineString(self._playlist)
if len(self._playlist) == 0: self._roomsDbHandle.saveRoom(self._name, processed_playlist, self._playlistIndex, self._position, self._lastSavedUpdate)
try:
os.remove(filename)
except Exception:
pass
return
data = {}
data['name'] = self._name
data['playlist'] = self._playlist
data['playlistIndex'] = self._playlistIndex
data['position'] = self._position
data['lastSavedUpdate'] = self._lastSavedUpdate
with open(filename, "w") as outfile:
json.dump(data, outfile)
def loadFromFile(self, filename): def loadRoom(self, room):
with open(filename) as json_file: name, playlist, playlistindex, position, lastupdate = room
data = json.load(json_file) self._name = name
self._name = truncateText(data['name'], constants.MAX_ROOM_NAME_LENGTH) self._playlist = convertMultilineStringToList(playlist)
self._playlist = data['playlist'] self._playlistIndex = playlistindex
self._playlistIndex = data['playlistIndex'] self._position = position
self._position = data['position'] self._lastSavedUpdate = lastupdate
self._lastSavedUpdate = data['lastSavedUpdate']
def isStale(self, timer):
if timer == 0 or self._lastSavedUpdate == 0:
return False
return time.time() - self._lastSavedUpdate > timer
def getName(self): def getName(self):
return self._name return self._name
@ -488,14 +593,17 @@ class Room(object):
def setPaused(self, paused=STATE_PAUSED, setBy=None): def setPaused(self, paused=STATE_PAUSED, setBy=None):
self._playState = paused self._playState = paused
self._setBy = setBy self._setBy = setBy
self.writeToFile() self.writeToDb()
def setPosition(self, position, setBy=None): def setPosition(self, position, setBy=None):
self._position = position self._position = position
for watcher in self._watchers.values(): for watcher in self._watchers.values():
watcher.setPosition(position) watcher.setPosition(position)
self._setBy = setBy self._setBy = setBy
self.writeToFile() self.writeToDb()
def setPermanent(self, newState):
self._permanent = newState
def isPlaying(self): def isPlaying(self):
return self._playState == self.STATE_PLAYING return self._playState == self.STATE_PLAYING
@ -507,7 +615,7 @@ class Room(object):
return list(self._watchers.values()) return list(self._watchers.values())
def addWatcher(self, watcher): def addWatcher(self, watcher):
if self._watchers or self.isPermanent(): if self._watchers or self.isPersistent():
watcher.setPosition(self.getPosition()) watcher.setPosition(self.getPosition())
self._watchers[watcher.getName()] = watcher self._watchers[watcher.getName()] = watcher
watcher.setRoom(self) watcher.setRoom(self)
@ -517,9 +625,9 @@ class Room(object):
return return
del self._watchers[watcher.getName()] del self._watchers[watcher.getName()]
watcher.setRoom(None) watcher.setRoom(None)
if not self._watchers and not self.isPermanent(): if not self._watchers and not self.isPersistent():
self._position = 0 self._position = 0
self.writeToFile() self.writeToDb()
def isEmpty(self): def isEmpty(self):
return not bool(self._watchers) return not bool(self._watchers)
@ -532,11 +640,11 @@ class Room(object):
def setPlaylist(self, files, setBy=None): def setPlaylist(self, files, setBy=None):
self._playlist = files self._playlist = files
self.writeToFile() self.writeToDb()
def setPlaylistIndex(self, index, setBy=None): def setPlaylistIndex(self, index, setBy=None):
self._playlistIndex = index self._playlistIndex = index
self.writeToFile() self.writeToDb()
def getPlaylist(self): def getPlaylist(self):
return self._playlist return self._playlist
@ -544,10 +652,12 @@ class Room(object):
def getPlaylistIndex(self): def getPlaylistIndex(self):
return self._playlistIndex return self._playlistIndex
def getControllers(self):
return []
class ControlledRoom(Room): class ControlledRoom(Room):
def __init__(self, name): def __init__(self, name, roomsdbhandle):
Room.__init__(self, name) Room.__init__(self, name, roomsdbhandle)
self._controllers = {} self._controllers = {}
def getPosition(self): def getPosition(self):
@ -570,6 +680,7 @@ class ControlledRoom(Room):
Room.removeWatcher(self, watcher) Room.removeWatcher(self, watcher)
if watcher.getName() in self._controllers: if watcher.getName() in self._controllers:
del self._controllers[watcher.getName()] del self._controllers[watcher.getName()]
self.writeToDb()
def setPaused(self, paused=Room.STATE_PAUSED, setBy=None): def setPaused(self, paused=Room.STATE_PAUSED, setBy=None):
if self.canControl(setBy): if self.canControl(setBy):
@ -591,7 +702,7 @@ class ControlledRoom(Room):
return watcher.getName() in self._controllers return watcher.getName() in self._controllers
def getControllers(self): def getControllers(self):
return self._controllers return {}
class Watcher(object): class Watcher(object):
@ -671,6 +782,23 @@ class Watcher(object):
if self._connector.meetsMinVersion(constants.CHAT_MIN_VERSION): if self._connector.meetsMinVersion(constants.CHAT_MIN_VERSION):
self._connector.sendMessage({"Chat": message}) self._connector.sendMessage({"Chat": message})
def sendList(self, toGUIOnly=False):
if toGUIOnly and self.isGUIUser(self._connector.getFeatures()):
clientFeatures = self._connector.getFeatures()
if "uiMode" in clientFeatures:
if clientFeatures["uiMode"] == constants.CONSOLE_UI_MODE:
return
else:
return
self._connector.sendList()
def isGUIUser(self, clientFeatures):
clientFeatures = self._connector.getFeatures()
uiMode = clientFeatures["uiMode"] if "uiMode" in clientFeatures else constants.UNKNOWN_UI_MODE
if uiMode == constants.UNKNOWN_UI_MODE:
uiMode = constants.FALLBACK_ASSUMED_UI_MODE
return uiMode == constants.GRAPHICAL_UI_MODE
def sendSetReady(self, username, isReady, manuallyInitiated=True): def sendSetReady(self, username, isReady, manuallyInitiated=True):
self._connector.sendSetReady(username, isReady, manuallyInitiated) self._connector.sendSetReady(username, isReady, manuallyInitiated)
@ -756,8 +884,8 @@ class ConfigurationGetter(object):
self._argparser.add_argument('--disable-chat', action='store_true', help=getMessage("server-chat-argument")) self._argparser.add_argument('--disable-chat', action='store_true', help=getMessage("server-chat-argument"))
self._argparser.add_argument('--salt', metavar='salt', type=str, nargs='?', help=getMessage("server-salt-argument"), default=os.environ.get('SYNCPLAY_SALT')) self._argparser.add_argument('--salt', metavar='salt', type=str, nargs='?', help=getMessage("server-salt-argument"), default=os.environ.get('SYNCPLAY_SALT'))
self._argparser.add_argument('--motd-file', metavar='file', type=str, nargs='?', help=getMessage("server-motd-argument")) self._argparser.add_argument('--motd-file', metavar='file', type=str, nargs='?', help=getMessage("server-motd-argument"))
self._argparser.add_argument('--rooms-dir', metavar='rooms', type=str, nargs='?', help=getMessage("server-rooms-argument")) self._argparser.add_argument('--rooms-db-file', metavar='rooms', type=str, nargs='?', help=getMessage("server-rooms-argument"))
self._argparser.add_argument('--rooms-timer', metavar='timer', type=int, nargs='?',default=31558149, help=getMessage("server-timer-argument")) self._argparser.add_argument('--permanent-rooms-file', metavar='permanentrooms', type=str, nargs='?', help=getMessage("server-permanent-rooms-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-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('--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")) self._argparser.add_argument('--stats-db-file', metavar='file', type=str, nargs='?', help=getMessage("server-stats-db-file-argument"))

View File

@ -18,6 +18,7 @@ class ConsoleUI(threading.Thread):
self.PromptResult = "" self.PromptResult = ""
self.promptMode.set() self.promptMode.set()
self._syncplayClient = None self._syncplayClient = None
self.uiMode = constants.CONSOLE_UI_MODE
threading.Thread.__init__(self, name="ConsoleUI") threading.Thread.__init__(self, name="ConsoleUI")
def addClient(self, client): def addClient(self, client):

View File

@ -468,6 +468,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.roomsCombobox.clear() self.roomsCombobox.clear()
for roomListValue in self.config['roomList']: for roomListValue in self.config['roomList']:
self.roomsCombobox.addItem(roomListValue) self.roomsCombobox.addItem(roomListValue)
for room in self.currentRooms:
if room not in self.config['roomList']:
self.roomsCombobox.addItem(room)
self.roomsCombobox.setEditText(previousRoomSelection) self.roomsCombobox.setEditText(previousRoomSelection)
def addRoomToList(self, newRoom=None): def addRoomToList(self, newRoom=None):
@ -603,7 +606,16 @@ class MainWindow(QtWidgets.QMainWindow):
): ):
self._syncplayClient.fileSwitch.setCurrentDirectory(os.path.dirname(self._syncplayClient.userlist.currentUser.file["path"])) self._syncplayClient.fileSwitch.setCurrentDirectory(os.path.dirname(self._syncplayClient.userlist.currentUser.file["path"]))
self.currentRooms = []
for room in rooms: for room in rooms:
self.currentRooms.append(room)
if self.hideEmptyRooms:
foundEmptyRooms = False
for user in rooms[room]:
if user.username.strip() == "":
foundEmptyRooms = True
if foundEmptyRooms:
continue
self.newWatchlist = [] self.newWatchlist = []
roomitem = QtGui.QStandardItem(room) roomitem = QtGui.QStandardItem(room)
font = QtGui.QFont() font = QtGui.QFont()
@ -624,6 +636,8 @@ class MainWindow(QtWidgets.QMainWindow):
roomitem.setIcon(QtGui.QPixmap(resourcespath + 'chevrons_right.png')) roomitem.setIcon(QtGui.QPixmap(resourcespath + 'chevrons_right.png'))
for user in rooms[room]: for user in rooms[room]:
if user.username.strip() == "":
continue
useritem = QtGui.QStandardItem(user.username) useritem = QtGui.QStandardItem(user.username)
isController = user.isController() isController = user.isController()
sameRoom = room == currentUser.room sameRoom = room == currentUser.room
@ -695,6 +709,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.listTreeView.expandAll() self.listTreeView.expandAll()
self.updateListGeometry() self.updateListGeometry()
self._syncplayClient.fileSwitch.setFilenameWatchlist(self.newWatchlist) self._syncplayClient.fileSwitch.setFilenameWatchlist(self.newWatchlist)
self.fillRoomsCombobox()
@needsClient @needsClient
def undoPlaylistChange(self): def undoPlaylistChange(self):
@ -1753,6 +1768,9 @@ class MainWindow(QtWidgets.QMainWindow):
window.autoplayAction.setCheckable(True) window.autoplayAction.setCheckable(True)
window.autoplayAction.triggered.connect(self.updateAutoplayVisibility) window.autoplayAction.triggered.connect(self.updateAutoplayVisibility)
window.hideEmptyRoomsAction = window.windowMenu.addAction(getMessage("hideemptyrooms-menu-label"))
window.hideEmptyRoomsAction.setCheckable(True)
window.hideEmptyRoomsAction.triggered.connect(self.updateEmptyRoomVisiblity)
# Help menu # Help menu
@ -1818,6 +1836,11 @@ class MainWindow(QtWidgets.QMainWindow):
def updateAutoplayVisibility(self): def updateAutoplayVisibility(self):
self.autoplayFrame.setVisible(self.autoplayAction.isChecked()) self.autoplayFrame.setVisible(self.autoplayAction.isChecked())
def updateEmptyRoomVisiblity(self):
self.hideEmptyRooms = self.hideEmptyRoomsAction.isChecked()
if self._syncplayClient:
self._syncplayClient.getUserList()
def changeReadyState(self): def changeReadyState(self):
self.updateReadyIcon() self.updateReadyIcon()
if self._syncplayClient: if self._syncplayClient:
@ -2017,6 +2040,7 @@ class MainWindow(QtWidgets.QMainWindow):
settings.setValue("pos", self.pos()) settings.setValue("pos", self.pos())
settings.setValue("showPlaybackButtons", self.playbackAction.isChecked()) settings.setValue("showPlaybackButtons", self.playbackAction.isChecked())
settings.setValue("showAutoPlayButton", self.autoplayAction.isChecked()) settings.setValue("showAutoPlayButton", self.autoplayAction.isChecked())
settings.setValue("hideEmptyRooms", self.hideEmptyRoomsAction.isChecked())
settings.setValue("autoplayChecked", self.autoplayPushButton.isChecked()) settings.setValue("autoplayChecked", self.autoplayPushButton.isChecked())
settings.setValue("autoplayMinUsers", self.autoplayThresholdSpinbox.value()) settings.setValue("autoplayMinUsers", self.autoplayThresholdSpinbox.value())
settings.endGroup() settings.endGroup()
@ -2040,6 +2064,9 @@ class MainWindow(QtWidgets.QMainWindow):
if settings.value("showAutoPlayButton", "false") == "true": if settings.value("showAutoPlayButton", "false") == "true":
self.autoplayAction.setChecked(True) self.autoplayAction.setChecked(True)
self.updateAutoplayVisibility() self.updateAutoplayVisibility()
if settings.value("hideEmptyRooms", "false") == "true":
self.hideEmptyRooms = True
self.hideEmptyRoomsAction.setChecked(True)
if settings.value("autoplayChecked", "false") == "true": if settings.value("autoplayChecked", "false") == "true":
self.updateAutoPlayState(True) self.updateAutoPlayState(True)
self.autoplayPushButton.setChecked(True) self.autoplayPushButton.setChecked(True)
@ -2063,6 +2090,8 @@ class MainWindow(QtWidgets.QMainWindow):
self.lastCheckedForUpdates = None self.lastCheckedForUpdates = None
self._syncplayClient = None self._syncplayClient = None
self.folderSearchEnabled = True self.folderSearchEnabled = True
self.hideEmptyRooms = False
self.currentRooms = []
self.QtGui = QtGui self.QtGui = QtGui
if isMacOS(): if isMacOS():
self.setWindowFlags(self.windowFlags()) self.setWindowFlags(self.windowFlags())
@ -2081,3 +2110,4 @@ class MainWindow(QtWidgets.QMainWindow):
self.show() self.show()
self.setAcceptDrops(True) self.setAcceptDrops(True)
self.clearedPlaylistNote = False self.clearedPlaylistNote = False
self.uiMode = constants.GRAPHICAL_UI_MODE