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.
.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]

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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:

View File

@ -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 {})",

View File

@ -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 {})",

View File

@ -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 {})",

View File

@ -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 è {})",

View File

@ -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 é {})",

View File

@ -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 é {})",

View File

@ -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": "Максимальное число символов в именах пользователей (по умолчанию {})",

View File

@ -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 {})",

View File

@ -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

View File

@ -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,30 +354,73 @@ 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
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):
@ -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"))

View File

@ -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):

View File

@ -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