From c0e8652c8c2a94f3a75d67bd0798292c55527e32 Mon Sep 17 00:00:00 2001 From: Etoh Date: Thu, 9 Dec 2021 16:23:57 +0000 Subject: [PATCH] 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 * 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 --- docs/syncplay-server.1 | 9 +- syncplay/client.py | 6 + syncplay/constants.py | 5 + syncplay/ep_server.py | 4 +- syncplay/messages.py | 2 +- syncplay/messages_de.py | 6 +- syncplay/messages_en.py | 6 +- syncplay/messages_es.py | 6 +- syncplay/messages_it.py | 6 +- syncplay/messages_pt_BR.py | 6 +- syncplay/messages_pt_PT.py | 6 +- syncplay/messages_ru.py | 6 +- syncplay/messages_tr.py | 6 +- syncplay/protocols.py | 39 +++++- syncplay/server.py | 276 +++++++++++++++++++++++++++---------- syncplay/ui/consoleUI.py | 1 + syncplay/ui/gui.py | 30 ++++ 17 files changed, 321 insertions(+), 99 deletions(-) diff --git a/docs/syncplay-server.1 b/docs/syncplay-server.1 index e9b91e8..7865cc7 100644 --- a/docs/syncplay-server.1 +++ b/docs/syncplay-server.1 @@ -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. .TP -.B \-\-rooms\-dir [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. +.B \-\-rooms\-db-file [directory] +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 -.B \-\-rooms\-timer [directory] -Requires \fB\-\-rooms\-timer\fP. Time in seconds that rooms will persist without users. \fB0\fP disables the timer, meaning rooms persist permanently. +.B \-\-permanent\-rooms-file [directory] +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 .B \-\-max\-chat\-message\-length [maxChatMessageLength] diff --git a/syncplay/client.py b/syncplay/client.py index d3c4c3f..cc6fc83 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -640,6 +640,7 @@ class SyncplayClient(object): "chat": utils.meetsMinVersion(self.serverVersion, constants.CHAT_MIN_VERSION), "readiness": utils.meetsMinVersion(self.serverVersion, constants.USER_READY_MIN_VERSION), "managedRooms": utils.meetsMinVersion(self.serverVersion, constants.CONTROLLED_ROOMS_MIN_VERSION), + "persistentRooms": False, "maxChatMessageLength": constants.FALLBACK_MAX_CHAT_MESSAGE_LENGTH, "maxUsernameLength": constants.FALLBACK_MAX_USERNAME_LENGTH, "maxRoomNameLength": constants.FALLBACK_MAX_ROOM_NAME_LENGTH, @@ -706,11 +707,13 @@ class SyncplayClient(object): # Can change during runtime: features["sharedPlaylists"] = self.sharedPlaylistIsEnabled() # Can change during runtime features["chat"] = self.chatIsEnabled() # Can change during runtime + features["uiMode"] = self.ui.getUIMode() # Static for this version/release of Syncplay: features["featureList"] = True features["readiness"] = True features["managedRooms"] = True + features["persistentRooms"] = True return features @@ -1593,6 +1596,9 @@ class UiManager(object): self.lastAlertOSDEndTime = None self.lastError = "" + def getUIMode(self): + return self.__ui.uiMode + def addFileToPlaylist(self, newPlaylistItem): self.__ui.addFileToPlaylist(newPlaylistItem) diff --git a/syncplay/constants.py b/syncplay/constants.py index ddb2f73..148de97 100755 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -349,3 +349,8 @@ DEFAULT_TRUSTED_DOMAINS = ["youtube.com", "youtu.be"] TRUSTABLE_WEB_PROTOCOLS = ["http", "https"] PRIVATE_FILE_FIELDS = ["path"] + +CONSOLE_UI_MODE = "CLI" +GRAPHICAL_UI_MODE = "GUI" +UNKNOWN_UI_MODE = "Unknown" +FALLBACK_ASSUMED_UI_MODE = GRAPHICAL_UI_MODE diff --git a/syncplay/ep_server.py b/syncplay/ep_server.py index d2e4aef..548ddd0 100644 --- a/syncplay/ep_server.py +++ b/syncplay/ep_server.py @@ -34,8 +34,8 @@ def main(): args.port, args.password, args.motd_file, - args.rooms_dir, - args.rooms_timer, + args.rooms_db_file, + args.permanent_rooms_file, args.isolate_rooms, args.salt, args.disable_ready, diff --git a/syncplay/messages.py b/syncplay/messages.py index 23ed80a..0e3d199 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -64,7 +64,7 @@ def getInitialLanguage(): try: import sys frozen = getattr(sys, 'frozen', '') - if frozen in 'macosx_app': + if frozen and frozen in 'macosx_app': from PySide2.QtCore import QLocale initialLanguage = QLocale.system().uiLanguages()[0].split('-')[0] else: diff --git a/syncplay/messages_de.py b/syncplay/messages_de.py index 595ea50..d3efe86 100755 --- a/syncplay/messages_de.py +++ b/syncplay/messages_de.py @@ -288,6 +288,7 @@ de = { "autoplay-menu-label": "Auto-Play-Knopf anzeigen", "autoplay-guipushbuttonlabel": "Automatisch abspielen wenn alle bereit sind", "autoplay-minimum-label": "Minimum an Nutzern:", + "hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate "sendmessage-label": "Senden", @@ -458,6 +459,7 @@ de = { # Server messages to client "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 "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-disable-ready-argument": "Bereitschaftsfeature deaktivieren", "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-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # 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-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-maxchars-argument": "Maximale Zeichenzahl in einer Chatnachricht (Standard ist {})", "server-maxusernamelength-argument": "Maximale Zeichenzahl in einem Benutzernamen (Standard ist {})", diff --git a/syncplay/messages_en.py b/syncplay/messages_en.py index ac7b25b..8daae25 100755 --- a/syncplay/messages_en.py +++ b/syncplay/messages_en.py @@ -292,6 +292,7 @@ en = { "autoplay-menu-label": "Show auto-play button", "autoplay-guipushbuttonlabel": "Play when all ready", "autoplay-minimum-label": "Min users:", + "hideemptyrooms-menu-label": "Hide empty persistent rooms", "sendmessage-label": "Send", @@ -458,6 +459,7 @@ en = { # Server messages to client "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 "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-disable-ready-argument": "disable readiness feature", "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-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", + "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-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-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 {})", diff --git a/syncplay/messages_es.py b/syncplay/messages_es.py index cf4cd57..efe8f8c 100644 --- a/syncplay/messages_es.py +++ b/syncplay/messages_es.py @@ -291,6 +291,7 @@ es = { "autoplay-menu-label": "Mostrar botón de auto-reproducción", "autoplay-guipushbuttonlabel": "Reproducir cuando todos estén listos", "autoplay-minimum-label": "Mín. de usuarios:", + "hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate "sendmessage-label": "Enviar", @@ -457,6 +458,7 @@ es = { # 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 + "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 "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-disable-ready-argument": "deshabilitar la función de preparación", "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-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # 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-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-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 {})", diff --git a/syncplay/messages_it.py b/syncplay/messages_it.py index 44dd2a4..9a38b5f 100755 --- a/syncplay/messages_it.py +++ b/syncplay/messages_it.py @@ -291,6 +291,7 @@ it = { "autoplay-menu-label": "Mostra il tasto di riproduzione automatica", "autoplay-guipushbuttonlabel": "Riproduci quando tutti sono pronti", "autoplay-minimum-label": "Minimo utenti pronti:", + "hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate "sendmessage-label": "Invia", @@ -457,6 +458,7 @@ it = { # Server messages to client "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 "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-disable-ready-argument": "disabilita la funzionalità \"pronto\"", "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-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # 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-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-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 è {})", diff --git a/syncplay/messages_pt_BR.py b/syncplay/messages_pt_BR.py index 49407d5..ec7b868 100644 --- a/syncplay/messages_pt_BR.py +++ b/syncplay/messages_pt_BR.py @@ -292,6 +292,7 @@ pt_BR = { "autoplay-menu-label": "Mostrar botão de reprodução automática", "autoplay-guipushbuttonlabel": "Tocar quando todos estiverem prontos", "autoplay-minimum-label": "Mín. de usuários:", + "hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate "sendmessage-label": "Enviar", @@ -458,6 +459,7 @@ pt_BR = { # 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 + "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 "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-disable-ready-argument": "desativar recurso de prontidão", "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-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # 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-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-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 é {})", diff --git a/syncplay/messages_pt_PT.py b/syncplay/messages_pt_PT.py index 4cdbedb..aae0f40 100644 --- a/syncplay/messages_pt_PT.py +++ b/syncplay/messages_pt_PT.py @@ -290,6 +290,7 @@ pt_PT = { "autoplay-menu-label": "Mostrar botão de reprodução automática", "autoplay-guipushbuttonlabel": "Tocar quando todos estiverem prontos", "autoplay-minimum-label": "Mín. de utilizadores:", + "hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate "sendmessage-label": "Enviar", @@ -457,6 +458,7 @@ pt_PT = { # 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 + "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 "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-disable-ready-argument": "desativar recurso de prontidão", "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-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # 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-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-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 é {})", diff --git a/syncplay/messages_ru.py b/syncplay/messages_ru.py index b9a3e4a..76236c3 100755 --- a/syncplay/messages_ru.py +++ b/syncplay/messages_ru.py @@ -291,6 +291,7 @@ ru = { "autoplay-menu-label": "Показывать кнопку &автовоспроизведения", "autoplay-guipushbuttonlabel": "Стартовать, когда все будут готовы", "autoplay-minimum-label": "Минимум зрителей:", + "hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate "sendmessage-label": "Выслать", "ready-guipushbuttonlabel": "Я готов", @@ -455,6 +456,7 @@ ru = { # Server messages to client "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 "welcome-server-notification": "Добро пожаловать на сервер Syncplay версии {0}", # version @@ -471,8 +473,8 @@ ru = { "server-salt-argument": "генерировать пароли к управляемым комнатам на основании указанной строки (соли)", "server-disable-ready-argument": "отключить статусы готов/не готов", "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-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # 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-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-maxchars-argument": "Максимальное число символов в сообщениях в чате (по умолчанию {})", "server-maxusernamelength-argument": "Максимальное число символов в именах пользователей (по умолчанию {})", diff --git a/syncplay/messages_tr.py b/syncplay/messages_tr.py index ad602b4..293fdca 100644 --- a/syncplay/messages_tr.py +++ b/syncplay/messages_tr.py @@ -292,6 +292,7 @@ tr = { "autoplay-menu-label": "Otomatik oynat düğmesini göster", "autoplay-guipushbuttonlabel": "Her şey hazır olduğunda oynat", "autoplay-minimum-label": "Asgari kullanıcı:", + "hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate "sendmessage-label": "Gönder", @@ -458,6 +459,7 @@ tr = { # 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 + "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 "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-disable-ready-argument": "hazır olma özelliğini devre dışı bırak", "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-timer-argument": "time in seconds before a persistent room with no watchers is pruned. 0 disables pruning", # 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-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-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 {})", diff --git a/syncplay/protocols.py b/syncplay/protocols.py index 7e780a5..53cdaf3 100755 --- a/syncplay/protocols.py +++ b/syncplay/protocols.py @@ -11,7 +11,7 @@ from twisted.python.versions import Version from zope.interface.declarations import implementer 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.utils import meetsMinVersion @@ -131,6 +131,10 @@ class SyncClientProtocol(JSONCommandProtocol): self._client.setUsername(username) self._client.setRoom(roomName) self.logged = True + if self.persistentRoomWarning(featureList): + if len(motd) > 0: + motd += "\n\n" + motd += getMessage("persistent-rooms-notice") if motd: self._client.ui.showMessage(motd, True, True) self._client.ui.showMessage(getMessage("connected-successful-notification")) @@ -138,6 +142,9 @@ class SyncClientProtocol(JSONCommandProtocol): self._client.sendFile() self._client.setServerVersion(version, featureList) + def persistentRoomWarning(self, serverFeatures): + return serverFeatures["persistentRooms"] if "persistentRooms" in serverFeatures else False + def sendHello(self): hello = {} hello["username"] = self._client.getUsername() @@ -441,6 +448,8 @@ class SyncServerProtocol(JSONCommandProtocol): self._features["featureList"] = False self._features["readiness"] = meetsMinVersion(self._version, USER_READY_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 def isLogged(self): @@ -496,6 +505,11 @@ class SyncServerProtocol(JSONCommandProtocol): self._logged = True 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 def handleChat(self, chatMessage): if not self._factory.disableChat: @@ -520,8 +534,12 @@ class SyncServerProtocol(JSONCommandProtocol): hello["room"] = {"name": room.getName()} hello["version"] = clientVersion # Used so 1.2.X client works on newer server hello["realversion"] = syncplay.version - hello["motd"] = self._factory.getMotd(userIp, username, room, clientVersion) 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}) @requireLogged @@ -617,11 +635,28 @@ class SyncServerProtocol(JSONCommandProtocol): } 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): userlist = {} watchers = self._factory.getAllWatchersForUser(self._watcher) + dummyCount = 0 for watcher in watchers: 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}) @requireLogged diff --git a/syncplay/server.py b/syncplay/server.py index 58b7c8d..ca5a8ee 100755 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -22,14 +22,14 @@ import syncplay from syncplay import constants from syncplay.messages import getMessage 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): - 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, maxUsernameLength=constants.MAX_USERNAME_LENGTH, statsDbFile=None, tlsCertPath=None): self.isolateRooms = isolateRooms + syncplay.messages.setLanguage(syncplay.messages.getInitialLanguage()) print(getMessage("welcome-server-notification").format(syncplay.version)) self.port = port if password: @@ -41,18 +41,19 @@ class SyncFactory(Factory): print(getMessage("no-salt-notification").format(salt)) self._salt = salt self._motdFilePath = motdFilePath - self._roomsDirPath = roomsDirPath if roomsDirPath is not None and os.path.isdir(roomsDirPath) else None - self._roomsTimer = roomsTimer if roomsDirPath is not None and isinstance(roomsTimer, int) and roomsTimer > 0 else 0 + self.roomsDbFile = roomsDbFile self.disableReady = disableReady self.disableChat = disableChat 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.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: - self._roomManager = RoomManager(self._roomsDirPath, self._roomsTimer) + self._roomManager = RoomManager(self.roomsDbFile, self.permanentRooms) else: self._roomManager = PublicRoomManager() if statsDbFile is not None: - self._statsDbHandle = DBManager(statsDbFile) + self._statsDbHandle = StatsDBManager(statsDbFile) self._statsRecorder = StatsRecorder(self._statsDbHandle, self._roomManager) statsDelay = 5*(int(self.port)%10 + 1) self._statsRecorder.startRecorder(statsDelay) @@ -67,6 +68,16 @@ class SyncFactory(Factory): self.options = None 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): return SyncServerProtocol(self) @@ -82,6 +93,7 @@ class SyncFactory(Factory): features["isolateRooms"] = self.isolateRooms features["readiness"] = not self.disableReady features["managedRooms"] = True + features["persistentRooms"] = self.roomsDbFile is not None features["chat"] = not self.disableChat features["maxChatMessageLength"] = self.maxChatMessageLength features["maxUsernameLength"] = self.maxUsernameLength @@ -137,11 +149,17 @@ class SyncFactory(Factory): l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None) self._roomManager.broadcast(watcher, l) 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): if watcher and watcher.getRoom(): self.sendLeftMessage(watcher) self._roomManager.removeWatcher(watcher) + if self.roomsDbFile: + l = lambda w: w.sendList(toGUIOnly=True) + self._roomManager.broadcast(watcher, l) def sendLeftMessage(self, watcher): 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 self._roomManager.broadcast(watcher, l) 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): if watcher.getFile(): @@ -172,6 +193,9 @@ class SyncFactory(Factory): def getAllWatchersForUser(self, forUser): return self._roomManager.getAllWatchersForUser(forUser) + def getEmptyPersistentRooms(self): + return self._roomManager.getEmptyPersistentRooms() + def authRoomController(self, watcher, password, roomBaseName=None): room = watcher.getRoom() roomName = roomBaseName if roomBaseName else room.getName() @@ -290,8 +314,33 @@ class StatsRecorder(object): except: 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): self._dbPath = dbpath self._connection = None @@ -305,31 +354,74 @@ class DBManager(object): self._createSchema() def _createSchema(self): - initQuery = 'create table if not exists clients_snapshots (snapshot_time integer, version string)' - self._connection.runQuery(initQuery) + initQuery = 'create table if not exists clients_snapshots (snapshot_time INTEGER, version STRING)' + return self._connection.runQuery(initQuery) def addVersionLog(self, timestamp, version): content = (timestamp, version, ) 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): - def __init__(self, roomsDir=None, timer=0): - self._roomsDir = roomsDir - self._timer = timer + def __init__(self, roomsdbfile=None, permanentRooms=[]): + self._roomsDbFile = roomsdbfile self._rooms = {} - if self._roomsDir is not None: - for root, dirs, files in os.walk(self._roomsDir): - for file in files: - if file.endswith(".room"): - room = Room('', self._roomsDir) - room.loadFromFile(os.path.join(root, file)) - roomName = truncateText(room.getName(), constants.MAX_ROOM_NAME_LENGTH) - if len(room.getPlaylist()) == 0 or room.isStale(self._timer): - os.remove(os.path.join(root, file)) - del room - else: - self._rooms[roomName] = room + self._permanentRooms = permanentRooms + if self._roomsDbFile is not None: + self._roomsDbHandle = RoomDBManager(self._roomsDbFile, self.loadRooms) + self._roomsDbHandle.connect() + else: + self._roomsDbHandle = None + + def loadRooms(self, rooms): + roomsLoaded = [] + for roomDetails in rooms: + roomName = truncateText(roomDetails[0], constants.MAX_ROOM_NAME_LENGTH) + 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): room = sender.getRoom() @@ -349,6 +441,20 @@ class RoomManager(object): watchers.append(watcher) 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): roomName = truncateText(roomName, constants.MAX_ROOM_NAME_LENGTH) self.removeWatcher(watcher) @@ -367,16 +473,20 @@ class RoomManager(object): return self._rooms[roomName] else: if RoomPasswordProvider.isControlledRoom(roomName): - room = ControlledRoom(roomName) + room = ControlledRoom(roomName, self._roomsDbHandle) else: if roomName in self._rooms: self._deleteRoomIfEmpty(self._rooms[roomName]) - room = Room(roomName, self._roomsDir) + room = Room(roomName, self._roomsDbHandle) self._rooms[roomName] = room return 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()] def findFreeUsername(self, username): @@ -412,9 +522,9 @@ class Room(object): STATE_PAUSED = 0 STATE_PLAYING = 1 - def __init__(self, name, _roomsDir=None): + def __init__(self, name, roomsdbhandle): self._name = name - self._roomsDir = _roomsDir + self._roomsDbHandle = roomsdbhandle self._watchers = {} self._playState = self.STATE_PAUSED self._setBy = None @@ -423,51 +533,46 @@ class Room(object): self._lastUpdate = time.time() self._lastSavedUpdate = 0 self._position = 0 + self._permanent = False def __str__(self, *args, **kwargs): return self.getName() 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): - return self.roomsCanPersist() + return self._permanent + + def isNotPermanent(self): + return not self.isPermanent() def sanitizeFilename(self, filename, blacklist="<>:/\\|?*\"", placeholder="_"): return ''.join([c if c not in blacklist and ord(c) >= 32 else placeholder for c in filename]) - def writeToFile(self): - if not self.isPermanent(): + def writeToDb(self): + if not self.isPersistent(): return - filename = os.path.join(self._roomsDir, self.sanitizeFilename(self._name)+'.room') - if len(self._playlist) == 0: - 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) + processed_playlist = getListAsMultilineString(self._playlist) + self._roomsDbHandle.saveRoom(self._name, processed_playlist, self._playlistIndex, self._position, self._lastSavedUpdate) - def loadFromFile(self, filename): - with open(filename) as json_file: - data = json.load(json_file) - self._name = truncateText(data['name'], constants.MAX_ROOM_NAME_LENGTH) - self._playlist = data['playlist'] - self._playlistIndex = data['playlistIndex'] - self._position = data['position'] - self._lastSavedUpdate = data['lastSavedUpdate'] - - def isStale(self, timer): - if timer == 0 or self._lastSavedUpdate == 0: - return False - return time.time() - self._lastSavedUpdate > timer + def loadRoom(self, room): + name, playlist, playlistindex, position, lastupdate = room + self._name = name + self._playlist = convertMultilineStringToList(playlist) + self._playlistIndex = playlistindex + self._position = position + self._lastSavedUpdate = lastupdate def getName(self): return self._name @@ -488,14 +593,17 @@ class Room(object): def setPaused(self, paused=STATE_PAUSED, setBy=None): self._playState = paused self._setBy = setBy - self.writeToFile() + self.writeToDb() def setPosition(self, position, setBy=None): self._position = position for watcher in self._watchers.values(): watcher.setPosition(position) self._setBy = setBy - self.writeToFile() + self.writeToDb() + + def setPermanent(self, newState): + self._permanent = newState def isPlaying(self): return self._playState == self.STATE_PLAYING @@ -507,7 +615,7 @@ class Room(object): return list(self._watchers.values()) def addWatcher(self, watcher): - if self._watchers or self.isPermanent(): + if self._watchers or self.isPersistent(): watcher.setPosition(self.getPosition()) self._watchers[watcher.getName()] = watcher watcher.setRoom(self) @@ -517,9 +625,9 @@ class Room(object): return del self._watchers[watcher.getName()] watcher.setRoom(None) - if not self._watchers and not self.isPermanent(): + if not self._watchers and not self.isPersistent(): self._position = 0 - self.writeToFile() + self.writeToDb() def isEmpty(self): return not bool(self._watchers) @@ -532,11 +640,11 @@ class Room(object): def setPlaylist(self, files, setBy=None): self._playlist = files - self.writeToFile() + self.writeToDb() def setPlaylistIndex(self, index, setBy=None): self._playlistIndex = index - self.writeToFile() + self.writeToDb() def getPlaylist(self): return self._playlist @@ -544,10 +652,12 @@ class Room(object): def getPlaylistIndex(self): return self._playlistIndex + def getControllers(self): + return [] class ControlledRoom(Room): - def __init__(self, name): - Room.__init__(self, name) + def __init__(self, name, roomsdbhandle): + Room.__init__(self, name, roomsdbhandle) self._controllers = {} def getPosition(self): @@ -570,6 +680,7 @@ class ControlledRoom(Room): Room.removeWatcher(self, watcher) if watcher.getName() in self._controllers: del self._controllers[watcher.getName()] + self.writeToDb() def setPaused(self, paused=Room.STATE_PAUSED, setBy=None): if self.canControl(setBy): @@ -591,7 +702,7 @@ class ControlledRoom(Room): return watcher.getName() in self._controllers def getControllers(self): - return self._controllers + return {} class Watcher(object): @@ -671,6 +782,23 @@ class Watcher(object): if self._connector.meetsMinVersion(constants.CHAT_MIN_VERSION): 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): 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('--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('--rooms-dir', 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('--rooms-db-file', metavar='rooms', type=str, nargs='?', help=getMessage("server-rooms-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-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/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index 00a0003..f7cec73 100755 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -18,6 +18,7 @@ class ConsoleUI(threading.Thread): self.PromptResult = "" self.promptMode.set() self._syncplayClient = None + self.uiMode = constants.CONSOLE_UI_MODE threading.Thread.__init__(self, name="ConsoleUI") def addClient(self, client): diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 2669885..a6b4e12 100755 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -468,6 +468,9 @@ class MainWindow(QtWidgets.QMainWindow): self.roomsCombobox.clear() for roomListValue in self.config['roomList']: self.roomsCombobox.addItem(roomListValue) + for room in self.currentRooms: + if room not in self.config['roomList']: + self.roomsCombobox.addItem(room) self.roomsCombobox.setEditText(previousRoomSelection) 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.currentRooms = [] 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 = [] roomitem = QtGui.QStandardItem(room) font = QtGui.QFont() @@ -624,6 +636,8 @@ class MainWindow(QtWidgets.QMainWindow): roomitem.setIcon(QtGui.QPixmap(resourcespath + 'chevrons_right.png')) for user in rooms[room]: + if user.username.strip() == "": + continue useritem = QtGui.QStandardItem(user.username) isController = user.isController() sameRoom = room == currentUser.room @@ -695,6 +709,7 @@ class MainWindow(QtWidgets.QMainWindow): self.listTreeView.expandAll() self.updateListGeometry() self._syncplayClient.fileSwitch.setFilenameWatchlist(self.newWatchlist) + self.fillRoomsCombobox() @needsClient def undoPlaylistChange(self): @@ -1753,6 +1768,9 @@ class MainWindow(QtWidgets.QMainWindow): window.autoplayAction.setCheckable(True) 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 @@ -1818,6 +1836,11 @@ class MainWindow(QtWidgets.QMainWindow): def updateAutoplayVisibility(self): self.autoplayFrame.setVisible(self.autoplayAction.isChecked()) + def updateEmptyRoomVisiblity(self): + self.hideEmptyRooms = self.hideEmptyRoomsAction.isChecked() + if self._syncplayClient: + self._syncplayClient.getUserList() + def changeReadyState(self): self.updateReadyIcon() if self._syncplayClient: @@ -2017,6 +2040,7 @@ class MainWindow(QtWidgets.QMainWindow): settings.setValue("pos", self.pos()) settings.setValue("showPlaybackButtons", self.playbackAction.isChecked()) settings.setValue("showAutoPlayButton", self.autoplayAction.isChecked()) + settings.setValue("hideEmptyRooms", self.hideEmptyRoomsAction.isChecked()) settings.setValue("autoplayChecked", self.autoplayPushButton.isChecked()) settings.setValue("autoplayMinUsers", self.autoplayThresholdSpinbox.value()) settings.endGroup() @@ -2040,6 +2064,9 @@ class MainWindow(QtWidgets.QMainWindow): if settings.value("showAutoPlayButton", "false") == "true": self.autoplayAction.setChecked(True) self.updateAutoplayVisibility() + if settings.value("hideEmptyRooms", "false") == "true": + self.hideEmptyRooms = True + self.hideEmptyRoomsAction.setChecked(True) if settings.value("autoplayChecked", "false") == "true": self.updateAutoPlayState(True) self.autoplayPushButton.setChecked(True) @@ -2063,6 +2090,8 @@ class MainWindow(QtWidgets.QMainWindow): self.lastCheckedForUpdates = None self._syncplayClient = None self.folderSearchEnabled = True + self.hideEmptyRooms = False + self.currentRooms = [] self.QtGui = QtGui if isMacOS(): self.setWindowFlags(self.windowFlags()) @@ -2081,3 +2110,4 @@ class MainWindow(QtWidgets.QMainWindow): self.show() self.setAcceptDrops(True) self.clearedPlaylistNote = False + self.uiMode = constants.GRAPHICAL_UI_MODE