From 1bbbab245f3cbe9cfc3d5cc013bbd6d65754fd64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ata=20G=C3=BClalan?= Date: Sun, 17 Oct 2021 15:40:57 +0300 Subject: [PATCH 01/30] Add LANG Parameter (#460) If LANG parameter set, don't show language dialog. Example usage: .\Syncplay-X.X.X-Setup.exe /S /LANG=1033 --- buildPy2exe.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/buildPy2exe.py b/buildPy2exe.py index 362fa32..0747702 100755 --- a/buildPy2exe.py +++ b/buildPy2exe.py @@ -286,7 +286,16 @@ NSIS_SCRIPT_TEMPLATE = r""" Call GetSize Call DriveSpace - Call Language + + $${GetParameters} $$0 + ClearErrors + $${GetOptions} $$0 "/LANG=" $$0 + $${IfNot} $${Errors} + $${AndIf} $$0 <> 0 + StrCpy $$LANGUAGE $$0 + $${Else} + Call Language + $${EndIf} FunctionEnd ;Language selection dialog From 2bf3931f59fb0f5a4a55fa308247c18c1722f75d Mon Sep 17 00:00:00 2001 From: Tremolo4 Date: Sun, 17 Oct 2021 14:44:07 +0200 Subject: [PATCH 02/30] Trusted Domains: Allow trusting a domain regardless of HTTP basic auth credentials (#437) * Trusted Domains: don't consider HTTP basic auth credentials part of the domain name * Trusted Domains: hide "add as trusted domain" menu item if entry does not contain domain * Trusted Domains: strip HTTP basic auth credentials also when adding as trusted domain via context menu --- syncplay/client.py | 54 +++++++++++++++++++++++++++++-------------- syncplay/constants.py | 2 +- syncplay/ui/gui.py | 6 +++-- syncplay/utils.py | 13 +++++++---- 4 files changed, 50 insertions(+), 25 deletions(-) diff --git a/syncplay/client.py b/syncplay/client.py index 36d9632..fb769c6 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -9,9 +9,9 @@ import re import sys import threading import time -from fnmatch import fnmatch from copy import deepcopy from functools import wraps +from urllib.parse import urlparse from twisted.application.internet import ClientService from twisted.internet.endpoints import HostnameEndpoint @@ -546,27 +546,47 @@ class SyncplayClient(object): if oldRoomList != newRoomList: self._config['roomList'] = newRoomList + def _isURITrustableAndTrusted(self, URIToTest): + """Returns a tuple of booleans: (trustable, trusted). + + A given URI is "trustable" if it uses HTTP or HTTPS (constants.TRUSTABLE_WEB_PROTOCOLS). + A given URI is "trusted" if it matches an entry in the trustedDomains config. + Such an entry is considered matching if the domain is the same and the path + is a prefix of the given URI's path. + A "trustable" URI is always "trusted" if the config onlySwitchToTrustedDomains is false. + """ + o = urlparse(URIToTest) + trustable = o.scheme in constants.TRUSTABLE_WEB_PROTOCOLS + if not trustable: + # untrustable URIs are never trusted, return early + return False, False + if not self._config['onlySwitchToTrustedDomains']: + # trust all trustable URIs in this case + return trustable, True + # check for matching trusted domains + if self._config['trustedDomains']: + for entry in self._config['trustedDomains']: + trustedDomain, _, path = entry.partition('/') + if o.hostname not in (trustedDomain, "www." + trustedDomain): + # domain does not match + continue + if path and not o.path.startswith('/' + path): + # trusted domain has a path component and it does not match + continue + # match found, trust this domain + return trustable, True + # no matches found, do not trust this domain + return trustable, False + def isUntrustedTrustableURI(self, URIToTest): if utils.isURL(URIToTest): - for trustedProtocol in constants.TRUSTABLE_WEB_PROTOCOLS: - if URIToTest.startswith(trustedProtocol) and not self.isURITrusted(URIToTest): - return True + trustable, trusted = self._isURITrustableAndTrusted(URIToTest) + return trustable and not trusted return False def isURITrusted(self, URIToTest): - URIToTest = URIToTest+"/" - for trustedProtocol in constants.TRUSTABLE_WEB_PROTOCOLS: - if URIToTest.startswith(trustedProtocol): - if self._config['onlySwitchToTrustedDomains']: - if self._config['trustedDomains']: - for trustedDomain in self._config['trustedDomains']: - trustableURI = ''.join([trustedProtocol, trustedDomain, "/*"]) - if fnmatch(URIToTest, trustableURI): - return True - return False - else: - return True - return False + trustable, trusted = self._isURITrustableAndTrusted(URIToTest) + return trustable and trusted def openFile(self, filePath, resetPosition=False, fromUser=False): if fromUser and filePath.endswith(".txt") or filePath.endswith(".m3u") or filePath.endswith(".m3u8"): diff --git a/syncplay/constants.py b/syncplay/constants.py index 171e22f..b9c0412 100755 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -345,6 +345,6 @@ SYNCPLAY_DOWNLOAD_URL = "https://syncplay.pl/download/" SYNCPLAY_PUBLIC_SERVER_LIST_URL = "https://syncplay.pl/listpublicservers?{}" # Params DEFAULT_TRUSTED_DOMAINS = ["youtube.com", "youtu.be"] -TRUSTABLE_WEB_PROTOCOLS = ["http://www.", "https://www.", "http://", "https://"] +TRUSTABLE_WEB_PROTOCOLS = ["http", "https"] PRIVATE_FILE_FIELDS = ["path"] diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index a4884ef..f2195e9 100755 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -731,7 +731,8 @@ class MainWindow(QtWidgets.QMainWindow): lambda: utils.open_system_file_browser(pathFound)) if self._syncplayClient.isUntrustedTrustableURI(firstFile): domain = utils.getDomainFromURL(firstFile) - menu.addAction(QtGui.QPixmap(resourcespath + "shield_add.png"), getMessage("addtrusteddomain-menu-label").format(domain), lambda: self.addTrustedDomain(domain)) + if domain: + menu.addAction(QtGui.QPixmap(resourcespath + "shield_add.png"), getMessage("addtrusteddomain-menu-label").format(domain), lambda: self.addTrustedDomain(domain)) menu.addAction(QtGui.QPixmap(resourcespath + "delete.png"), getMessage("removefromplaylist-menu-label"), lambda: self.deleteSelectedPlaylistItems()) menu.addSeparator() menu.addAction(QtGui.QPixmap(resourcespath + "arrow_switch.png"), getMessage("shuffleremainingplaylist-menu-label"), lambda: self.shuffleRemainingPlaylist()) @@ -794,7 +795,8 @@ class MainWindow(QtWidgets.QMainWindow): menu.addAction(QtGui.QPixmap(resourcespath + "film_go.png"), getMessage("openusersfile-menu-label").format(shortUsername), lambda: self.openFile(pathFound, resetPosition=False, fromUser=True)) if self._syncplayClient.isUntrustedTrustableURI(filename): domain = utils.getDomainFromURL(filename) - menu.addAction(QtGui.QPixmap(resourcespath + "shield_add.png"), getMessage("addtrusteddomain-menu-label").format(domain), lambda: self.addTrustedDomain(domain)) + if domain: + menu.addAction(QtGui.QPixmap(resourcespath + "shield_add.png"), getMessage("addtrusteddomain-menu-label").format(domain), lambda: self.addTrustedDomain(domain)) if not isURL(filename) and filename != getMessage("nofile-note"): path = self._syncplayClient.fileSwitch.findFilepath(filename) diff --git a/syncplay/utils.py b/syncplay/utils.py index 5a394d3..bfc788c 100755 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -395,12 +395,15 @@ def playlistIsValid(files): def getDomainFromURL(URL): try: - URL = URL.split("//")[-1].split("/")[0] - if URL.startswith("www."): - URL = URL[4:] - return URL - except: + o = urllib.parse.urlparse(URL) + except ValueError: + # not a URL return None + if o.hostname is not None and o.hostname.startswith("www."): + return o.hostname[4:] + else: + # may return None if URL does not have domain (invalid url) + return o.hostname def open_system_file_browser(path): From bbcef5d82c1316ff41b95d4ef229d0ea850c41f7 Mon Sep 17 00:00:00 2001 From: Ridan Vandenbergh Date: Mon, 18 Oct 2021 21:10:35 +0200 Subject: [PATCH 03/30] Allow .m3u/.m3u8 files to be played from network (#419) --- syncplay/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/syncplay/client.py b/syncplay/client.py index fb769c6..acc4027 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -589,7 +589,8 @@ class SyncplayClient(object): return trustable and trusted def openFile(self, filePath, resetPosition=False, fromUser=False): - if fromUser and filePath.endswith(".txt") or filePath.endswith(".m3u") or filePath.endswith(".m3u8"): + if not (filePath.startswith("http://") or filePath.startswith("https://"))\ + and ((fromUser and filePath.endswith(".txt")) or filePath.endswith(".m3u") or filePath.endswith(".m3u8")): self.playlist.loadPlaylistFromFile(filePath, resetPosition) return From 69621ee71042de0ee226a930bfed038ce139b45b Mon Sep 17 00:00:00 2001 From: et0h Date: Mon, 18 Oct 2021 20:30:31 +0100 Subject: [PATCH 04/30] Fix room name case sensitivity UI issue (#403) --- syncplay/ui/GuiConfiguration.py | 3 +++ syncplay/ui/gui.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/syncplay/ui/GuiConfiguration.py b/syncplay/ui/GuiConfiguration.py index 7c43076..4a1d1e9 100755 --- a/syncplay/ui/GuiConfiguration.py +++ b/syncplay/ui/GuiConfiguration.py @@ -655,6 +655,9 @@ class ConfigDialog(QtWidgets.QDialog): self.serverpassLabel = QLabel(getMessage("password-label"), self) self.roomsCombobox = QtWidgets.QComboBox(self) self.roomsCombobox.setEditable(True) + caseSensitiveCompleter = QtWidgets.QCompleter("", self) + caseSensitiveCompleter.setCaseSensitivity(Qt.CaseSensitive) + self.roomsCombobox.setCompleter(caseSensitiveCompleter) self.fillRoomsCombobox() self.roomsCombobox.setEditText(config['room']) self.usernameLabel = QLabel(getMessage("name-label"), self) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index f2195e9..24890d4 100755 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -1491,6 +1491,9 @@ class MainWindow(QtWidgets.QMainWindow): window.listLayout.addWidget(window.listSplit) window.roomsCombobox = QtWidgets.QComboBox(self) window.roomsCombobox.setEditable(True) + caseSensitiveCompleter = QtWidgets.QCompleter("", self) + caseSensitiveCompleter.setCaseSensitivity(Qt.CaseSensitive) + window.roomsCombobox.setCompleter(caseSensitiveCompleter) #window.roomsCombobox.setMaxLength(constants.MAX_ROOM_NAME_LENGTH) window.roomButton = QtWidgets.QPushButton( QtGui.QPixmap(resourcespath + 'door_in.png'), From 0b8c2ddc4b82833c2ec3bcc26673f2b8d5a13e79 Mon Sep 17 00:00:00 2001 From: et0h Date: Mon, 18 Oct 2021 20:36:27 +0100 Subject: [PATCH 05/30] Remove redundant help button from dialogs (#403) --- syncplay/ui/GuiConfiguration.py | 2 +- syncplay/ui/gui.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/syncplay/ui/GuiConfiguration.py b/syncplay/ui/GuiConfiguration.py index 4a1d1e9..4feffb3 100755 --- a/syncplay/ui/GuiConfiguration.py +++ b/syncplay/ui/GuiConfiguration.py @@ -177,6 +177,7 @@ class ConfigDialog(QtWidgets.QDialog): RoomsLayout.addWidget(RoomsButtonBox, 2, 0, 1, 1) RoomsDialog.setLayout(RoomsLayout) RoomsDialog.setModal(True) + RoomsDialog.setWindowFlags(RoomsDialog.windowFlags() & ~Qt.WindowContextHelpButtonHint) RoomsDialog.show() result = RoomsDialog.exec_() if result == QtWidgets.QDialog.Accepted: @@ -668,7 +669,6 @@ class ConfigDialog(QtWidgets.QDialog): self.editRoomsButton.setIcon(QtGui.QIcon(resourcespath + 'bullet_edit_centered.png')) self.editRoomsButton.setObjectName(constants.LOAD_SAVE_MANUALLY_MARKER + "edit-rooms") self.editRoomsButton.released.connect(self.openRoomsDialog) - self.hostLabel.setObjectName("host") self.hostCombobox.setObjectName(constants.LOAD_SAVE_MANUALLY_MARKER + "host") self.usernameLabel.setObjectName("name") diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 24890d4..71a8011 100755 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -1145,6 +1145,7 @@ class MainWindow(QtWidgets.QMainWindow): URIsLayout.addWidget(URIsButtonBox, 2, 0, 1, 1) URIsDialog.setLayout(URIsLayout) URIsDialog.setModal(True) + URIsDialog.setWindowFlags(URIsDialog.windowFlags() & ~Qt.WindowContextHelpButtonHint) URIsDialog.show() result = URIsDialog.exec_() if result == QtWidgets.QDialog.Accepted: @@ -1175,6 +1176,7 @@ class MainWindow(QtWidgets.QMainWindow): RoomsLayout.addWidget(RoomsButtonBox, 2, 0, 1, 1) RoomsDialog.setLayout(RoomsLayout) RoomsDialog.setModal(True) + RoomsDialog.setWindowFlags(RoomsDialog.windowFlags() & ~Qt.WindowContextHelpButtonHint) RoomsDialog.show() result = RoomsDialog.exec_() if result == QtWidgets.QDialog.Accepted: @@ -1209,6 +1211,7 @@ class MainWindow(QtWidgets.QMainWindow): editPlaylistDialog.setModal(True) editPlaylistDialog.setMinimumWidth(600) editPlaylistDialog.setMinimumHeight(500) + editPlaylistDialog.setWindowFlags(editPlaylistDialog.windowFlags() & ~Qt.WindowContextHelpButtonHint) editPlaylistDialog.show() result = editPlaylistDialog.exec_() if result == QtWidgets.QDialog.Accepted: @@ -1239,6 +1242,7 @@ class MainWindow(QtWidgets.QMainWindow): MediaDirectoriesAddFolderButton.pressed.connect(lambda: self.openAddMediaDirectoryDialog(MediaDirectoriesTextbox, MediaDirectoriesDialog)) MediaDirectoriesLayout.addWidget(MediaDirectoriesAddFolderButton, 1, 1, 1, 1, Qt.AlignTop) MediaDirectoriesDialog.setLayout(MediaDirectoriesLayout) + MediaDirectoriesDialog.setWindowFlags(MediaDirectoriesDialog.windowFlags() & ~Qt.WindowContextHelpButtonHint) MediaDirectoriesDialog.setModal(True) MediaDirectoriesDialog.show() result = MediaDirectoriesDialog.exec_() @@ -1264,6 +1268,7 @@ class MainWindow(QtWidgets.QMainWindow): TrustedDomainsButtonBox.rejected.connect(TrustedDomainsDialog.reject) TrustedDomainsLayout.addWidget(TrustedDomainsButtonBox, 2, 0, 1, 1) TrustedDomainsDialog.setLayout(TrustedDomainsLayout) + TrustedDomainsDialog.setWindowFlags(TrustedDomainsDialog.windowFlags() & ~Qt.WindowContextHelpButtonHint) TrustedDomainsDialog.setModal(True) TrustedDomainsDialog.show() result = TrustedDomainsDialog.exec_() From 16168effc488756e38509eb35f6054d1c94f7906 Mon Sep 17 00:00:00 2001 From: Alberto Sottile Date: Sun, 12 Sep 2021 00:24:21 +0200 Subject: [PATCH 06/30] darkdetect: update vendor copy to 0.5.0 --- syncplay/vendor/darkdetect/__init__.py | 20 +++++++++---- syncplay/vendor/darkdetect/_dummy.py | 0 syncplay/vendor/darkdetect/_linux_detect.py | 30 +++++++++++++++++++ .../darkdetect/{_detect.py => _mac_detect.py} | 18 +++++++---- syncplay/vendor/darkdetect/_windows_detect.py | 18 +++++++++++ 5 files changed, 76 insertions(+), 10 deletions(-) mode change 100755 => 100644 syncplay/vendor/darkdetect/__init__.py mode change 100755 => 100644 syncplay/vendor/darkdetect/_dummy.py create mode 100644 syncplay/vendor/darkdetect/_linux_detect.py rename syncplay/vendor/darkdetect/{_detect.py => _mac_detect.py} (69%) mode change 100755 => 100644 create mode 100644 syncplay/vendor/darkdetect/_windows_detect.py diff --git a/syncplay/vendor/darkdetect/__init__.py b/syncplay/vendor/darkdetect/__init__.py old mode 100755 new mode 100644 index 4cce200..bb1df11 --- a/syncplay/vendor/darkdetect/__init__.py +++ b/syncplay/vendor/darkdetect/__init__.py @@ -4,19 +4,29 @@ # Distributed under the terms of the 3-clause BSD License. #----------------------------------------------------------------------------- -__version__ = '0.1.1' +__version__ = '0.5.0' import sys import platform -if sys.platform != "darwin": - from ._dummy import * -else: +if sys.platform == "darwin": from distutils.version import LooseVersion as V if V(platform.mac_ver()[0]) < V("10.14"): from ._dummy import * else: - from ._detect import * + from ._mac_detect import * del V +elif sys.platform == "win32" and platform.release() == "10": + # Checks if running Windows 10 version 10.0.14393 (Anniversary Update) or higher. The getwindowsversion method returns a tuple. + # The third item is the build number that we can use to check if the user has a new enough version of Windows. + winver = int(sys.getwindowsversion()[2]) + if winver >= 14393: + from ._windows_detect import * + else: + from ._dummy import * +elif sys.platform == "linux": + from ._linux_detect import * +else: + from ._dummy import * del sys, platform \ No newline at end of file diff --git a/syncplay/vendor/darkdetect/_dummy.py b/syncplay/vendor/darkdetect/_dummy.py old mode 100755 new mode 100644 diff --git a/syncplay/vendor/darkdetect/_linux_detect.py b/syncplay/vendor/darkdetect/_linux_detect.py new file mode 100644 index 0000000..3110ae8 --- /dev/null +++ b/syncplay/vendor/darkdetect/_linux_detect.py @@ -0,0 +1,30 @@ +#----------------------------------------------------------------------------- +# Copyright (C) 2019 Alberto Sottile, Eric Larson +# +# Distributed under the terms of the 3-clause BSD License. +#----------------------------------------------------------------------------- + +import subprocess + + +def theme(): + # Here we just triage to GTK settings for now + try: + out = subprocess.run( + ['gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-theme'], + capture_output=True) + stdout = out.stdout.decode() + except Exception: + return 'Light' + # we have a string, now remove start and end quote + theme = stdout.lower().strip()[1:-1] + if theme.endswith('-dark'): + return 'Dark' + else: + return 'Light' + +def isDark(): + return theme() == 'Dark' + +def isLight(): + return theme() == 'Light' diff --git a/syncplay/vendor/darkdetect/_detect.py b/syncplay/vendor/darkdetect/_mac_detect.py old mode 100755 new mode 100644 similarity index 69% rename from syncplay/vendor/darkdetect/_detect.py rename to syncplay/vendor/darkdetect/_mac_detect.py index 9a79f7c..322582f --- a/syncplay/vendor/darkdetect/_detect.py +++ b/syncplay/vendor/darkdetect/_mac_detect.py @@ -6,19 +6,27 @@ import ctypes import ctypes.util +import platform -appkit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('AppKit')) -objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc')) +from distutils.version import LooseVersion as V + +if V(platform.mac_ver()[0]) < V("10.16"): + appkit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('AppKit')) + objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc')) +else: + appkit = ctypes.cdll.LoadLibrary('AppKit.framework/AppKit') + objc = ctypes.cdll.LoadLibrary('libobjc.dylib') +del V void_p = ctypes.c_void_p ull = ctypes.c_uint64 objc.objc_getClass.restype = void_p objc.sel_registerName.restype = void_p -objc.objc_msgSend.restype = void_p -objc.objc_msgSend.argtypes = [void_p, void_p] -msg = objc.objc_msgSend +# See https://docs.python.org/3/library/ctypes.html#function-prototypes for arguments description +MSGPROTOTYPE = ctypes.CFUNCTYPE(void_p, void_p, void_p, void_p) +msg = MSGPROTOTYPE(('objc_msgSend', objc), ((1 ,'', None), (1, '', None), (1, '', None))) def _utf8(s): if not isinstance(s, bytes): diff --git a/syncplay/vendor/darkdetect/_windows_detect.py b/syncplay/vendor/darkdetect/_windows_detect.py new file mode 100644 index 0000000..61409a7 --- /dev/null +++ b/syncplay/vendor/darkdetect/_windows_detect.py @@ -0,0 +1,18 @@ +from winreg import HKEY_CURRENT_USER as hkey, QueryValueEx as getSubkeyValue, OpenKey as getKey + +def theme(): + """ Uses the Windows Registry to detect if the user is using Dark Mode """ + # Registry will return 0 if Windows is in Dark Mode and 1 if Windows is in Light Mode. This dictionary converts that output into the text that the program is expecting. + valueMeaning = {0: "Dark", 1: "Light"} + # In HKEY_CURRENT_USER, get the Personalisation Key. + key = getKey(hkey, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize") + # In the Personalisation Key, get the AppsUseLightTheme subkey. This returns a tuple. + # The first item in the tuple is the result we want (0 or 1 indicating Dark Mode or Light Mode); the other value is the type of subkey e.g. DWORD, QWORD, String, etc. + subkey = getSubkeyValue(key, "AppsUseLightTheme")[0] + return valueMeaning[subkey] + +def isDark(): + return theme() == 'Dark' + +def isLight(): + return theme() == 'Light' \ No newline at end of file From 8f44c28c739c66e746e482ba50effb6a14476858 Mon Sep 17 00:00:00 2001 From: Alberto Sottile Date: Sun, 19 Sep 2021 17:19:16 +0200 Subject: [PATCH 07/30] macOS build: upgrade to Python 3.9 and PySide2 5.15.2 --- .github/workflows/build.yml | 24 +++++++++++---- buildPy2app.py | 1 - ci/macos-deploy.sh | 2 +- ci/macos_app_cleaner.py | 61 ++++++++++++++++++++++++++++++++++--- 4 files changed, 76 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ec2d98d..ac4302c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -71,7 +71,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: '3.7' + python-version: '3.9' - name: Check Python install run: | @@ -84,8 +84,20 @@ jobs: run: | pip3 install -U setuptools wheel pip pip3 install twisted[tls] appnope requests certifi - pip3 install shiboken2==5.13.1 pyside2==5.13.1 - pip3 install py2app + pip3 install shiboken2==5.15.2 pyside2==5.15.2 + pip3 install altgraph modulegraph macholib + + - name: Install py2app + run: | + git clone https://github.com/albertosottile/py2app.git + cd py2app + git checkout stubs + cd py2app/apptemplate + python3 setup.py build + cd ../.. + python3 setup.py build + python3 setup.py install + cd .. - name: Check Python dependencies run: | @@ -96,13 +108,13 @@ jobs: python3 -c "from py2app.recipes import pyside2" echo $DYLD_LIBRARY_PATH echo $DYLD_FRAMEWORK_PATH - python3 -c 'from distutils.sysconfig import get_config_var; print(get_config_var("LDLIBRARY"))' + # python3 -c 'from distutils.sysconfig import get_config_var; print(get_config_var("LDLIBRARY"))' - name: Build run: | python3 ci/pyside2_linker.py - export LIBPYTHON_FOLDER="$(python3 -c 'from distutils.sysconfig import get_config_var; print(get_config_var("LIBDIR"))')" - ln -s $LIBPYTHON_FOLDER/libpython3.7m.dylib $LIBPYTHON_FOLDER/libpython3.7.dylib + # export LIBPYTHON_FOLDER="$(python3 -c 'from distutils.sysconfig import get_config_var; print(get_config_var("LIBDIR"))')" + # ln -s $LIBPYTHON_FOLDER/libpython3.9m.dylib $LIBPYTHON_FOLDER/libpython3.9.dylib export DYLD_FRAMEWORK_PATH="$(python3 -c 'from PySide2.QtCore import QLibraryInfo; print(QLibraryInfo.location(QLibraryInfo.LibrariesPath))')" export DYLD_LIBRARY_PATH="$(python3 -c 'import os.path, PySide2; print(os.path.dirname(PySide2.__file__))'):$(python3 -c 'import os.path, shiboken2; print(os.path.dirname(shiboken2.__file__))')" python3 buildPy2app.py py2app diff --git a/buildPy2app.py b/buildPy2app.py index f3271e1..44f584e 100755 --- a/buildPy2app.py +++ b/buildPy2app.py @@ -31,7 +31,6 @@ OPTIONS = { 'CFBundleIdentifier': 'pl.syncplay.Syncplay', 'LSMinimumSystemVersion': '10.12.0', 'NSHumanReadableCopyright': 'Copyright © 2019 Syncplay All Rights Reserved', - 'NSRequiresAquaSystemAppearance': False, } } diff --git a/ci/macos-deploy.sh b/ci/macos-deploy.sh index 8478444..48b0d92 100755 --- a/ci/macos-deploy.sh +++ b/ci/macos-deploy.sh @@ -17,4 +17,4 @@ mkdir dist/Syncplay.app/Contents/Resources/es_419.lproj pip3 install dmgbuild mv syncplay/resources/macOS_readme.pdf syncplay/resources/.macOS_readme.pdf -dmgbuild -s appdmg.py "Syncplay" dist_actions/Syncplay_${VER}.dmg +python3 -m dmgbuild -s appdmg.py "Syncplay" dist_actions/Syncplay_${VER}.dmg diff --git a/ci/macos_app_cleaner.py b/ci/macos_app_cleaner.py index 4fc9fe3..27d1731 100644 --- a/ci/macos_app_cleaner.py +++ b/ci/macos_app_cleaner.py @@ -1,12 +1,20 @@ import os +import platform +import shutil import zipfile +from glob import glob + +pyver = platform.python_version_tuple()[0] + platform.python_version_tuple()[1] + +# clean Python library zip archive + PATH = 'dist/Syncplay.app/Contents/Resources/lib' -zin = zipfile.ZipFile(f'{PATH}/python37.zip', 'r') +zin = zipfile.ZipFile(f'{PATH}/python{pyver}.zip', 'r') tbd = [path for path in zin.namelist() if 'PySide2/Qt/' in path] -zout = zipfile.ZipFile(f'{PATH}/python37_new.zip', 'w', zipfile.ZIP_DEFLATED) +zout = zipfile.ZipFile(f'{PATH}/python{pyver}_new.zip', 'w', zipfile.ZIP_DEFLATED) for item in zin.namelist(): buffer = zin.read(item) @@ -16,5 +24,50 @@ for item in zin.namelist(): zout.close() zin.close() -os.remove(f'{PATH}/python37.zip') -os.rename(f'{PATH}/python37_new.zip', f'{PATH}/python37.zip') +os.remove(f'{PATH}/python{pyver}.zip') +os.rename(f'{PATH}/python{pyver}_new.zip', f'{PATH}/python{pyver}.zip') + +# clean Frameworks folder + +PATH = 'dist/Syncplay.app/Contents/Frameworks' + +to_be_kept = ['QtCore', 'QtDBus', 'QtGui', 'QtNetwork', 'QtPrintSupport', 'QtQml', 'QtWidgets'] +to_be_deleted = [] + +for f in glob(f'{PATH}/Qt*'): + if not any({k in f for k in to_be_kept}): + to_be_deleted.append(f) + +for p in to_be_deleted: + if os.path.isdir(p): + shutil.rmtree(p, ignore_errors=True) + else: + os.remove(p) + +# Clean PySide2 folder + +PATH = 'dist/Syncplay.app/Contents/Resources/lib/python3.9/PySide2' + +shutil.rmtree(f'{PATH}/examples', ignore_errors=True) + +to_be_kept = ['QtCore', 'QtDBus', 'QtGui', 'QtNetwork', 'QtPrintSupport', 'QtQml', 'QtWidgets'] +to_be_deleted = [] + +for f in glob(f'{PATH}/Qt*'): + if not any({k in f for k in to_be_kept}): + to_be_deleted.append(f) + +for p in to_be_deleted: + if os.path.isdir(p): + shutil.rmtree(p, ignore_errors=True) + else: + os.remove(p) + +# move .so from Framework to PySide2 folder + +FROM = 'dist/Syncplay.app/Contents/Frameworks' +TO = 'dist/Syncplay.app/Contents/Resources/lib/python3.9/PySide2' + +for f in glob(f'{FROM}/Qt*.so'): + fn = os.path.basename(f) + shutil.move(f, f'{TO}/{fn}') From 9e3e1cb85732a04de1d99a7657c5266e4c0af18d Mon Sep 17 00:00:00 2001 From: daniel-123 <1662391+daniel-123@users.noreply.github.com> Date: Sun, 24 Oct 2021 20:30:43 +0200 Subject: [PATCH 08/30] Remove Encoding from .desktop files as it's depreciated now. --- syncplay/resources/syncplay-server.desktop | 1 - syncplay/resources/syncplay.desktop | 1 - 2 files changed, 2 deletions(-) diff --git a/syncplay/resources/syncplay-server.desktop b/syncplay/resources/syncplay-server.desktop index da76391..111b5cf 100644 --- a/syncplay/resources/syncplay-server.desktop +++ b/syncplay/resources/syncplay-server.desktop @@ -1,5 +1,4 @@ [Desktop Entry] -Encoding=UTF-8 Name=Syncplay server Comment=Synchronize video playback over network Exec=syncplay-server %u diff --git a/syncplay/resources/syncplay.desktop b/syncplay/resources/syncplay.desktop index a339c99..f649818 100644 --- a/syncplay/resources/syncplay.desktop +++ b/syncplay/resources/syncplay.desktop @@ -1,5 +1,4 @@ [Desktop Entry] -Encoding=UTF-8 Name=Syncplay Comment=Synchronize video playback over network Exec=syncplay %u From 0d9797e48d3ba2457fb5754b88f0c5da6289eac6 Mon Sep 17 00:00:00 2001 From: daniel-123 <1662391+daniel-123@users.noreply.github.com> Date: Sun, 24 Oct 2021 20:38:02 +0200 Subject: [PATCH 09/30] Add Keywords entry to .desktop files. --- syncplay/resources/syncplay-server.desktop | 1 + syncplay/resources/syncplay.desktop | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/syncplay/resources/syncplay-server.desktop b/syncplay/resources/syncplay-server.desktop index 111b5cf..91f94ef 100644 --- a/syncplay/resources/syncplay-server.desktop +++ b/syncplay/resources/syncplay-server.desktop @@ -6,3 +6,4 @@ Terminal=true Type=Application Icon=syncplay Categories=AudioVideo;Audio;Video; +Keywords=syncplay;server;sync; diff --git a/syncplay/resources/syncplay.desktop b/syncplay/resources/syncplay.desktop index f649818..71420bf 100644 --- a/syncplay/resources/syncplay.desktop +++ b/syncplay/resources/syncplay.desktop @@ -6,4 +6,4 @@ Type=Application Icon=syncplay Categories=AudioVideo;Audio;Video; MimeType=audio/ac3;audio/mp4;audio/mpeg;audio/vnd.rn-realaudio;audio/vorbis;audio/x-adpcm;audio/x-matroska;audio/x-mp2;audio/x-mp3;audio/x-ms-wma;audio/x-vorbis;audio/x-wav;audio/mpegurl;audio/x-mpegurl;audio/x-pn-realaudio;audio/x-scpls;video/avi;video/mp4;video/flv;video/mpeg;video/quicktime;video/vnd.rn-realvideo;video/x-matroska;video/x-ms-asf;video/x-msvideo;video/x-ms-wmv;video/x-ogm;video/x-theora; - +Keywords=syncplay;media;video;sync; From a593b850c1ff771c08cd678e922ffc625cab1522 Mon Sep 17 00:00:00 2001 From: Luke Dashjr Date: Wed, 27 Oct 2021 18:03:13 +0000 Subject: [PATCH 10/30] Console: Document setting offset in help (#435) --- syncplay/messages_de.py | 1 + syncplay/messages_en.py | 1 + syncplay/messages_es.py | 1 + syncplay/messages_it.py | 1 + syncplay/messages_pt_BR.py | 1 + syncplay/messages_pt_PT.py | 1 + syncplay/messages_ru.py | 1 + syncplay/messages_tr.py | 1 + syncplay/ui/consoleUI.py | 1 + 9 files changed, 9 insertions(+) diff --git a/syncplay/messages_de.py b/syncplay/messages_de.py index 38b033b..b1609d6 100755 --- a/syncplay/messages_de.py +++ b/syncplay/messages_de.py @@ -82,6 +82,7 @@ de = { "commandlist-notification/undo": "\tu - Letzter Zeitsprung rückgängig", "commandlist-notification/pause": "\tp - Pausieren / weiter", "commandlist-notification/seek": "\t[s][+-]Zeit - zu einer bestimmten Zeit spulen, ohne + oder - wird als absolute Zeit gewertet; Angabe in Sekunden oder Minuten:Sekunden", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", "commandlist-notification/help": "\th - Diese Hilfe", "commandlist-notification/toggle": "\tt - Bereitschaftsanzeige umschalten", "commandlist-notification/create": "\tc [name] - erstelle zentral gesteuerten Raum mit dem aktuellen Raumnamen", diff --git a/syncplay/messages_en.py b/syncplay/messages_en.py index a546a45..e8e5320 100755 --- a/syncplay/messages_en.py +++ b/syncplay/messages_en.py @@ -82,6 +82,7 @@ en = { "commandlist-notification/undo": "\tu - undo last seek", "commandlist-notification/pause": "\tp - toggle pause", "commandlist-notification/seek": "\t[s][+-]time - seek to the given value of time, if + or - is not specified it's absolute time in seconds or min:sec", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", "commandlist-notification/help": "\th - this help", "commandlist-notification/toggle": "\tt - toggles whether you are ready to watch or not", "commandlist-notification/create": "\tc [name] - create managed room using name of current room", diff --git a/syncplay/messages_es.py b/syncplay/messages_es.py index 30e3b98..07de723 100644 --- a/syncplay/messages_es.py +++ b/syncplay/messages_es.py @@ -82,6 +82,7 @@ es = { "commandlist-notification/undo": "\tu - deshacer última búsqueda", "commandlist-notification/pause": "\tp - activar pausa", "commandlist-notification/seek": "\t[s][+-]tiempo - ir al tiempo definido, si no se especifica + o -, será el tiempo absoluto en segundos o min:sec", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", "commandlist-notification/help": "\th - esta ayuda", "commandlist-notification/toggle": "\tt - activa/inactiva señal que estás listo para ver", "commandlist-notification/create": "\tc [nombre] - crear sala administrada usando el nombre de la sala actual", diff --git a/syncplay/messages_it.py b/syncplay/messages_it.py index d532355..fe43aa9 100755 --- a/syncplay/messages_it.py +++ b/syncplay/messages_it.py @@ -82,6 +82,7 @@ it = { "commandlist-notification/undo": "\tu - annulla l'ultima ricerca", "commandlist-notification/pause": "\tp - attiva o disattiva la pausa", "commandlist-notification/seek": "\t[s][+-]tempo - salta all'istante di tempo dato, se + o - non è specificato si considera il tempo assoluto in secondi o min:sec", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", "commandlist-notification/help": "\th - mostra questo help", "commandlist-notification/toggle": "\tt - attiva o disattiva la funzionalità \"pronto\"", "commandlist-notification/create": "\tc [nome] - crea una stanza gestita usando il nome della stanza attuale", diff --git a/syncplay/messages_pt_BR.py b/syncplay/messages_pt_BR.py index 7acdfb1..362873c 100644 --- a/syncplay/messages_pt_BR.py +++ b/syncplay/messages_pt_BR.py @@ -83,6 +83,7 @@ pt_BR = { "commandlist-notification/undo": "\tu - desfaz último salto", "commandlist-notification/pause": "\tp - alterna pausa", "commandlist-notification/seek": "\t[s][+-]time - salta para o valor de tempo dado, se + ou - não forem especificados, será o tempo absoluto em segundos ou minutos:segundos", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", "commandlist-notification/help": "\th - esta mensagem de ajuda", "commandlist-notification/toggle": "\tt - alterna o seu status de prontidão para assistir", "commandlist-notification/create": "\tc [nome] - cria sala gerenciado usando o nome da sala atual", diff --git a/syncplay/messages_pt_PT.py b/syncplay/messages_pt_PT.py index ddfd285..9e169fb 100644 --- a/syncplay/messages_pt_PT.py +++ b/syncplay/messages_pt_PT.py @@ -82,6 +82,7 @@ pt_PT = { "commandlist-notification/undo": "\tu - desfaz último salto", "commandlist-notification/pause": "\tp - alterna pausa", "commandlist-notification/seek": "\t[s][+-]time - salta para o valor de tempo dado, se + ou - não forem especificados, será o tempo absoluto em segundos ou minutos:segundos", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", "commandlist-notification/help": "\th - esta mensagem de ajuda", "commandlist-notification/toggle": "\tt - alterna o seu status de prontidão para assistir", "commandlist-notification/create": "\tc [nome] - cria sala gerenciada usando o nome da sala atual", diff --git a/syncplay/messages_ru.py b/syncplay/messages_ru.py index ed4be81..0b4fa4e 100755 --- a/syncplay/messages_ru.py +++ b/syncplay/messages_ru.py @@ -81,6 +81,7 @@ ru = { "commandlist-notification/undo": "\tu - отменить последнюю перемотку", "commandlist-notification/pause": "\tp - вкл./выкл. паузу", "commandlist-notification/seek": "\t[s][+-]time - перемотать к заданному моменту времени, если не указан + или -, то время считается абсолютным (от начала файла) в секундах или мин:сек", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", "commandlist-notification/help": "\th - помощь", "commandlist-notification/toggle": "\tt - переключить статус готов/не готов к просмотру", "commandlist-notification/create": "\tc [имя комнаты] - создать управляемую комнату с таким же именем, как у текущей", diff --git a/syncplay/messages_tr.py b/syncplay/messages_tr.py index 8d78fc0..cdc43ad 100644 --- a/syncplay/messages_tr.py +++ b/syncplay/messages_tr.py @@ -82,6 +82,7 @@ tr = { "commandlist-notification/undo": "\tu - son isteği geri alır", "commandlist-notification/pause": "\tp - duraklatmayı değiştirir", "commandlist-notification/seek": "\t[s][+-]time - verilen zaman değerine atlar, eğer + veya - belirtilmezse, saniye:dakika cinsinden mutlak zamandır.", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", "commandlist-notification/help": "\th - yardım", "commandlist-notification/toggle": "\tt - izlemeye hazır olup olmadığınızı değiştirir", "commandlist-notification/create": "\tc [name] - mevcut odanın adını kullanarak yönetilen oda oluştur", diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index 1e96494..7363ae6 100755 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -237,6 +237,7 @@ class ConsoleUI(threading.Thread): self.showMessage(getMessage("commandlist-notification/undo"), True) self.showMessage(getMessage("commandlist-notification/pause"), True) self.showMessage(getMessage("commandlist-notification/seek"), True) + self.showMessage(getMessage("commandlist-notification/offset"), True) self.showMessage(getMessage("commandlist-notification/help"), True) self.showMessage(getMessage("commandlist-notification/toggle"), True) self.showMessage(getMessage("commandlist-notification/create"), True) From 625512fc1db12f6b7f934301b95be629b907cbb8 Mon Sep 17 00:00:00 2001 From: et0h Date: Wed, 27 Oct 2021 19:12:10 +0100 Subject: [PATCH 11/30] Add deprecation notice for offset help (#435) --- syncplay/messages_de.py | 2 +- syncplay/messages_en.py | 2 +- syncplay/messages_es.py | 2 +- syncplay/messages_it.py | 2 +- syncplay/messages_pt_BR.py | 2 +- syncplay/messages_pt_PT.py | 2 +- syncplay/messages_ru.py | 2 +- syncplay/messages_tr.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/syncplay/messages_de.py b/syncplay/messages_de.py index b1609d6..b9060bb 100755 --- a/syncplay/messages_de.py +++ b/syncplay/messages_de.py @@ -82,7 +82,7 @@ de = { "commandlist-notification/undo": "\tu - Letzter Zeitsprung rückgängig", "commandlist-notification/pause": "\tp - Pausieren / weiter", "commandlist-notification/seek": "\t[s][+-]Zeit - zu einer bestimmten Zeit spulen, ohne + oder - wird als absolute Zeit gewertet; Angabe in Sekunden oder Minuten:Sekunden", - "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position - this is a deprecated feature", "commandlist-notification/help": "\th - Diese Hilfe", "commandlist-notification/toggle": "\tt - Bereitschaftsanzeige umschalten", "commandlist-notification/create": "\tc [name] - erstelle zentral gesteuerten Raum mit dem aktuellen Raumnamen", diff --git a/syncplay/messages_en.py b/syncplay/messages_en.py index e8e5320..71302d4 100755 --- a/syncplay/messages_en.py +++ b/syncplay/messages_en.py @@ -82,7 +82,7 @@ en = { "commandlist-notification/undo": "\tu - undo last seek", "commandlist-notification/pause": "\tp - toggle pause", "commandlist-notification/seek": "\t[s][+-]time - seek to the given value of time, if + or - is not specified it's absolute time in seconds or min:sec", - "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position - this is a deprecated feature", "commandlist-notification/help": "\th - this help", "commandlist-notification/toggle": "\tt - toggles whether you are ready to watch or not", "commandlist-notification/create": "\tc [name] - create managed room using name of current room", diff --git a/syncplay/messages_es.py b/syncplay/messages_es.py index 07de723..7283b28 100644 --- a/syncplay/messages_es.py +++ b/syncplay/messages_es.py @@ -82,7 +82,7 @@ es = { "commandlist-notification/undo": "\tu - deshacer última búsqueda", "commandlist-notification/pause": "\tp - activar pausa", "commandlist-notification/seek": "\t[s][+-]tiempo - ir al tiempo definido, si no se especifica + o -, será el tiempo absoluto en segundos o min:sec", - "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position - this is a deprecated feature", # TODO: Translate "commandlist-notification/help": "\th - esta ayuda", "commandlist-notification/toggle": "\tt - activa/inactiva señal que estás listo para ver", "commandlist-notification/create": "\tc [nombre] - crear sala administrada usando el nombre de la sala actual", diff --git a/syncplay/messages_it.py b/syncplay/messages_it.py index fe43aa9..42c62ea 100755 --- a/syncplay/messages_it.py +++ b/syncplay/messages_it.py @@ -82,7 +82,7 @@ it = { "commandlist-notification/undo": "\tu - annulla l'ultima ricerca", "commandlist-notification/pause": "\tp - attiva o disattiva la pausa", "commandlist-notification/seek": "\t[s][+-]tempo - salta all'istante di tempo dato, se + o - non è specificato si considera il tempo assoluto in secondi o min:sec", - "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position - this is a deprecated feature", # TODO: Translate "commandlist-notification/help": "\th - mostra questo help", "commandlist-notification/toggle": "\tt - attiva o disattiva la funzionalità \"pronto\"", "commandlist-notification/create": "\tc [nome] - crea una stanza gestita usando il nome della stanza attuale", diff --git a/syncplay/messages_pt_BR.py b/syncplay/messages_pt_BR.py index 362873c..c75fb7d 100644 --- a/syncplay/messages_pt_BR.py +++ b/syncplay/messages_pt_BR.py @@ -83,7 +83,7 @@ pt_BR = { "commandlist-notification/undo": "\tu - desfaz último salto", "commandlist-notification/pause": "\tp - alterna pausa", "commandlist-notification/seek": "\t[s][+-]time - salta para o valor de tempo dado, se + ou - não forem especificados, será o tempo absoluto em segundos ou minutos:segundos", - "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position - this is a deprecated feature", # TODO: Translate "commandlist-notification/help": "\th - esta mensagem de ajuda", "commandlist-notification/toggle": "\tt - alterna o seu status de prontidão para assistir", "commandlist-notification/create": "\tc [nome] - cria sala gerenciado usando o nome da sala atual", diff --git a/syncplay/messages_pt_PT.py b/syncplay/messages_pt_PT.py index 9e169fb..2ab1876 100644 --- a/syncplay/messages_pt_PT.py +++ b/syncplay/messages_pt_PT.py @@ -82,7 +82,7 @@ pt_PT = { "commandlist-notification/undo": "\tu - desfaz último salto", "commandlist-notification/pause": "\tp - alterna pausa", "commandlist-notification/seek": "\t[s][+-]time - salta para o valor de tempo dado, se + ou - não forem especificados, será o tempo absoluto em segundos ou minutos:segundos", - "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position - this is a deprecated feature", # TODO: Translate "commandlist-notification/help": "\th - esta mensagem de ajuda", "commandlist-notification/toggle": "\tt - alterna o seu status de prontidão para assistir", "commandlist-notification/create": "\tc [nome] - cria sala gerenciada usando o nome da sala atual", diff --git a/syncplay/messages_ru.py b/syncplay/messages_ru.py index 0b4fa4e..a0e6c0d 100755 --- a/syncplay/messages_ru.py +++ b/syncplay/messages_ru.py @@ -81,7 +81,7 @@ ru = { "commandlist-notification/undo": "\tu - отменить последнюю перемотку", "commandlist-notification/pause": "\tp - вкл./выкл. паузу", "commandlist-notification/seek": "\t[s][+-]time - перемотать к заданному моменту времени, если не указан + или -, то время считается абсолютным (от начала файла) в секундах или мин:сек", - "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position - this is a deprecated feature", # TODO: Translate "commandlist-notification/help": "\th - помощь", "commandlist-notification/toggle": "\tt - переключить статус готов/не готов к просмотру", "commandlist-notification/create": "\tc [имя комнаты] - создать управляемую комнату с таким же именем, как у текущей", diff --git a/syncplay/messages_tr.py b/syncplay/messages_tr.py index cdc43ad..115f432 100644 --- a/syncplay/messages_tr.py +++ b/syncplay/messages_tr.py @@ -82,7 +82,7 @@ tr = { "commandlist-notification/undo": "\tu - son isteği geri alır", "commandlist-notification/pause": "\tp - duraklatmayı değiştirir", "commandlist-notification/seek": "\t[s][+-]time - verilen zaman değerine atlar, eğer + veya - belirtilmezse, saniye:dakika cinsinden mutlak zamandır.", - "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position - this is a deprecated feature", # TODO: Translate "commandlist-notification/help": "\th - yardım", "commandlist-notification/toggle": "\tt - izlemeye hazır olup olmadığınızı değiştirir", "commandlist-notification/create": "\tc [name] - mevcut odanın adını kullanarak yönetilen oda oluştur", From 16b6623f36325ccd3cdeaeb6bb9d4e63d8698674 Mon Sep 17 00:00:00 2001 From: et0h Date: Wed, 27 Oct 2021 19:18:51 +0100 Subject: [PATCH 12/30] Begin move from m3u/m3u8 to txt for playlist (#408) --- syncplay/ui/gui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 71a8011..2669885 100755 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -1099,7 +1099,7 @@ class MainWindow(QtWidgets.QMainWindow): defaultdirectory = currentdirectory else: defaultdirectory = self.getInitialMediaDirectory() - browserfilter = "Playlists (*.m3u *.m3u8 *.txt)" + browserfilter = "Playlists (*.txt *.m3u8)" filepath, filtr = QtWidgets.QFileDialog.getOpenFileName( self, "Load playlist from file", defaultdirectory, browserfilter, "", options) # TODO: Note Shuffle and move to messages_en @@ -1120,7 +1120,7 @@ class MainWindow(QtWidgets.QMainWindow): defaultdirectory = currentdirectory else: defaultdirectory = self.getInitialMediaDirectory() - browserfilter = "Playlist (*.m3u8 *.m3u *.txt)" + browserfilter = "Playlist (*.txt)" filepath, filtr = QtWidgets.QFileDialog.getSaveFileName( self, "Save playlist to file", defaultdirectory, browserfilter, "", options) # TODO: Move to messages_en From e2605577f5169dad14a238e6ab53b409af32f4d3 Mon Sep 17 00:00:00 2001 From: et0h Date: Thu, 28 Oct 2021 01:25:25 +0100 Subject: [PATCH 13/30] Add queue and select command (#472) --- syncplay/__init__.py | 2 +- syncplay/client.py | 5 +++++ syncplay/constants.py | 1 + syncplay/messages_de.py | 1 + syncplay/messages_en.py | 1 + syncplay/messages_es.py | 1 + syncplay/messages_it.py | 1 + syncplay/messages_pt_BR.py | 1 + syncplay/messages_pt_PT.py | 1 + syncplay/messages_ru.py | 1 + syncplay/messages_tr.py | 1 + syncplay/ui/consoleUI.py | 5 ++++- 12 files changed, 19 insertions(+), 2 deletions(-) diff --git a/syncplay/__init__.py b/syncplay/__init__.py index 88dea82..f3c37c4 100755 --- a/syncplay/__init__.py +++ b/syncplay/__init__.py @@ -1,5 +1,5 @@ version = '1.7.0' revision = ' development' milestone = 'Yoitsu' -release_number = '100' +release_number = '101' projectURL = 'https://syncplay.pl/' diff --git a/syncplay/client.py b/syncplay/client.py index acc4027..d3c4c3f 100755 --- a/syncplay/client.py +++ b/syncplay/client.py @@ -1700,6 +1700,7 @@ class SyncplayPlaylist(): self._playlist = [] self._playlistIndex = None self.addedChangeListCallback = False + self.switchToNewPlaylistItem = False self._lastPlaylistIndexChange = time.time() def needsSharedPlaylistsEnabled(f): # @NoSelf @@ -1864,6 +1865,10 @@ class SyncplayPlaylist(): self._ui.showDebugMessage("Could not change playlist index due to IndexError") def _getValidIndexFromNewPlaylist(self, newPlaylist=None): + if self.switchToNewPlaylistItem: + self.switchToNewPlaylistItem = False + return len(self._playlist) + if self._playlistIndex is None or not newPlaylist or len(newPlaylist) <= 1: return 0 diff --git a/syncplay/constants.py b/syncplay/constants.py index b9c0412..ddb2f73 100755 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -125,6 +125,7 @@ COMMANDS_CREATE = ['c', 'create'] COMMANDS_AUTH = ['a', 'auth'] COMMANDS_TOGGLE = ['t', 'toggle'] COMMANDS_QUEUE = ['queue', 'qa', 'add'] +COMMANDS_QUEUEANDSELECT = ['queueandselect','qas'] COMMANDS_PLAYLIST = ['playlist', 'ql', 'pl'] COMMANDS_SELECT = ['select', 'qs'] COMMANDS_DELETE = ['delete', 'd', 'qd'] diff --git a/syncplay/messages_de.py b/syncplay/messages_de.py index b9060bb..632293e 100755 --- a/syncplay/messages_de.py +++ b/syncplay/messages_de.py @@ -89,6 +89,7 @@ de = { "commandlist-notification/auth": "\ta [password] - authentifiziere als Raumleiter mit Passwort", "commandlist-notification/chat": "\tch [message] - Chatnachricht an einem Raum senden", "commandList-notification/queue": "\tqa [file/url] - add file or url to bottom of playlist", # TO DO: Translate + "commandList-notification/queueandselect": "\tqas [file/url] - add file or url to bottom of playlist and select it", # TO DO: Translate "commandList-notification/playlist": "\tql - show the current playlist", # TO DO: Translate "commandList-notification/select": "\tqs [index] - select given entry in the playlist", # TO DO: Translate "commandList-notification/delete": "\tqd [index] - delete the given entry from the playlist", # TO DO: Translate diff --git a/syncplay/messages_en.py b/syncplay/messages_en.py index 71302d4..8ad9d05 100755 --- a/syncplay/messages_en.py +++ b/syncplay/messages_en.py @@ -89,6 +89,7 @@ en = { "commandlist-notification/auth": "\ta [password] - authenticate as room operator with operator password", "commandlist-notification/chat": "\tch [message] - send a chat message in a room", "commandList-notification/queue": "\tqa [file/url] - add file or url to bottom of playlist", + "commandList-notification/queueandselect": "\tqas [file/url] - add file or url to bottom of playlist and select it", "commandList-notification/playlist": "\tql - show the current playlist", "commandList-notification/select": "\tqs [index] - select given entry in the playlist", "commandList-notification/delete": "\tqd [index] - delete the given entry from the playlist", diff --git a/syncplay/messages_es.py b/syncplay/messages_es.py index 7283b28..b45a7d1 100644 --- a/syncplay/messages_es.py +++ b/syncplay/messages_es.py @@ -89,6 +89,7 @@ es = { "commandlist-notification/auth": "\ta [contraseña] - autentificar como operador de la sala con la contraseña de operador", "commandlist-notification/chat": "\tch [mensaje] - enviar un mensaje en la sala", "commandList-notification/queue": "\tqa [file/url] - add file or url to bottom of playlist", # TO DO: Translate + "commandList-notification/queueandselect": "\tqas [file/url] - add file or url to bottom of playlist and select it", # TO DO: Translate "commandList-notification/playlist": "\tql - show the current playlist", # TO DO: Translate "commandList-notification/select": "\tqs [index] - select given entry in the playlist", # TO DO: Translate "commandList-notification/delete": "\tqd [index] - delete the given entry from the playlist", # TO DO: Translate diff --git a/syncplay/messages_it.py b/syncplay/messages_it.py index 42c62ea..72429a2 100755 --- a/syncplay/messages_it.py +++ b/syncplay/messages_it.py @@ -89,6 +89,7 @@ it = { "commandlist-notification/auth": "\ta [password] - autentica come gestore della stanza, utilizzando la password del gestore", "commandlist-notification/chat": "\tch [message] - invia un messaggio nella chat della stanza", "commandList-notification/queue": "\tqa [file/url] - add file or url to bottom of playlist", # TO DO: Translate + "commandList-notification/queueandselect": "\tqas [file/url] - add file or url to bottom of playlist and select it", # TO DO: Translate "commandList-notification/playlist": "\tql - show the current playlist", # TO DO: Translate "commandList-notification/select": "\tqs [index] - select given entry in the playlist", # TO DO: Translate "commandList-notification/delete": "\tqd [index] - delete the given entry from the playlist", # TO DO: Translate diff --git a/syncplay/messages_pt_BR.py b/syncplay/messages_pt_BR.py index c75fb7d..05c4060 100644 --- a/syncplay/messages_pt_BR.py +++ b/syncplay/messages_pt_BR.py @@ -90,6 +90,7 @@ pt_BR = { "commandlist-notification/auth": "\ta [senha] - autentica-se como operador da sala com a senha", "commandlist-notification/chat": "\tch [mensagem] - envia uma mensagem no chat da sala", "commandList-notification/queue": "\tqa [file/url] - adiciona arquivo ou URL para o final da playlist", + "commandList-notification/queueandselect": "\tqas [file/url] - add file or url to bottom of playlist and select it", # TO DO: Translate "commandList-notification/playlist": "\tql - mostra a playlist atual", "commandList-notification/select": "\tqs [index] - seleciona uma dada entrada na playlist", "commandList-notification/delete": "\tqd [index] - deleta uma dada entrada na playlist", diff --git a/syncplay/messages_pt_PT.py b/syncplay/messages_pt_PT.py index 2ab1876..a451390 100644 --- a/syncplay/messages_pt_PT.py +++ b/syncplay/messages_pt_PT.py @@ -89,6 +89,7 @@ pt_PT = { "commandlist-notification/auth": "\ta [senha] - autentica-se como administrador da sala com a senha", "commandlist-notification/chat": "\tch [mensagem] - envia uma mensagem no chat da sala", "commandList-notification/queue": "\tqa [file/url] - add file or url to bottom of playlist", # TO DO: Translate + "commandList-notification/queueandselect": "\tqas [file/url] - add file or url to bottom of playlist and select it", # TO DO: Translate "commandList-notification/playlist": "\tql - show the current playlist", # TO DO: Translate "commandList-notification/select": "\tqs [index] - select given entry in the playlist", # TO DO: Translate "commandList-notification/delete": "\tqd [index] - delete the given entry from the playlist", # TO DO: Translate diff --git a/syncplay/messages_ru.py b/syncplay/messages_ru.py index a0e6c0d..c9cda46 100755 --- a/syncplay/messages_ru.py +++ b/syncplay/messages_ru.py @@ -88,6 +88,7 @@ ru = { "commandlist-notification/auth": "\ta [пароль] - авторизоваться как оператор комнаты с помощью пароля", "commandlist-notification/chat": "\tch [сообщение] - выслать сообщение в комнату", "commandList-notification/queue": "\tqa [файл/URL] - добавить файл или URL в конец списка воспроизведения", + "commandList-notification/queueandselect": "\tqas [file/url] - add file or url to bottom of playlist and select it", # TO DO: Translate "commandList-notification/playlist": "\tql - показать текущий список воспроизведения", "commandList-notification/select": "\tqs [индекс] - выделить указанный пункт в списке воспроизведения", "commandList-notification/delete": "\tqd [индекс] - удалить указанный пункт из списка воспроизведения", diff --git a/syncplay/messages_tr.py b/syncplay/messages_tr.py index 115f432..efb045a 100644 --- a/syncplay/messages_tr.py +++ b/syncplay/messages_tr.py @@ -89,6 +89,7 @@ tr = { "commandlist-notification/auth": "\ta [password] - operatör şifresi ile oda operatörü olarak kimlik doğrular", "commandlist-notification/chat": "\tch [message] - bir odaya sohbet mesajı gönderir", "commandList-notification/queue": "\tqa [file/url] - oynatma listesinin altına dosya veya bağlantı ekler", + "commandList-notification/queueandselect": "\tqas [file/url] - add file or url to bottom of playlist and select it", # TO DO: Translate "commandList-notification/playlist": "\tql - mevcut oynatma listesini gösterir", "commandList-notification/select": "\tqs [index] - oynatma listesinde verilen girişi seçer", "commandList-notification/delete": "\tqd [index] - verilen girişi oynatma listesinden siler", diff --git a/syncplay/ui/consoleUI.py b/syncplay/ui/consoleUI.py index 7363ae6..00a0003 100755 --- a/syncplay/ui/consoleUI.py +++ b/syncplay/ui/consoleUI.py @@ -193,8 +193,10 @@ class ConsoleUI(threading.Thread): if filename is None: self.showErrorMessage("No file/url given") return - self._syncplayClient.ui.addFileToPlaylist(filename) + elif command.group('command') in constants.COMMANDS_QUEUEANDSELECT: + self._syncplayClient.playlist.switchToNewPlaylistItem = True + self.executeCommand("{} {}".format(constants.COMMANDS_QUEUE[0], command.group('parameter'))) elif command.group('command') in constants.COMMANDS_PLAYLIST: playlist = self._syncplayClient.playlist playlist_elements = ["\t{}: {}".format(i+1, el) for i, el in enumerate(playlist._playlist)] @@ -244,6 +246,7 @@ class ConsoleUI(threading.Thread): self.showMessage(getMessage("commandlist-notification/auth"), True) self.showMessage(getMessage("commandlist-notification/chat"), True) self.showMessage(getMessage("commandList-notification/queue"), True) + self.showMessage(getMessage("commandList-notification/queueandselect"), True) self.showMessage(getMessage("commandList-notification/playlist"), True) self.showMessage(getMessage("commandList-notification/select"), True) self.showMessage(getMessage("commandList-notification/delete"), True) From 148198b21284b137df369d8829deb5d8a499d873 Mon Sep 17 00:00:00 2001 From: Assistant Date: Mon, 29 Nov 2021 17:53:09 -0400 Subject: [PATCH 14/30] Added room permanence option to server (#434) --- docs/syncplay-server.1 | 8 ++++ syncplay/ep_server.py | 2 + syncplay/messages_de.py | 2 + syncplay/messages_en.py | 2 + syncplay/messages_es.py | 2 + syncplay/messages_it.py | 2 + syncplay/messages_pt_BR.py | 2 + syncplay/messages_pt_PT.py | 2 + syncplay/messages_ru.py | 2 + syncplay/messages_tr.py | 2 + syncplay/server.py | 91 +++++++++++++++++++++++++++++++++----- 11 files changed, 107 insertions(+), 10 deletions(-) diff --git a/docs/syncplay-server.1 b/docs/syncplay-server.1 index 32b779e..e9b91e8 100644 --- a/docs/syncplay-server.1 +++ b/docs/syncplay-server.1 @@ -77,6 +77,14 @@ Random string used to generate managed room passwords. .B \-\-motd\-file [file] 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. + +.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. + .TP .B \-\-max\-chat\-message\-length [maxChatMessageLength] Maximum number of characters in one chat message (default is 150). diff --git a/syncplay/ep_server.py b/syncplay/ep_server.py index 22aec44..d2e4aef 100644 --- a/syncplay/ep_server.py +++ b/syncplay/ep_server.py @@ -34,6 +34,8 @@ def main(): args.port, args.password, args.motd_file, + args.rooms_dir, + args.rooms_timer, args.isolate_rooms, args.salt, args.disable_ready, diff --git a/syncplay/messages_de.py b/syncplay/messages_de.py index 632293e..595ea50 100755 --- a/syncplay/messages_de.py +++ b/syncplay/messages_de.py @@ -474,6 +474,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-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 8ad9d05..ac7b25b 100755 --- a/syncplay/messages_en.py +++ b/syncplay/messages_en.py @@ -475,6 +475,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-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 b45a7d1..cf4cd57 100644 --- a/syncplay/messages_es.py +++ b/syncplay/messages_es.py @@ -474,6 +474,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-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 72429a2..44dd2a4 100755 --- a/syncplay/messages_it.py +++ b/syncplay/messages_it.py @@ -474,6 +474,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-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 05c4060..49407d5 100644 --- a/syncplay/messages_pt_BR.py +++ b/syncplay/messages_pt_BR.py @@ -475,6 +475,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-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 a451390..4cdbedb 100644 --- a/syncplay/messages_pt_PT.py +++ b/syncplay/messages_pt_PT.py @@ -474,6 +474,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-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 c9cda46..b9a3e4a 100755 --- a/syncplay/messages_ru.py +++ b/syncplay/messages_ru.py @@ -471,6 +471,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-chat-argument": "Должен ли чат быть отключён?", "server-chat-maxchars-argument": "Максимальное число символов в сообщениях в чате (по умолчанию {})", "server-maxusernamelength-argument": "Максимальное число символов в именах пользователей (по умолчанию {})", diff --git a/syncplay/messages_tr.py b/syncplay/messages_tr.py index efb045a..ad602b4 100644 --- a/syncplay/messages_tr.py +++ b/syncplay/messages_tr.py @@ -475,6 +475,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-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/server.py b/syncplay/server.py index 21de91b..58b7c8d 100755 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -4,6 +4,7 @@ import hashlib import os import random import time +import json from string import Template from twisted.enterprise import adbapi @@ -25,7 +26,7 @@ from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomString class SyncFactory(Factory): - def __init__(self, port='', password='', motdFilePath=None, isolateRooms=False, salt=None, + def __init__(self, port='', password='', motdFilePath=None, roomsDirPath=None, roomsTimer=31558149, 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 @@ -40,12 +41,14 @@ 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.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 if not isolateRooms: - self._roomManager = RoomManager() + self._roomManager = RoomManager(self._roomsDirPath, self._roomsTimer) else: self._roomManager = PublicRoomManager() if statsDbFile is not None: @@ -311,8 +314,22 @@ class DBManager(object): class RoomManager(object): - def __init__(self): + def __init__(self, roomsDir=None, timer=0): + self._roomsDir = roomsDir + self._timer = timer 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 def broadcastRoom(self, sender, whatLambda): room = sender.getRoom() @@ -342,16 +359,19 @@ class RoomManager(object): oldRoom = watcher.getRoom() if oldRoom: oldRoom.removeWatcher(watcher) - self._deleteRoomIfEmpty(oldRoom) + if self._roomsDir is None or oldRoom.isStale(self._timer): + self._deleteRoomIfEmpty(oldRoom) def _getRoom(self, roomName): - if roomName in self._rooms: + if roomName in self._rooms and not self._rooms[roomName].isStale(self._timer): return self._rooms[roomName] else: if RoomPasswordProvider.isControlledRoom(roomName): room = ControlledRoom(roomName) else: - room = Room(roomName) + if roomName in self._rooms: + self._deleteRoomIfEmpty(self._rooms[roomName]) + room = Room(roomName, self._roomsDir) self._rooms[roomName] = room return room @@ -392,19 +412,63 @@ class Room(object): STATE_PAUSED = 0 STATE_PLAYING = 1 - def __init__(self, name): + def __init__(self, name, _roomsDir=None): self._name = name + self._roomsDir = _roomsDir self._watchers = {} self._playState = self.STATE_PAUSED self._setBy = None self._playlist = [] self._playlistIndex = None self._lastUpdate = time.time() + self._lastSavedUpdate = 0 self._position = 0 def __str__(self, *args, **kwargs): return self.getName() + def roomsCanPersist(self): + return self._roomsDir is not None + + def isPermanent(self): + return self.roomsCanPersist() + + 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(): + 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) + + 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 getName(self): return self._name @@ -414,7 +478,7 @@ class Room(object): watcher = min(self._watchers.values()) self._setBy = watcher self._position = watcher.getPosition() - self._lastUpdate = time.time() + self._lastSavedUpdate = self._lastUpdate = time.time() return self._position elif self._position is not None: return self._position + (age if self._playState == self.STATE_PLAYING else 0) @@ -424,12 +488,14 @@ class Room(object): def setPaused(self, paused=STATE_PAUSED, setBy=None): self._playState = paused self._setBy = setBy + self.writeToFile() def setPosition(self, position, setBy=None): self._position = position for watcher in self._watchers.values(): watcher.setPosition(position) self._setBy = setBy + self.writeToFile() def isPlaying(self): return self._playState == self.STATE_PLAYING @@ -441,7 +507,7 @@ class Room(object): return list(self._watchers.values()) def addWatcher(self, watcher): - if self._watchers: + if self._watchers or self.isPermanent(): watcher.setPosition(self.getPosition()) self._watchers[watcher.getName()] = watcher watcher.setRoom(self) @@ -451,8 +517,9 @@ class Room(object): return del self._watchers[watcher.getName()] watcher.setRoom(None) - if not self._watchers: + if not self._watchers and not self.isPermanent(): self._position = 0 + self.writeToFile() def isEmpty(self): return not bool(self._watchers) @@ -465,9 +532,11 @@ class Room(object): def setPlaylist(self, files, setBy=None): self._playlist = files + self.writeToFile() def setPlaylistIndex(self, index, setBy=None): self._playlistIndex = index + self.writeToFile() def getPlaylist(self): return self._playlist @@ -687,6 +756,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('--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")) From c0e8652c8c2a94f3a75d67bd0798292c55527e32 Mon Sep 17 00:00:00 2001 From: Etoh Date: Thu, 9 Dec 2021 16:23:57 +0000 Subject: [PATCH 15/30] 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 From d60efabbadd3dd973c10f758532f2065042aac32 Mon Sep 17 00:00:00 2001 From: Etoh Date: Thu, 9 Dec 2021 16:24:40 +0000 Subject: [PATCH 16/30] Windows dark mode (#483) * Add Dark Mode support for Windows * Only use dark mode if enabled * Actions: debug Windows build * Don't error if dark mode is unavailable * Revert Actions: debug Windows build * darkdetect: update vendor copy to 0.5.1 * Revert "Don't error if dark mode is unavailable" This reverts commit 5a9e871c12312c55355a9eb75df15c66bb4238ac. * Set up tmate * Stronger patching of darkdetect is needed * Revert "Set up tmate" This reverts commit 4012051f9328c52ab6d1297ede111ab48fad1f2d. * Fix EXE by using Windows darkdetect directly * Add try wrap to Windows darkmode check * darkdetect: update vendor copy to 0.5.2 * Revert to public darkdetect API Co-authored-by: Alberto Sottile Co-authored-by: Alberto Sottile --- syncplay/ui/ConfigurationGetter.py | 36 +++++++++++++++++-- syncplay/ui/gui.py | 2 +- syncplay/vendor/darkdetect/__init__.py | 8 ++--- syncplay/vendor/darkdetect/_mac_detect.py | 14 ++++---- syncplay/vendor/darkdetect/_windows_detect.py | 18 ++++++---- 5 files changed, 57 insertions(+), 21 deletions(-) diff --git a/syncplay/ui/ConfigurationGetter.py b/syncplay/ui/ConfigurationGetter.py index 78b2ba1..8be4d7d 100755 --- a/syncplay/ui/ConfigurationGetter.py +++ b/syncplay/ui/ConfigurationGetter.py @@ -10,7 +10,7 @@ from configparser import SafeConfigParser, DEFAULTSECT from syncplay import constants, utils, version, milestone from syncplay.messages import getMessage, setLanguage, isValidLanguage from syncplay.players.playerFactory import PlayerFactory -from syncplay.utils import isMacOS +from syncplay.utils import isMacOS, isWindows class InvalidConfigValue(Exception): @@ -514,13 +514,22 @@ class ConfigurationGetter(object): self._overrideConfigWithArgs(args) if not self._config['noGui']: try: - from syncplay.vendor.Qt import QtWidgets, IsPySide, IsPySide2 + from syncplay.vendor.Qt import QtWidgets, IsPySide, IsPySide2, QtGui from syncplay.vendor.Qt.QtCore import QCoreApplication from syncplay.vendor import qt5reactor if not (IsPySide2 or IsPySide): raise ImportError if QCoreApplication.instance() is None: self.app = QtWidgets.QApplication(sys.argv) + if isWindows(): + try: + from syncplay.vendor import darkdetect + isDarkMode = darkdetect.isDark() + except: + isDarkMode = False + if isDarkMode: + self.app.setStyle(QtWidgets.QStyleFactory.create("fusion")) + self.app.setPalette(self.getDarkPalette(QtGui)) qt5reactor.install() if isMacOS(): import appnope @@ -550,6 +559,29 @@ class ConfigurationGetter(object): setLanguage(self._config['language']) return self._config + def getDarkPalette(self, QtGui): + # Based on https://gist.github.com/lschmierer/443b8e21ad93e2a2d7eb#gistcomment-3503395 + darkPalette = QtGui.QPalette() + darkPalette.setColor(QtGui.QPalette.Window, QtGui.QColor(53, 53, 53)) + darkPalette.setColor(QtGui.QPalette.WindowText, QtGui.QColor(255, 255, 255)) + darkPalette.setColor(QtGui.QPalette.Base, QtGui.QColor(35, 35, 35)) + darkPalette.setColor(QtGui.QPalette.AlternateBase, QtGui.QColor(53, 53, 53)) + darkPalette.setColor(QtGui.QPalette.ToolTipBase, QtGui.QColor(25, 25, 25)) + darkPalette.setColor(QtGui.QPalette.ToolTipText, QtGui.QColor(255, 255, 255)) + darkPalette.setColor(QtGui.QPalette.Text, QtGui.QColor(255, 255, 255)) + darkPalette.setColor(QtGui.QPalette.Button, QtGui.QColor(53, 53, 53)) + darkPalette.setColor(QtGui.QPalette.ButtonText, QtGui.QColor(255, 255, 255)) + darkPalette.setColor(QtGui.QPalette.BrightText, QtGui.QColor(255, 0, 0)) + darkPalette.setColor(QtGui.QPalette.Link, QtGui.QColor(42, 130, 218)) + darkPalette.setColor(QtGui.QPalette.Highlight, QtGui.QColor(42, 130, 218)) + darkPalette.setColor(QtGui.QPalette.HighlightedText, QtGui.QColor(35, 35, 35)) + darkPalette.setColor(QtGui.QPalette.Active, QtGui.QPalette.Button, QtGui.QColor(53, 53, 53)) + darkPalette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, QtGui.QColor(128, 128, 128)) + darkPalette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, QtGui.QColor(128, 128, 128)) + darkPalette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Text, QtGui.QColor(128, 128, 128)) + darkPalette.setColor(QtGui.QPalette.Disabled, QtGui.QPalette.Light, QtGui.QColor(53, 53, 53)) + return darkPalette + def setConfigOption(self, option, value): path = self._getConfigurationFilePath() backup = self._config.copy() diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index a6b4e12..7068db6 100755 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -40,7 +40,7 @@ if isMacOS() and IsPySide: from Cocoa import NSString, NSUTF8StringEncoding lastCheckedForUpdates = None from syncplay.vendor import darkdetect -if isMacOS(): +if isMacOS() or isWindows(): isDarkMode = darkdetect.isDark() else: isDarkMode = None diff --git a/syncplay/vendor/darkdetect/__init__.py b/syncplay/vendor/darkdetect/__init__.py index bb1df11..fc68343 100644 --- a/syncplay/vendor/darkdetect/__init__.py +++ b/syncplay/vendor/darkdetect/__init__.py @@ -4,7 +4,7 @@ # Distributed under the terms of the 3-clause BSD License. #----------------------------------------------------------------------------- -__version__ = '0.5.0' +__version__ = '0.5.2' import sys import platform @@ -16,10 +16,10 @@ if sys.platform == "darwin": else: from ._mac_detect import * del V -elif sys.platform == "win32" and platform.release() == "10": - # Checks if running Windows 10 version 10.0.14393 (Anniversary Update) or higher. The getwindowsversion method returns a tuple. +elif sys.platform == "win32" and int(platform.release()) >= 10: + # Checks if running Windows 10 version 10.0.14393 (Anniversary Update) OR HIGHER. The getwindowsversion method returns a tuple. # The third item is the build number that we can use to check if the user has a new enough version of Windows. - winver = int(sys.getwindowsversion()[2]) + winver = int(platform.version().split('.')[2]) if winver >= 14393: from ._windows_detect import * else: diff --git a/syncplay/vendor/darkdetect/_mac_detect.py b/syncplay/vendor/darkdetect/_mac_detect.py index 322582f..fbe6a35 100644 --- a/syncplay/vendor/darkdetect/_mac_detect.py +++ b/syncplay/vendor/darkdetect/_mac_detect.py @@ -6,17 +6,15 @@ import ctypes import ctypes.util -import platform -from distutils.version import LooseVersion as V - -if V(platform.mac_ver()[0]) < V("10.16"): - appkit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('AppKit')) - objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc')) -else: +try: + # macOS Big Sur+ use "a built-in dynamic linker cache of all system-provided libraries" appkit = ctypes.cdll.LoadLibrary('AppKit.framework/AppKit') objc = ctypes.cdll.LoadLibrary('libobjc.dylib') -del V +except OSError: + # revert to full path for older OS versions and hardened programs + appkit = ctypes.cdll.LoadLibrary(ctypes.util.find_library('AppKit')) + objc = ctypes.cdll.LoadLibrary(ctypes.util.find_library('objc')) void_p = ctypes.c_void_p ull = ctypes.c_uint64 diff --git a/syncplay/vendor/darkdetect/_windows_detect.py b/syncplay/vendor/darkdetect/_windows_detect.py index 61409a7..a976885 100644 --- a/syncplay/vendor/darkdetect/_windows_detect.py +++ b/syncplay/vendor/darkdetect/_windows_detect.py @@ -5,14 +5,20 @@ def theme(): # Registry will return 0 if Windows is in Dark Mode and 1 if Windows is in Light Mode. This dictionary converts that output into the text that the program is expecting. valueMeaning = {0: "Dark", 1: "Light"} # In HKEY_CURRENT_USER, get the Personalisation Key. - key = getKey(hkey, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize") - # In the Personalisation Key, get the AppsUseLightTheme subkey. This returns a tuple. - # The first item in the tuple is the result we want (0 or 1 indicating Dark Mode or Light Mode); the other value is the type of subkey e.g. DWORD, QWORD, String, etc. - subkey = getSubkeyValue(key, "AppsUseLightTheme")[0] + try: + key = getKey(hkey, "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize") + # In the Personalisation Key, get the AppsUseLightTheme subkey. This returns a tuple. + # The first item in the tuple is the result we want (0 or 1 indicating Dark Mode or Light Mode); the other value is the type of subkey e.g. DWORD, QWORD, String, etc. + subkey = getSubkeyValue(key, "AppsUseLightTheme")[0] + except FileNotFoundError: + # some headless Windows instances (e.g. GitHub Actions or Docker images) do not have this key + return None return valueMeaning[subkey] def isDark(): - return theme() == 'Dark' + if theme() is not None: + return theme() == 'Dark' def isLight(): - return theme() == 'Light' \ No newline at end of file + if theme() is not None: + return theme() == 'Light' \ No newline at end of file From 566f90b47f5dc774439749a3029eb6c5750e170e Mon Sep 17 00:00:00 2001 From: Florian Badie Date: Sat, 11 Dec 2021 09:03:27 +0100 Subject: [PATCH 17/30] fix persistent rooms condition (#492) --- syncplay/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncplay/server.py b/syncplay/server.py index ca5a8ee..3cfc9af 100755 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -465,11 +465,11 @@ class RoomManager(object): oldRoom = watcher.getRoom() if oldRoom: oldRoom.removeWatcher(watcher) - if self._roomsDir is None or oldRoom.isStale(self._timer): + if self._roomsDbFile is None: self._deleteRoomIfEmpty(oldRoom) def _getRoom(self, roomName): - if roomName in self._rooms and not self._rooms[roomName].isStale(self._timer): + if roomName in self._rooms: return self._rooms[roomName] else: if RoomPasswordProvider.isControlledRoom(roomName): From 1f4f5ca7ce474f923ba0b8001a86046297b74fab Mon Sep 17 00:00:00 2001 From: Etoh Date: Sun, 12 Dec 2021 12:24:57 +0000 Subject: [PATCH 18/30] Fix Windows build fail --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac4302c..985edb9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,7 +42,7 @@ jobs: echo $ver echo "VER=$ver" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append python buildPy2exe.py - New-Item -Path syncplay_v$ver -Name "syncplay.ini" -Value $null + New-Item -Path syncplay_v$ver -Name "syncplay.ini" -Value "\n" - name: Prepare for deployment run: dir From a57ea6dec5694a00918a139f2b6f601ecd25a569 Mon Sep 17 00:00:00 2001 From: Etoh Date: Sun, 12 Dec 2021 12:34:00 +0000 Subject: [PATCH 19/30] Use space for blank INI --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 985edb9..3d4c638 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,7 +42,7 @@ jobs: echo $ver echo "VER=$ver" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append python buildPy2exe.py - New-Item -Path syncplay_v$ver -Name "syncplay.ini" -Value "\n" + New-Item -Path syncplay_v$ver -Name "syncplay.ini" -Value " " - name: Prepare for deployment run: dir From 7083d8cbbcbbe15e86462a8d9687daef655b5003 Mon Sep 17 00:00:00 2001 From: et0h Date: Sun, 12 Dec 2021 12:58:34 +0000 Subject: [PATCH 20/30] Fix room not deleting bug (#434) --- syncplay/server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/syncplay/server.py b/syncplay/server.py index 3cfc9af..149be12 100755 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -465,8 +465,7 @@ class RoomManager(object): oldRoom = watcher.getRoom() if oldRoom: oldRoom.removeWatcher(watcher) - if self._roomsDbFile is None: - self._deleteRoomIfEmpty(oldRoom) + self._deleteRoomIfEmpty(oldRoom) def _getRoom(self, roomName): if roomName in self._rooms: From 842ac9ea6551b6b959def3160536b203dfd7aedd Mon Sep 17 00:00:00 2001 From: SoSie <70700670+sosie-js@users.noreply.github.com> Date: Tue, 21 Dec 2021 16:39:57 +0100 Subject: [PATCH 21/30] Added French Translation (#455) --- buildPy2exe.py | 17 +- syncplay/messages.py | 2 + syncplay/messages_de.py | 2 +- syncplay/messages_en.py | 8 +- syncplay/messages_es.py | 2 +- syncplay/messages_fr.py | 522 +++++++++++++++++++++++++++++++++++++ syncplay/messages_it.py | 2 +- syncplay/messages_pt_BR.py | 2 +- syncplay/messages_pt_PT.py | 2 +- syncplay/messages_ru.py | 2 +- syncplay/messages_tr.py | 2 +- 11 files changed, 551 insertions(+), 12 deletions(-) mode change 100755 => 100644 syncplay/messages_en.py create mode 100644 syncplay/messages_fr.py diff --git a/buildPy2exe.py b/buildPy2exe.py index 0747702..1ac3880 100755 --- a/buildPy2exe.py +++ b/buildPy2exe.py @@ -66,7 +66,8 @@ NSIS_SCRIPT_TEMPLATE = r""" LoadLanguageFile "$${NSISDIR}\Contrib\Language files\PortugueseBR.nlf" LoadLanguageFile "$${NSISDIR}\Contrib\Language files\Portuguese.nlf" LoadLanguageFile "$${NSISDIR}\Contrib\Language files\Turkish.nlf" - + LoadLanguageFile "$${NSISDIR}\Contrib\Language files\French.nlf" + Unicode true Name "Syncplay $version" @@ -118,6 +119,11 @@ NSIS_SCRIPT_TEMPLATE = r""" VIAddVersionKey /LANG=$${LANG_TURKISH} "FileVersion" "$version.0" VIAddVersionKey /LANG=$${LANG_TURKISH} "LegalCopyright" "Syncplay" VIAddVersionKey /LANG=$${LANG_TURKISH} "FileDescription" "Syncplay" + + VIAddVersionKey /LANG=$${LANG_FRENCH} "ProductName" "Syncplay" + VIAddVersionKey /LANG=$${LANG_FRENCH} "FileVersion" "$version.0" + VIAddVersionKey /LANG=$${LANG_FRENCH} "LegalCopyright" "Syncplay" + VIAddVersionKey /LANG=$${LANG_FRENCH} "FileDescription" "Syncplay" LangString ^SyncplayLanguage $${LANG_ENGLISH} "en" LangString ^Associate $${LANG_ENGLISH} "Associate Syncplay with multimedia files." @@ -198,6 +204,15 @@ NSIS_SCRIPT_TEMPLATE = r""" LangString ^QuickLaunchBar $${LANG_TURKISH} "Hızlı Başlatma Çubuğu" LangString ^AutomaticUpdates $${LANG_TURKISH} "Güncellemeleri otomatik denetle" LangString ^UninstConfig $${LANG_TURKISH} "Yapılandırma dosyasını silin." + + LangString ^SyncplayLanguage $${LANG_FRENCH} "fr" + LangString ^Associate $${LANG_FRENCH} "Associer Syncplay avec les fichiers multimedias." + LangString ^Shortcut $${LANG_FRENCH} "Créer Racourcis pour les chemins suivants:" + LangString ^StartMenu $${LANG_FRENCH} "Menu Démarrer" + LangString ^Desktop $${LANG_FRENCH} "Bureau" + LangString ^QuickLaunchBar $${LANG_FRENCH} "Barre de Lancement Rapide" + LangString ^AutomaticUpdates $${LANG_FRENCH} "Vérifier automatiquement les mises à jour" + LangString ^UninstConfig $${LANG_FRENCH} "Supprimer le fichier de configuration." ; Remove text to save space LangString ^ClickInstall $${LANG_GERMAN} " " diff --git a/syncplay/messages.py b/syncplay/messages.py index 0e3d199..0b41ffc 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -9,12 +9,14 @@ from . import messages_es from . import messages_pt_BR from . import messages_pt_PT from . import messages_tr +from . import messages_fr import re messages = { "de": messages_de.de, "en": messages_en.en, "es": messages_es.es, + "fr": messages_fr.fr, "it": messages_it.it, "pt_PT": messages_pt_PT.pt_PT, "pt_BR": messages_pt_BR.pt_BR, diff --git a/syncplay/messages_de.py b/syncplay/messages_de.py index d3efe86..0f7943e 100755 --- a/syncplay/messages_de.py +++ b/syncplay/messages_de.py @@ -176,7 +176,7 @@ de = { "file-argument": 'Abzuspielende Datei', "args-argument": 'Player-Einstellungen; Wenn du Einstellungen, die mit - beginnen, nutzen willst, stelle ein einzelnes \'--\'-Argument davor', "clear-gui-data-argument": 'Setzt die Pfad- und GUI-Fenster-Daten die in den QSettings gespeichert sind zurück', - "language-argument": 'Sprache für Syncplay-Nachrichten (de/en/ru/it/es/pt_BR/pt_PT/tr)', + "language-argument": 'Sprache für Syncplay-Nachrichten (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', "version-argument": 'gibt die aktuelle Version aus', "version-message": "Du verwendest Syncplay v. {} ({})", diff --git a/syncplay/messages_en.py b/syncplay/messages_en.py old mode 100755 new mode 100644 index 8daae25..0de7348 --- a/syncplay/messages_en.py +++ b/syncplay/messages_en.py @@ -35,7 +35,7 @@ en = { "left-notification": "{} has left", # User "left-paused-notification": "{} left, {} paused", # User who left, User who paused "playing-notification": "{} is playing '{}' ({})", # User, file, duration - "playing-notification/room-addendum": " in room: '{}'", # Room + "playing-notification/room-addendum": " in room: '{}'", # Room "not-all-ready": "Not ready: {}", # Usernames "all-users-ready": "Everyone is ready ({} users)", # Number of ready users @@ -165,18 +165,18 @@ en = { "argument-description": 'Solution to synchronize playback of multiple media player instances over the network.', "argument-epilog": 'If no options supplied _config values will be used', "nogui-argument": 'show no GUI', - "host-argument": 'server\'s address', + "host-argument": "server's address", "name-argument": 'desired username', "debug-argument": 'debug mode', "force-gui-prompt-argument": 'make configuration prompt appear', - "no-store-argument": 'don\'t store values in .syncplay', + "no-store-argument": "don't store values in .syncplay", "room-argument": 'default room', "password-argument": 'server password', "player-path-argument": 'path to your player executable', "file-argument": 'file to play', "args-argument": 'player options, if you need to pass options starting with - prepend them with single \'--\' argument', "clear-gui-data-argument": 'resets path and window state GUI data stored as QSettings', - "language-argument": 'language for Syncplay messages (de/en/ru/it/es/pt_BR/pt_PT/tr)', + "language-argument": 'language for Syncplay messages (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', "version-argument": 'prints your version', "version-message": "You're using Syncplay version {} ({})", diff --git a/syncplay/messages_es.py b/syncplay/messages_es.py index efe8f8c..3259b1d 100644 --- a/syncplay/messages_es.py +++ b/syncplay/messages_es.py @@ -176,7 +176,7 @@ es = { "file-argument": 'archivo a reproducir', "args-argument": 'opciones del reproductor, si necesitas pasar opciones que empiezan con -, pásalas utilizando \'--\'', "clear-gui-data-argument": 'restablece ruta y los datos del estado de la ventana GUI almacenados como QSettings', - "language-argument": 'lenguaje para los mensajes de Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr)', + "language-argument": 'lenguaje para los mensajes de Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', "version-argument": 'imprime tu versión', "version-message": "Estás usando la versión de Syncplay {} ({})", diff --git a/syncplay/messages_fr.py b/syncplay/messages_fr.py new file mode 100644 index 0000000..6cb24e5 --- /dev/null +++ b/syncplay/messages_fr.py @@ -0,0 +1,522 @@ +# coding:utf8 + +"""French dictionary""" + +fr = { + "LANGUAGE": "Français", + + # Client notifications + "config-cleared-notification": "Paramètres effacés. Les modifications seront enregistrées lorsque vous enregistrez une configuration valide.", + + "relative-config-notification": "Fichiers de configuration relatifs chargés: {}", + + "connection-attempt-notification": "Tentative de connexion à {}:{}", # Port, IP + "reconnection-attempt-notification": "Connexion avec le serveur perdue, tentative de reconnexion", + "disconnection-notification": "Déconnecté du serveur", + "connection-failed-notification": "Échec de la connexion avec le serveur", + "connected-successful-notification": "Connexion réussie au serveur", + "retrying-notification": "%s, nouvelle tentative dans %d secondes...", # Seconds + "reachout-successful-notification": "Vous avez atteint {} ({})", + + "rewind-notification": "Retour en arrière en raison du décalage de temps avec {}", # User + "fastforward-notification": "Avance rapide en raison du décalage de temps avec {}", # User + "slowdown-notification": "Ralentissement dû au décalage de temps avec {}", # User + "revert-notification": "Retour à la vitesse normale", + + "pause-notification": "{} en pause", # User + "unpause-notification": "{} non suspendu", # User + "seek-notification": "{} est passé de {} à {}", # User, from time, to time + + "current-offset-notification": "Décalage actuel: {}secondes", # Offset + + "media-directory-list-updated-notification": "Les répertoires multimédias Syncplay ont été mis à jour.", + + "room-join-notification": "{} a rejoint la salle: '{}'", # User + "left-notification": "{} est parti", # User + "left-paused-notification": "{} restants, {} en pause", # User who left, User who paused + "playing-notification": "{} est en train de jouer '{}' ({})", # User, file, duration + "playing-notification/room-addendum": "dans le salon: '{}'", # Room + + "not-all-ready": "Pas prêt: {}", # Usernames + "all-users-ready": "Tout le monde est prêt ({} utilisateurs)", # Number of ready users + "ready-to-unpause-notification": "Vous êtes maintenant défini comme prêt - réactivez la pause pour réactiver", + "set-as-ready-notification": "Vous êtes maintenant défini comme prêt", + "set-as-not-ready-notification": "Vous êtes maintenant défini comme non prêt", + "autoplaying-notification": "Lecture automatique dans {}...", # Number of seconds until playback will start + + "identifying-as-controller-notification": "Identification en tant qu'opérateur de salle avec le mot de passe '{}'...", + "failed-to-identify-as-controller-notification": "{} n'a pas réussi à s'identifier en tant qu'opérateur de salle.", + "authenticated-as-controller-notification": "{} authentifié en tant qu'opérateur de salle", + "created-controlled-room-notification": "Salle gérée créée «{}» avec le mot de passe «{}». Veuillez conserver ces informations pour référence future !\n\nDans les salons gérés, tout le monde est synchronisé avec le ou les opérateurs de salon qui sont les seuls à pouvoir mettre en pause, reprendre, se déplacer dans la lecture et modifier la liste de lecture.\n\nVous devez demander aux spectateurs réguliers de rejoindre le salon '{}' mais les opérateurs de salon peuvent rejoindre le salon '{}' pour s'authentifier automatiquement.", # RoomName, operatorPassword, roomName, roomName:operatorPassword + + "file-different-notification": "Le fichier que vous lisez semble être différent de celui de {}", # User + "file-differences-notification": "Votre fichier diffère de la (des) manière(s) suivante(s): {}", # Differences + "room-file-differences": "Différences de fichiers: {}", # File differences (filename, size, and/or duration) + "file-difference-filename": "Nom", + "file-difference-filesize": "Taille", + "file-difference-duration": "durée", + "alone-in-the-room": "Vous êtes seul dans le salon", + + "different-filesize-notification": "(leur taille de fichier est différente de la vôtre!)", + "userlist-playing-notification": "{} est en train de jouer:", # Username + "file-played-by-notification": "Fichier: {} est lu par:", # File + "no-file-played-notification": "{} ne lit pas de fichier", # Username + "notplaying-notification": "Les personnes qui ne lisent aucun fichier:", + "userlist-room-notification": "Dans la chambre '{}':", # Room + "userlist-file-notification": "Fichier", + "controller-userlist-userflag": "Opérateur", + "ready-userlist-userflag": "Prêt", + + "update-check-failed-notification": "Impossible de vérifier automatiquement si Syncplay {} est à jour. Vous voulez visiter https://syncplay.pl/ pour vérifier manuellement les mises à jour?", # Syncplay version + "syncplay-uptodate-notification": "Syncplay est à jour", + "syncplay-updateavailable-notification": "Une nouvelle version de Syncplay est disponible. Voulez-vous visiter la page de publication?", + + "mplayer-file-required-notification": "Syncplay à l'aide de mplayer nécessite que vous fournissiez un fichier au démarrage", + "mplayer-file-required-notification/example": "Exemple d'utilisation: syncplay [options] [url|chemin/]nom de fichier", + "mplayer2-required": "Syncplay est incompatible avec MPlayer 1.x, veuillez utiliser mplayer2 ou mpv", + + "unrecognized-command-notification": "commande non reconnue", + "commandlist-notification": "Commandes disponibles:", + "commandlist-notification/room": "\tr [nom] - changer de chambre", + "commandlist-notification/list": "\tl - afficher la liste des utilisateurs", + "commandlist-notification/undo": "\tu - annuler la dernière recherche", + "commandlist-notification/pause": "\tp - basculer sur pause", + "commandlist-notification/seek": "\t[s][+-]temps - recherche la valeur de temps donnée, si + ou - n'est pas spécifié c'est le temps absolu en secondes ou min:sec", + "commandlist-notification/help": "\th - cette aide", + "commandlist-notification/toggle": "\tt - bascule si vous êtes prêt à regarder ou non", + "commandlist-notification/create": "\tc [nom] - crée une salle gérée en utilisant le nom de la salle actuelle", + "commandlist-notification/auth": "\tun [mot de passe] - s'authentifier en tant qu'opérateur de salle avec le mot de passe opérateur", + "commandlist-notification/chat": "\tch [message] - envoyer un message de chat dans une pièce", + "commandList-notification/queue": "\tqa [fichier/url] - ajoute un fichier ou une URL au bas de la liste de lecture", + "commandList-notification/playlist": "\tql - afficher la liste de lecture actuelle", + "commandList-notification/select": "\tqs [index] - sélectionnez l'entrée donnée dans la liste de lecture", + "commandList-notification/delete": "\tqd [index] - supprime l'entrée donnée de la liste de lecture", + "syncplay-version-notification": "Version de Syncplay: {}", # syncplay.version + "more-info-notification": "Plus d'informations disponibles sur: {}", # projectURL + + "gui-data-cleared-notification": "Syncplay a effacé les données d'état de chemin et de fenêtre utilisées par l'interface graphique.", + "language-changed-msgbox-label": "La langue sera modifiée lorsque vous exécuterez Syncplay.", + "promptforupdate-label": "Est-ce que Syncplay peut vérifier automatiquement les mises à jour de temps en temps?", + + "media-player-latency-warning": "Avertissement: Le lecteur multimédia a mis {}secondes à répondre. Si vous rencontrez des problèmes de synchronisation, fermez les applications pour libérer des ressources système, et si cela ne fonctionne pas, essayez un autre lecteur multimédia.", # Seconds to respond + "mpv-unresponsive-error": "mpv n'a pas répondu pendant {} secondes et semble donc avoir mal fonctionné. Veuillez redémarrer Syncplay.", # Seconds to respond + + # Client prompts + "enter-to-exit-prompt": "Appuyez sur entrée pour quitter", + + # Client errors + "missing-arguments-error": "Certains arguments nécessaires sont manquants, reportez-vous à --help", + "server-timeout-error": "La connexion avec le serveur a expiré", + "mpc-slave-error": "Impossible de démarrer MPC en mode esclave!", + "mpc-version-insufficient-error": "La version MPC n'est pas suffisante, veuillez utiliser `mpc-hc` >= `{}`", + "mpc-be-version-insufficient-error": "La version MPC n'est pas suffisante, veuillez utiliser `mpc-be` >= `{}`", + "mpv-version-error": "Syncplay n'est pas compatible avec cette version de mpv. Veuillez utiliser une version différente de mpv (par exemple Git HEAD).", + "mpv-failed-advice": "La raison pour laquelle mpv ne peut pas démarrer peut être due à l'utilisation d'arguments de ligne de commande non pris en charge ou à une version non prise en charge de mpv.", + "player-file-open-error": "Le lecteur n'a pas réussi à ouvrir le fichier", + "player-path-error": "Le chemin du lecteur n'est pas défini correctement. Les lecteurs pris en charge sont : mpv, mpv.net, VLC, MPC-HC, MPC-BE, mplayer2 et IINA", + "hostname-empty-error": "Le nom d'hôte ne peut pas être vide", + "empty-error": "{} ne peut pas être vide", # Configuration + "media-player-error": "Media player error: \"{}\"", # Error line + "unable-import-gui-error": "Impossible d'importer les bibliothèques GUI. Si vous n'avez pas installé PySide, vous devrez l'installer pour que l'interface graphique fonctionne.", + "unable-import-twisted-error": "Impossible d'importer Twisted. Veuillez installer Twisted v16.4.0 ou une version ultérieure.", + + "arguments-missing-error": "Certains arguments nécessaires sont manquants, reportez-vous à --help", + + "unable-to-start-client-error": "Impossible de démarrer le client", + + "player-path-config-error": "Le chemin du lecteur n'est pas défini correctement. Les lecteurs pris en charge sont : mpv, mpv.net, VLC, MPC-HC, MPC-BE, mplayer2 et IINA.", + "no-file-path-config-error": "Le fichier doit être sélectionné avant de démarrer votre lecteur", + "no-hostname-config-error": "Le nom d'hôte ne peut pas être vide", + "invalid-port-config-error": "Le port doit être valide", + "empty-value-config-error": "{} ne peut pas être vide", # Config option + + "not-json-error": "Pas une chaîne encodée en json", + "hello-arguments-error": "Pas assez d'arguments pour Hello", # DO NOT TRANSLATE + "version-mismatch-error": "Non-concordance entre les versions du client et du serveur", + "vlc-failed-connection": "Échec de la connexion à VLC. Si vous n'avez pas installé syncplay.lua et utilisez la dernière version de VLC, veuillez vous référer à https://syncplay.pl/LUA/ pour obtenir des instructions. Syncplay et VLC 4 ne sont actuellement pas compatibles, utilisez donc VLC 3 ou une alternative telle que mpv.", + "vlc-failed-noscript": "VLC a signalé que le script d'interface syncplay.lua n'a pas été installé. Veuillez vous référer à https://syncplay.pl/LUA/ pour obtenir des instructions.", + "vlc-failed-versioncheck": "Cette version de VLC n'est pas prise en charge par Syncplay.", + "vlc-initial-warning": "VLC ne fournit pas toujours des informations de position précises à Syncplay, en particulier pour les fichiers .mp4 et .avi. Si vous rencontrez des problèmes de recherche erronée, essayez un autre lecteur multimédia tel que mpv (ou mpv.net pour les utilisateurs de Windows).", + + "feature-sharedPlaylists": "listes de lecture partagées", # used for not-supported-by-server-error + "feature-chat": "chat", # used for not-supported-by-server-error + "feature-readiness": "préparation", # used for not-supported-by-server-error + "feature-managedRooms": "salons gérés", # used for not-supported-by-server-error + + "not-supported-by-server-error": "La fonctionnalité {} n'est pas prise en charge par ce serveur.", # feature + "shared-playlists-not-supported-by-server-error": "La fonctionnalité de listes de lecture partagées peut ne pas être prise en charge par le serveur. Pour s'assurer qu'il fonctionne correctement, il faut un serveur exécutant Syncplay {}+, mais le serveur exécute Syncplay {}.", # minVersion, serverVersion + "shared-playlists-disabled-by-server-error": "La fonctionnalité de liste de lecture partagée a été désactivée dans la configuration du serveur. Pour utiliser cette fonctionnalité, vous devrez vous connecter à un autre serveur.", + + "invalid-seek-value": "Valeur de recherche non valide", + "invalid-offset-value": "Valeur de décalage non valide", + + "switch-file-not-found-error": "Impossible de passer au fichier ''. Syncplay recherche dans les répertoires multimédias spécifiés.", # File not found + "folder-search-timeout-error": "La recherche de médias dans les répertoires de médias a été abandonnée car la recherche dans '{}' a pris trop de temps. Cela se produira si vous sélectionnez un dossier avec trop de sous-dossiers dans votre liste de dossiers multimédias à parcourir. Pour que le basculement automatique des fichiers fonctionne à nouveau, veuillez sélectionner Fichier->Définir les répertoires multimédias dans la barre de menu et supprimez ce répertoire ou remplacez-le par un sous-dossier approprié. Si le dossier est correct, vous pouvez le réactiver en sélectionnant Fichier->Définir les répertoires multimédias et en appuyant sur «OK».", # Folder + "folder-search-first-file-timeout-error": "La recherche de média dans '{}' a été abandonnée car elle a pris trop de temps pour accéder au répertoire. Cela peut arriver s'il s'agit d'un lecteur réseau ou si vous configurez votre lecteur pour qu'il ralentisse après une période d'inactivité. Pour que le basculement automatique des fichiers fonctionne à nouveau, accédez à Fichier-> Définir les répertoires multimédias et supprimez le répertoire ou résolvez le problème (par exemple en modifiant les paramètres d'économie d'énergie).", # Folder + "added-file-not-in-media-directory-error": "Vous avez chargé un fichier dans '{}' qui n'est pas un répertoire média connu. Vous pouvez l'ajouter en tant que répertoire multimédia en sélectionnant Fichier->Définir les répertoires multimédias dans la barre de menus.", # Folder + "no-media-directories-error": "Aucun répertoire multimédia n'a été défini. Pour que les fonctionnalités de liste de lecture partagée et de changement de fichier fonctionnent correctement, sélectionnez Fichier-> Définir les répertoires multimédias et spécifiez où Syncplay doit rechercher les fichiers multimédias.", + "cannot-find-directory-error": "Impossible de trouver le répertoire multimédia '{}'. Pour mettre à jour votre liste de répertoires multimédias, veuillez sélectionner Fichier->Définir les répertoires multimédias dans la barre de menu et spécifiez où Syncplay doit chercher pour trouver les fichiers multimédias.", + + "failed-to-load-server-list-error": "Échec du chargement de la liste des serveurs publics. Veuillez visiter https://www.syncplay.pl/ dans votre navigateur.", + + # Client arguments + "argument-description": "Solution pour synchroniser la lecture de plusieurs instances de lecteur multimédia sur le réseau.", + "argument-epilog": "Si aucune option n'est fournie, les valeurs _config seront utilisées", + "nogui-argument": "masquer l'interface graphique", + "host-argument": "adresse du serveur", + "name-argument": "nom d'utilisateur souhaité", + "debug-argument": "Mode débogage", + "force-gui-prompt-argument": "faire apparaître l'invite de configuration", + "no-store-argument": "ne pas stocker de valeurs dans .syncplay", + "room-argument": "salon par défaut", + "password-argument": "Mot de passe du serveur", + "player-path-argument": "chemin d'accès à l'exécutable de votre lecteur", + "file-argument": "fichier à lire", + "args-argument": 'player options, if you need to pass options starting with - prepend them with single \'--\' argument', + "clear-gui-data-argument": "réinitialise les données GUI du chemin et de l'état de la fenêtre stockées en tant que QSettings", + "language-argument": "langue pour les messages Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)", + + "version-argument": "imprime votre version", + "version-message": "Vous utilisez Syncplay version {} ({})", + + "load-playlist-from-file-argument": "charge la liste de lecture à partir d'un fichier texte (une entrée par ligne)", + + + # Client labels + "config-window-title": "configuration Syncplay", + + "connection-group-title": "Paramètres de connexion", + "host-label": "Adresse du serveur:", + "name-label": "Nom d'utilisateur (facultatif):", + "password-label": "Mot de passe du serveur (le cas échéant):", + "room-label": "Salon par défaut:", + "roomlist-msgbox-label": "Modifier la liste des salons (une par ligne)", + + "media-setting-title": "Paramètres du lecteur multimédia", + "executable-path-label": "Chemin d'accès au lecteur multimédia:", + "media-path-label": "Chemin d'accès à la vidéo (facultatif):", + "player-arguments-label": "Arguments du joueur (le cas échéant):", + "browse-label": "Parcourir", + "update-server-list-label": "Mettre à jour la liste", + + "more-title": "Afficher plus de paramètres", + "never-rewind-value": "Jamais", + "seconds-suffix": "secs", + "privacy-sendraw-option": "Envoyer brut", + "privacy-sendhashed-option": "Envoyer haché", + "privacy-dontsend-option": "Ne pas envoyer", + "filename-privacy-label": "Informations sur le nom de fichier:", + "filesize-privacy-label": "Informations sur la taille du fichier:", + "checkforupdatesautomatically-label": "Rechercher automatiquement les mises à jour de Syncplay", + "autosavejoinstolist-label": "Ajouter les salons que vous rejoignez à la liste des salons", + "slowondesync-label": "Ralentissement en cas de désynchronisation mineure (non pris en charge sur MPC-HC/BE)", + "rewindondesync-label": "Retour en arrière en cas de désynchronisation majeure (recommandé)", + "fastforwardondesync-label": "Avance rapide en cas de retard (recommandé)", + "dontslowdownwithme-label": "Ne jamais ralentir ou rembobiner les autres (expérimental)", + "pausing-title": "Pause", + "pauseonleave-label": "Pause lorsque l'utilisateur quitte (par exemple s'il est déconnecté)", + "readiness-title": "État de préparation initial", + "readyatstart-label": "Définissez-moi comme «prêt à regarder» par défaut", + "forceguiprompt-label": "Ne pas toujours afficher la fenêtre de configuration Syncplay", # (Inverted) + "showosd-label": "Activer les Messages OSD", + + "showosdwarnings-label": "Inclure des avertissements (par exemple, lorsque les fichiers sont différents, les utilisateurs ne sont pas prêts)", + "showsameroomosd-label": "Inclure des événements dans votre salon", + "shownoncontrollerosd-label": "Inclure les événements des non-opérateurs dans les salons gérés", + "showdifferentroomosd-label": "Inclure des événements dans d'autres salons", + "showslowdownosd-label": "Inclure les notifications de ralentissement/annulation", + "language-label": "Langue:", + "automatic-language": "Défaut ({})", # Default language + "showdurationnotification-label": "Avertir des incohérences de durée de média", + "basics-label": "Réglages de base", + "readiness-label": "Jouer pause", + "misc-label": "Divers", + "core-behaviour-title": "Comportement du salon principal", + "syncplay-internals-title": "procédures internes", + "syncplay-mediasearchdirectories-title": "Répertoires pour rechercher des médias", + "syncplay-mediasearchdirectories-label": "Répertoires pour rechercher des médias (un chemin par ligne)", + "sync-label": "Synchroniser", + "sync-otherslagging-title": "Si d'autres sont à la traîne...", + "sync-youlaggging-title": "Si vous êtes à la traîne...", + "messages-label": "Messages", + "messages-osd-title": "Paramètres d'affichage à l'écran", + "messages-other-title": "Autres paramètres d'affichage", + "chat-label": "Chat", + "privacy-label": "Sécurité données", # Currently unused, but will be brought back if more space is needed in Misc tab + "privacy-title": "Paramètres de confidentialité", + "unpause-title": "Si vous appuyez sur play, définissez comme prêt et:", + "unpause-ifalreadyready-option": "Annuler la pause si déjà défini comme prêt", + "unpause-ifothersready-option": "Reprendre la pause si déjà prêt ou si d'autres personnes dans la pièce sont prêtes (par défaut)", + "unpause-ifminusersready-option": "Annuler la pause si déjà prêt ou si tous les autres sont prêts et utilisateurs minimum prêts", + "unpause-always": "Toujours reprendre", + "syncplay-trusteddomains-title": "Domaines de confiance (pour les services de streaming et le contenu hébergé)", + + "chat-title": "Saisie du message de discussion", + "chatinputenabled-label": "Activer la saisie de discussion via mpv", + "chatdirectinput-label": "Autoriser la saisie de discussion instantanée (éviter d'avoir à appuyer sur la touche Entrée pour discuter)", + "chatinputfont-label": "Police de caractères pour la saisie sur le Chat ", + "chatfont-label": "Définir la fonte", + "chatcolour-label": "Définir la couleur", + "chatinputposition-label": "Position de la zone de saisie des messages dans mpv", + "chat-top-option": "Haut", + "chat-middle-option": "Milieu", + "chat-bottom-option": "Bas", + "chatoutputheader-label": "Sortie du message de discussion", + "chatoutputfont-label": "Police de sortie du chat", + "chatoutputenabled-label": "Activer la sortie du chat dans le lecteur multimédia (mpv uniquement pour l'instant)", + "chatoutputposition-label": "Mode de sortie", + "chat-chatroom-option": "Style de salon de discussion", + "chat-scrolling-option": "Style de défilement", + + "mpv-key-tab-hint": "[TAB] pour basculer l'accès aux raccourcis des touches de la ligne alphabétique.", + "mpv-key-hint": "[ENTER] pour envoyer un message. [ESC] pour quitter le mode chat.", + "alphakey-mode-warning-first-line": "Vous pouvez temporairement utiliser les anciennes liaisons mpv avec les touches az.", + "alphakey-mode-warning-second-line": "Appuyez sur [TAB] pour revenir au mode de discussion Syncplay.", + + "help-label": "Aider", + "reset-label": "Réinitialiser", + "run-label": "Exécuter Syncplay", + "storeandrun-label": "Stocker la configuration et exécuter Syncplay", + + "contact-label": "Feel free to e-mail dev@syncplay.pl, create an issue to report a bug/problem via GitHub, start a discussion to make a suggestion or ask a question via GitHub, like us on Facebook, follow us on Twitter, or visit https://syncplay.pl/. Do not use Syncplay to send sensitive information.", + + "joinroom-label": "Rejoindre la salle", + "joinroom-menu-label": "Rejoindre la salle {}", + "seektime-menu-label": "Chercher le temps", + "undoseek-menu-label": "Annuler la recherche", + "play-menu-label": "Jouer", + "pause-menu-label": "Pause", + "playbackbuttons-menu-label": "Afficher les boutons de lecture", + "autoplay-menu-label": "Afficher le bouton de lecture automatique", + "autoplay-guipushbuttonlabel": "Jouer quand tout est prêt", + "autoplay-minimum-label": "Utilisateurs minimum:", + + "sendmessage-label": "Envoyer", + + "ready-guipushbuttonlabel": "Je suis prêt à regarder !", + + "roomuser-heading-label": "Salon / Utilisateur", + "size-heading-label": "Taille", + "duration-heading-label": "Durée", + "filename-heading-label": "Nom de fichier", + "notifications-heading-label": "Notifications", + "userlist-heading-label": "Liste de qui joue quoi", + + "browseformedia-label": "Parcourir les fichiers multimédias", + + "file-menu-label": "&Fichier", # & precedes shortcut key + "openmedia-menu-label": "&Ouvrir le fichier multimédia", + "openstreamurl-menu-label": "Ouvrir l'URL du &flux multimédia", + "setmediadirectories-menu-label": "Définir les &répertoires multimédias", + "loadplaylistfromfile-menu-label": "&Charger la liste de lecture à partir du fichier", + "saveplaylisttofile-menu-label": "&Enregistrer la liste de lecture dans un fichier", + "exit-menu-label": "Sortir", + "advanced-menu-label": "&Avancée", + "window-menu-label": "&Fenêtre", + "setoffset-menu-label": "Définir &décalage", + "createcontrolledroom-menu-label": "&Créer une salon à gérer", + "identifyascontroller-menu-label": "&Identifier en tant qu'opérateur de salon", + "settrusteddomains-menu-label": "Définir des &domaines de confiance", + "addtrusteddomain-menu-label": "Ajouter {} comme domaine de confiance", # Domain + + "edit-menu-label": "&Éditer", + "cut-menu-label": "Couper", + "copy-menu-label": "&Copier", + "paste-menu-label": "&Coller", + "selectall-menu-label": "&Tout sélectionner", + + "playback-menu-label": "&Relecture", + + "help-menu-label": "&Aide", + "userguide-menu-label": "Ouvrir le &guide de l'utilisateur", + "update-menu-label": "Rechercher et mettre à jour", + + "startTLS-initiated": "Tentative de connexion sécurisée", + "startTLS-secure-connection-ok": "Connexion sécurisée établie ({})", + "startTLS-server-certificate-invalid": "Échec de la Connexion Sécurisée. Le serveur utilise un certificat de sécurité non valide. Cette communication pourrait être interceptée par un tiers. Pour plus de détails et de dépannage, voir ici .", + "startTLS-server-certificate-invalid-DNS-ID": "Syncplay ne fait pas confiance à ce serveur car il utilise un certificat qui n'est pas valide pour son nom d'hôte.", + "startTLS-not-supported-client": "Ce client ne prend pas en charge TLS", + "startTLS-not-supported-server": "Ce serveur ne prend pas en charge TLS", + + # TLS certificate dialog + "tls-information-title": "Détails du certificat", + "tls-dialog-status-label": "Syncplay utilise une connexion cryptée à {}.", + "tls-dialog-desc-label": "Le cryptage avec un certificat numérique préserve la confidentialité des informations lorsqu'elles sont envoyées vers ou depuis le serveur {}.", + "tls-dialog-connection-label": "Informations chiffrées à l'aide de Transport Layer Security (TLS), version {} avec la suite de chiffrement: {}.", + "tls-dialog-certificate-label": "Certificat délivré par {} valable jusqu'au {}.", + + # About dialog + "about-menu-label": "&À propos de la lecture synchronisée", + "about-dialog-title": "À propos de Syncplay", + "about-dialog-release": "Version {} release {}", + "about-dialog-license-text": "Sous licence Apache, version 2.0", + "about-dialog-license-button": "Licence", + "about-dialog-dependencies": "Dépendances", + + "setoffset-msgbox-label": "Définir le décalage", + "offsetinfo-msgbox-label": "Offset (voir https://syncplay.pl/guide/ pour les instructions d'utilisation):", + + "promptforstreamurl-msgbox-label": "Ouvrir l'URL du flux multimédia", + "promptforstreamurlinfo-msgbox-label": "URL de diffusion", + + "addfolder-label": "Ajouter le dossier", + + "adduris-msgbox-label": "Ajouter des URL à la liste de lecture (une par ligne)", + "editplaylist-msgbox-label": "Définir la liste de lecture (une par ligne)", + "trusteddomains-msgbox-label": "Domaines vers lesquels vous pouvez basculer automatiquement (un par ligne)", + + "createcontrolledroom-msgbox-label": "Créer un salon à gérer", + "controlledroominfo-msgbox-label": "Enter name of managed room\r\n(see https://syncplay.pl/guide/ for usage instructions):", + + "identifyascontroller-msgbox-label": "S'identifier en tant qu'opérateur de salon", + "identifyinfo-msgbox-label": "Enter operator password for this room\r\n(see https://syncplay.pl/guide/ for usage instructions):", + + "public-server-msgbox-label": "Sélectionnez le serveur public pour cette session de visualisation", + + "megabyte-suffix": "Mo", + + # Tooltips + + "host-tooltip": "Nom d'hôte ou IP auquel se connecter, incluant éventuellement le port (par exemple syncplay.pl:8999). Uniquement synchronisé avec des personnes sur le même serveur/port.", + "name-tooltip": "Surnom sous lequel vous serez connu. Pas d'inscription, donc peut facilement changer plus tard. Nom aléatoire généré si aucun n'est spécifié.", + "password-tooltip": "Les mots de passe ne sont nécessaires que pour se connecter à des serveurs privés.", + "room-tooltip": "Le salon à rejoindre lors de la connexion peut être presque n'importe quoi, mais vous ne serez synchronisé qu'avec des personnes dans le même salon.", + + "edit-rooms-tooltip": "Modifier la liste des salons.", + + "executable-path-tooltip": "Emplacement du lecteur multimédia pris en charge que vous avez choisi (mpv, mpv.net, VLC, MPC-HC/BE, mplayer2 ou IINA).", + "media-path-tooltip": "Emplacement de la vidéo ou du flux à ouvrir. Nécessaire pour mplayer2.", + "player-arguments-tooltip": "Arguments/commutateurs de ligne de commande supplémentaires à transmettre à ce lecteur multimédia.", + "mediasearcdirectories-arguments-tooltip": "Répertoires dans lesquels Syncplay recherchera les fichiers multimédias, par exemple lorsque vous utilisez la fonctionalité cliquer pour basculer. Syncplay recherchera récursivement dans les sous-dossiers.", + + "more-tooltip": "Afficher les paramètres moins fréquemment utilisés.", + "filename-privacy-tooltip": "Mode de confidentialité pour l'envoi du nom de fichier en cours de lecture au serveur.", + "filesize-privacy-tooltip": "Mode de confidentialité pour l'envoi de la taille du fichier en cours de lecture au serveur.", + "privacy-sendraw-tooltip": "Envoyez ces informations sans brouillage. Il s'agit de l'option par défaut avec la plupart des fonctionnalités.", + "privacy-sendhashed-tooltip": "Envoyez une version hachée des informations, les rendant moins visibles pour les autres clients.", + "privacy-dontsend-tooltip": "N'envoyez pas ces informations au serveur. Cela garantit une confidentialité maximale.", + "checkforupdatesautomatically-tooltip": "Vérifiez régulièrement sur le site Web de Syncplay si une nouvelle version de Syncplay est disponible.", + "autosavejoinstolist-tooltip": "Lorsque vous rejoignez un salon sur un serveur, mémorisez automatiquement le nom de la salle dans la liste des salons à rejoindre.", + "slowondesync-tooltip": "Réduisez temporairement le taux de lecture si nécessaire pour vous synchroniser avec les autres téléspectateurs. Non pris en charge sur MPC-HC/BE.", + "dontslowdownwithme-tooltip": "Cela signifie que les autres ne sont pas ralentis ou rembobinés si votre lecture est en retard. Utile pour les opérateurs de salon.", + "pauseonleave-tooltip": "Mettez la lecture en pause si vous êtes déconnecté ou si quelqu'un quitte votre salon.", + "readyatstart-tooltip": "Définissez-vous comme «prêt» au début (sinon, vous êtes défini comme «pas prêt» jusqu'à ce que vous changiez votre état de préparation)", + "forceguiprompt-tooltip": "La boîte de dialogue de configuration ne s'affiche pas lors de l'ouverture d'un fichier avec Syncplay.", # (Inverted) + "nostore-tooltip": "Exécutez Syncplay avec la configuration donnée, mais ne stockez pas les modifications de manière permanente.", # (Inverted) + "rewindondesync-tooltip": "Revenez en arrière au besoin pour vous synchroniser. La désactivation de cette option peut entraîner des désynchronisations majeures!", + "fastforwardondesync-tooltip": "Avancez en cas de désynchronisation avec l'opérateur de la salle (ou votre position factice si l'option «Ne jamais ralentir ou rembobiner les autres» est activée).", + "showosd-tooltip": "Envoie des messages Syncplay à l'OSD du lecteur multimédia.", + "showosdwarnings-tooltip": "Afficher des avertissements en cas de lecture d'un fichier différent, seul dans la pièce, utilisateurs non prêts, etc.", + "showsameroomosd-tooltip": "Afficher les notifications OSD pour les événements liés à l'utilisateur du salon.", + "shownoncontrollerosd-tooltip": "Afficher les notifications OSD pour les événements relatifs aux non-opérateurs qui se trouvent dans les salles gérées.", + "showdifferentroomosd-tooltip": "Afficher les notifications OSD pour les événements liés à l'absence de l'utilisateur du salon.", + "showslowdownosd-tooltip": "Afficher les notifications de ralentissement / de retour au décalage temps.", + "showdurationnotification-tooltip": "Utile lorsqu'un segment dans un fichier en plusieurs parties est manquant, mais peut entraîner des faux positifs.", + "language-tooltip": "Langue à utiliser par Syncplay.", + "unpause-always-tooltip": "Si vous appuyez sur unpause, cela vous définit toujours comme prêt et non-pause, plutôt que de simplement vous définir comme prêt.", + "unpause-ifalreadyready-tooltip": "Si vous appuyez sur unpause lorsque vous n'êtes pas prêt, cela vous mettra comme prêt - appuyez à nouveau sur unpause pour reprendre la pause.", + "unpause-ifothersready-tooltip": "Si vous appuyez sur unpause lorsque vous n'êtes pas prêt, il ne reprendra la pause que si d'autres sont prêts.", + "unpause-ifminusersready-tooltip": "Si vous appuyez sur annuler la pause lorsqu'il n'est pas prêt, il ne s'arrêtera que si d'autres personnes sont prêtes et que le seuil minimal d'utilisateurs est atteint.", + "trusteddomains-arguments-tooltip": "Domaines vers lesquels Syncplay peut basculer automatiquement lorsque les listes de lecture partagées sont activées.", + + "chatinputenabled-tooltip": "Activer la saisie du chat dans mpv (appuyez sur Entrée pour discuter, Entrée pour envoyer, Échap pour annuler)", + "chatdirectinput-tooltip": "Évitez d'avoir à appuyer sur «enter» pour passer en mode de saisie de discussion dans mpv. Appuyez sur TAB dans mpv pour désactiver temporairement cette fonctionnalité.", + "font-label-tooltip": "Police utilisée lors de la saisie de messages de discussion dans mpv. Côté client uniquement, n'affecte donc pas ce que les autres voient.", + "set-input-font-tooltip": "Famille de polices utilisée lors de la saisie de messages de discussion dans mpv. Côté client uniquement, n'affecte donc pas ce que les autres voient.", + "set-input-colour-tooltip": "Couleur de police utilisée lors de la saisie de messages de discussion dans mpv. Côté client uniquement, n'affecte donc pas ce que les autres voient.", + "chatinputposition-tooltip": "Emplacement dans mpv où le texte d'entrée de discussion apparaîtra lorsque vous appuyez sur Entrée et tapez.", + "chatinputposition-top-tooltip": "Placez l'entrée de discussion en haut de la fenêtre mpv.", + "chatinputposition-middle-tooltip": "Placez l'entrée de discussion au point mort de la fenêtre mpv.", + "chatinputposition-bottom-tooltip": "Placez l'entrée de discussion en bas de la fenêtre mpv.", + "chatoutputenabled-tooltip": "Afficher les messages de discussion dans l'OSD (si pris en charge par le lecteur multimédia).", + "font-output-label-tooltip": "Police de sortie du chat.", + "set-output-font-tooltip": "Police utilisée pour l'affichage des messages de discussion.", + "chatoutputmode-tooltip": "Comment les messages de chat sont affichés.", + "chatoutputmode-chatroom-tooltip": "Affichez les nouvelles lignes de discussion directement sous la ligne précédente.", + "chatoutputmode-scrolling-tooltip": "Faites défiler le texte du chat de droite à gauche.", + + "help-tooltip": "Ouvre le guide de l'utilisateur de Syncplay.pl.", + "reset-tooltip": "Réinitialisez tous les paramètres à la configuration par défaut.", + "update-server-list-tooltip": "Connectez-vous à syncplay.pl pour mettre à jour la liste des serveurs publics.", + + "sslconnection-tooltip": "Connecté en toute sécurité au serveur. Cliquez pour obtenir les détails du certificat.", + + "joinroom-tooltip": "Quitter la salle actuelle et rejoindre le salon spécifié.", + "seektime-msgbox-label": "Aller au temps spécifié (en secondes / min:sec). Utilisez +/- pour la recherche relative.", + "ready-tooltip": "Indique si vous êtes prêt à regarder.", + "autoplay-tooltip": "Lecture automatique lorsque tous les utilisateurs qui ont un indicateur de disponibilité sont prêts et que le seuil d'utilisateur minimum est atteint.", + "switch-to-file-tooltip": "Double-cliquez pour passer à {}", # Filename + "sendmessage-tooltip": "Envoyer un message au salon", + + # In-userlist notes (GUI) + "differentsize-note": "Différentes tailles!", + "differentsizeandduration-note": "Taille et durée différentes !", + "differentduration-note": "Durée différente !", + "nofile-note": "(Aucun fichier en cours de lecture)", + + # Server messages to client + "new-syncplay-available-motd-message": "Vous utilisez Syncplay {} mais une version plus récente est disponible sur https://syncplay.pl", # ClientVersion + + # Server notifications + "welcome-server-notification": "Bienvenue sur le serveur Syncplay, ver.", # version + "client-connected-room-server-notification": "({2}) connecté à la salle '{1}'", # username, host, room + "client-left-server-notification": "a quitté le serveur", # name + "no-salt-notification": "VEUILLEZ NOTER: Pour permettre aux mots de passe d'opérateur de salle générés par cette instance de serveur de fonctionner lorsque le serveur est redémarré, veuillez ajouter l'argument de ligne de commande suivant lors de l'exécution du serveur Syncplay à l'avenir: --salt {}", # Salt + + + # Server arguments + "server-argument-description": "Solution pour synchroniser la lecture de plusieurs instances de lecteur multimédia sur le réseau. Instance de serveur", + "server-argument-epilog": "Si aucune option n'est fournie, les valeurs _config seront utilisées", + "server-port-argument": "port TCP du serveur", + "server-password-argument": "Mot de passe du serveur", + "server-isolate-room-argument": "faut-il isoler les salons ?", + "server-salt-argument": "chaîne aléatoire utilisée pour générer les mots de passe des salons gérés", + "server-disable-ready-argument": "désactiver la fonction de préparation", + "server-motd-argument": "chemin vers le fichier à partir duquel motd sera récupéré", + "server-chat-argument": "Le chat doit-il être désactivé?", + "server-chat-maxchars-argument": "Nombre maximum de caractères dans un message de discussion (la valeur par défaut est {})", # Default number of characters + "server-maxusernamelength-argument": "Nombre maximum de caractères dans un nom d'utilisateur (la valeur par défaut est {})", + "server-stats-db-file-argument": "Activer les statistiques du serveur à l'aide du fichier db SQLite fourni", + "server-startTLS-argument": "Activer les connexions TLS à l'aide des fichiers de certificat dans le chemin fourni", + "server-messed-up-motd-unescaped-placeholders": "Le message du jour a des espaces réservés non échappés. Tous les signes $ doivent être doublés ($$).", + "server-messed-up-motd-too-long": "Le message du jour est trop long: {}caractères maximum, {} donnés.", + + # Server errors + "unknown-command-server-error": "Commande inconnue {}", # message + "not-json-server-error": "Pas une chaîne encodée json {}", # message + "line-decode-server-error": "Pas une chaîne utf-8", + "not-known-server-error": "Vous devez être connu du serveur avant d'envoyer cette commande", + "client-drop-server-error": "Client drop: {} -- {}", # host, error + "password-required-server-error": "Mot de passe requis", + "wrong-password-server-error": "Mauvais mot de passe fourni", + "hello-server-error": "Pas assez d'arguments pour Hello", # DO NOT TRANSLATE + + # Playlists + "playlist-selection-changed-notification": "{} a modifié la sélection de la liste de lecture", # Username + "playlist-contents-changed-notification": "{} a mis à jour la liste de lecture", # Username + "cannot-find-file-for-playlist-switch-error": "Impossible de trouver le fichier {} dans les répertoires multimédias pour le changement de liste de lecture!", # Filename + "cannot-add-duplicate-error": "Impossible d'ajouter la deuxième entrée pour '{}' à la liste de lecture car aucun doublon n'est autorisé.", # Filename + "cannot-add-unsafe-path-error": "Impossible de charger automatiquement {}, car il ne se trouve pas sur un domaine approuvé. Vous pouvez basculer manuellement vers l'URL en double-cliquant dessus dans la liste de lecture et ajouter des domaines de confiance via Fichier->Avancé->Définir les domaines de confiance. Si vous faites un clic droit sur une URL, vous pouvez ajouter son domaine en tant que domaine de confiance via le menu contextuel.", # Filename + "sharedplaylistenabled-label": "Activer les listes de lecture partagées", + "removefromplaylist-menu-label": "Supprimer de la liste de lecture", + "shuffleremainingplaylist-menu-label": "Mélanger la liste de lecture restante", + "shuffleentireplaylist-menu-label": "Mélanger toute la liste de lecture", + "undoplaylist-menu-label": "Annuler la dernière modification de la liste de lecture", + "addfilestoplaylist-menu-label": "Ajouter des fichiers au bas de la liste de lecture", + "addurlstoplaylist-menu-label": "Ajouter des URL au bas de la liste de lecture", + "editplaylist-menu-label": "Modifier la liste de lecture", + + "open-containing-folder": "Ouvrir le dossier contenant ce fichier", + "addyourfiletoplaylist-menu-label": "Ajoutez votre fichier à la liste de lecture", + "addotherusersfiletoplaylist-menu-label": "Ajouter le fichier de {} à la liste de lecture", # [Username] + "addyourstreamstoplaylist-menu-label": "Ajoutez votre flux à la liste de lecture", + "addotherusersstreamstoplaylist-menu-label": "Ajouter {}' stream à la playlist", # [Username] + "openusersstream-menu-label": "Ouvrir le flux de {}", # [username]'s + "openusersfile-menu-label": "Ouvrir le fichier de {}", # [username]'s + + "playlist-instruction-item-message": "Faites glisser le fichier ici pour l'ajouter à la liste de lecture partagée.", + "sharedplaylistenabled-tooltip": "Les opérateurs de salle peuvent ajouter des fichiers à une liste de lecture synchronisée pour permettre à tout le monde de regarder facilement la même chose. Configurez les répertoires multimédias sous «Divers».", + + "playlist-empty-error": "La liste de lecture est actuellement vide.", + "playlist-invalid-index-error": "Index de liste de lecture non valide", +} diff --git a/syncplay/messages_it.py b/syncplay/messages_it.py index 9a38b5f..ede6eff 100755 --- a/syncplay/messages_it.py +++ b/syncplay/messages_it.py @@ -176,7 +176,7 @@ it = { "file-argument": 'file da riprodurre', "args-argument": 'opzioni del player, se hai bisogno di utilizzare opzioni che iniziano con - anteponi un singolo \'--\'', "clear-gui-data-argument": 'ripristina il percorso e i dati impostati tramite interfaccia grafica e salvati come QSettings', - "language-argument": 'lingua per i messaggi di Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr)', + "language-argument": 'lingua per i messaggi di Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', "version-argument": 'mostra la tua versione', "version-message": "Stai usando la versione di Syncplay {} ({})", diff --git a/syncplay/messages_pt_BR.py b/syncplay/messages_pt_BR.py index ec7b868..705c629 100644 --- a/syncplay/messages_pt_BR.py +++ b/syncplay/messages_pt_BR.py @@ -177,7 +177,7 @@ pt_BR = { "file-argument": 'arquivo a ser tocado', "args-argument": 'opções do reprodutor; se você precisar passar opções começando com -, as preceda com um único argumento \'--\'', "clear-gui-data-argument": 'redefine o caminho e o estado de dados da janela da GUI para as de QSettings', - "language-argument": 'idioma para mensagens do Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr)', + "language-argument": 'idioma para mensagens do Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', "version-argument": 'exibe sua versão', "version-message": "Você está usando o Syncplay versão {} ({})", diff --git a/syncplay/messages_pt_PT.py b/syncplay/messages_pt_PT.py index aae0f40..9ba0918 100644 --- a/syncplay/messages_pt_PT.py +++ b/syncplay/messages_pt_PT.py @@ -176,7 +176,7 @@ pt_PT = { "file-argument": 'ficheiro a ser tocado', "args-argument": 'opções do reprodutor; se você precisar passar opções começando com -, as preceda com um único argumento \'--\'', "clear-gui-data-argument": 'redefine o caminho e o estado de dados da janela da GUI para as de QSettings', - "language-argument": 'idioma para mensagens do Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr)', + "language-argument": 'idioma para mensagens do Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', "version-argument": 'exibe sua versão', "version-message": "Você está usando o Syncplay versão {} ({})", diff --git a/syncplay/messages_ru.py b/syncplay/messages_ru.py index 76236c3..6d71294 100755 --- a/syncplay/messages_ru.py +++ b/syncplay/messages_ru.py @@ -175,7 +175,7 @@ ru = { "file-argument": 'воспроизводимый файл', "args-argument": 'параметры проигрывателя; если нужно передать параметры, начинающиеся с - , то сначала пишите \'--\'', "clear-gui-data-argument": 'сбрасывает путь и данные о состоянии окна GUI, хранимые как QSettings', - "language-argument": 'язык сообщений Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr)', + "language-argument": 'язык сообщений Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', "version-argument": 'выводит номер версии', "version-message": "Вы используете Syncplay версии {} ({})", diff --git a/syncplay/messages_tr.py b/syncplay/messages_tr.py index 293fdca..f518166 100644 --- a/syncplay/messages_tr.py +++ b/syncplay/messages_tr.py @@ -176,7 +176,7 @@ tr = { "file-argument": 'oynatmak için dosya', "args-argument": 'oynatıcı seçenekleri, ile başlayan seçenekleri iletmeniz gerekiyorsa - bunların başına tek \'--\' argümanı ekleyin', "clear-gui-data-argument": 'QSettings olarak depolanan yol ve pencere durumu GUI verilerini sıfırlar', - "language-argument": 'Syncplay mesajları için dil (de/en/ru/it/es/pt_BR/pt_PT/tr)', + "language-argument": 'Syncplay mesajları için dil (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', "version-argument": 'versiyonunuzu yazdırır', "version-message": "Syncplay sürümünü kullanıyorsunuz {} ({})", From 4e7c651d2d9601a0800381e572f07215193b5802 Mon Sep 17 00:00:00 2001 From: Etoh Date: Tue, 21 Dec 2021 15:45:57 +0000 Subject: [PATCH 22/30] Add French TL stubs for new messages --- syncplay/messages_fr.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/syncplay/messages_fr.py b/syncplay/messages_fr.py index 6cb24e5..ff76e77 100644 --- a/syncplay/messages_fr.py +++ b/syncplay/messages_fr.py @@ -82,6 +82,7 @@ fr = { "commandlist-notification/undo": "\tu - annuler la dernière recherche", "commandlist-notification/pause": "\tp - basculer sur pause", "commandlist-notification/seek": "\t[s][+-]temps - recherche la valeur de temps donnée, si + ou - n'est pas spécifié c'est le temps absolu en secondes ou min:sec", + "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position - this is a deprecated feature", # TODO: Translate "commandlist-notification/help": "\th - cette aide", "commandlist-notification/toggle": "\tt - bascule si vous êtes prêt à regarder ou non", "commandlist-notification/create": "\tc [nom] - crée une salle gérée en utilisant le nom de la salle actuelle", @@ -290,6 +291,7 @@ fr = { "autoplay-menu-label": "Afficher le bouton de lecture automatique", "autoplay-guipushbuttonlabel": "Jouer quand tout est prêt", "autoplay-minimum-label": "Utilisateurs minimum:", + "hideemptyrooms-menu-label": "Hide empty persistent rooms", # TODO: Translate "sendmessage-label": "Envoyer", @@ -456,6 +458,7 @@ fr = { # Server messages to client "new-syncplay-available-motd-message": "Vous utilisez Syncplay {} mais une version plus récente est disponible sur 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 # TODO: Translate # Server notifications "welcome-server-notification": "Bienvenue sur le serveur Syncplay, ver.", # version @@ -473,6 +476,8 @@ fr = { "server-salt-argument": "chaîne aléatoire utilisée pour générer les mots de passe des salons gérés", "server-disable-ready-argument": "désactiver la fonction de préparation", "server-motd-argument": "chemin vers le fichier à partir duquel motd sera récupéré", + "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": "Le chat doit-il être désactivé?", "server-chat-maxchars-argument": "Nombre maximum de caractères dans un message de discussion (la valeur par défaut est {})", # Default number of characters "server-maxusernamelength-argument": "Nombre maximum de caractères dans un nom d'utilisateur (la valeur par défaut est {})", From b760ee662a5e4fb80049d0793d40cb5d39384b9d Mon Sep 17 00:00:00 2001 From: Etoh Date: Tue, 21 Dec 2021 15:48:24 +0000 Subject: [PATCH 23/30] Remove extra , --- syncplay/messages_fr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncplay/messages_fr.py b/syncplay/messages_fr.py index ff76e77..14d38d8 100644 --- a/syncplay/messages_fr.py +++ b/syncplay/messages_fr.py @@ -477,7 +477,7 @@ fr = { "server-disable-ready-argument": "désactiver la fonction de préparation", "server-motd-argument": "chemin vers le fichier à partir duquel motd sera récupéré", "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-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": "Le chat doit-il être désactivé?", "server-chat-maxchars-argument": "Nombre maximum de caractères dans un message de discussion (la valeur par défaut est {})", # Default number of characters "server-maxusernamelength-argument": "Nombre maximum de caractères dans un nom d'utilisateur (la valeur par défaut est {})", From bed49869795f5bffd59d3dad657a965c2644b107 Mon Sep 17 00:00:00 2001 From: et0h Date: Sun, 2 Jan 2022 12:49:56 +0000 Subject: [PATCH 24/30] Add Windows Store mpv.net path support (#494) --- syncplay/constants.py | 8 ++++++-- syncplay/players/mpvnet.py | 7 ++++--- syncplay/ui/GuiConfiguration.py | 12 +++++++----- syncplay/utils.py | 7 +++++++ 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/syncplay/constants.py b/syncplay/constants.py index 148de97..04e2d75 100755 --- a/syncplay/constants.py +++ b/syncplay/constants.py @@ -173,8 +173,12 @@ MPLAYER_PATHS = ["mplayer2", "mplayer"] MPV_PATHS = ["mpv", "/opt/mpv/mpv", r"c:\program files\mpv\mpv.exe", r"c:\program files\mpv-player\mpv.exe", r"c:\program Files (x86)\mpv\mpv.exe", r"c:\program Files (x86)\mpv-player\mpv.exe", "/Applications/mpv.app/Contents/MacOS/mpv"] -MPVNET_PATHS = [r"c:\program files\mpv.net\mpvnet.exe", r"c:\program files\mpv.net\mpvnet.exe", - r"c:\program Files (x86)\mpv.net\mpvnet.exe", r"c:\program Files (x86)\mpv.net\mpvnet.exe"] +MPVNET_PATHS = [r"c:\program files\mpv.net\mpvnet.exe", r"c:\program Files (x86)\mpv.net\mpvnet.exe"] +try: + import os + MPVNET_PATHS.append(os.path.expandvars(r'%LOCALAPPDATA%\Microsoft\WindowsApps\mpvnet.exe')) +except: + pass VLC_PATHS = [ r"c:\program files (x86)\videolan\vlc\vlc.exe", r"c:\program files\videolan\vlc\vlc.exe", diff --git a/syncplay/players/mpvnet.py b/syncplay/players/mpvnet.py index 41088d5..ef28d18 100644 --- a/syncplay/players/mpvnet.py +++ b/syncplay/players/mpvnet.py @@ -1,6 +1,7 @@ import os from syncplay import constants from syncplay.players.mpv import MpvPlayer +from syncplay.utils import playerPathExists class MpvnetPlayer(MpvPlayer): @@ -30,11 +31,11 @@ class MpvnetPlayer(MpvPlayer): @staticmethod def getExpandedPath(playerPath): - if not os.path.isfile(playerPath): - if os.path.isfile(playerPath + "mpvnet.exe"): + if not playerPathExists(playerPath): + if playerPathExists(playerPath + "mpvnet.exe"): playerPath += "mpvnet.exe" return playerPath - elif os.path.isfile(playerPath + "\\mpvnet.exe"): + elif playerPathExists(playerPath + "\\mpvnet.exe"): playerPath += "\\mpvnet.exe" return playerPath if os.access(playerPath, os.X_OK): diff --git a/syncplay/ui/GuiConfiguration.py b/syncplay/ui/GuiConfiguration.py index 4feffb3..4a25300 100755 --- a/syncplay/ui/GuiConfiguration.py +++ b/syncplay/ui/GuiConfiguration.py @@ -9,7 +9,7 @@ from syncplay import utils from syncplay.messages import getMessage, getLanguages, setLanguage, getInitialLanguage from syncplay.players.playerFactory import PlayerFactory from syncplay.utils import isBSD, isLinux, isMacOS, isWindows -from syncplay.utils import resourcespath, posixresourcespath +from syncplay.utils import resourcespath, posixresourcespath, playerPathExists from syncplay.vendor.Qt import QtCore, QtWidgets, QtGui, __binding__, IsPySide, IsPySide2 from syncplay.vendor.Qt.QtCore import Qt, QSettings, QCoreApplication, QSize, QPoint, QUrl, QLine, QEventLoop, Signal @@ -210,12 +210,14 @@ class ConfigDialog(QtWidgets.QDialog): self.executablepathCombobox.addItem(foundpath) else: - if not os.path.isfile(playerpath): + if not playerPathExists(playerpath): expandedpath = PlayerFactory().getExpandedPlayerPathByPath(playerpath) - if expandedpath is not None and os.path.isfile(expandedpath): + if expandedpath is not None and playerPathExists(expandedpath): playerpath = expandedpath + elif "mpvnet.exe" in playerpath and playerPathExists(playerpath.replace("mpvnet.exe","mpvnet.com")): + self.executablepathCombobox.addItem(playerpath) - if os.path.isfile(playerpath): + if playerPathExists(playerpath): foundpath = playerpath self.executablepathCombobox.addItem(foundpath) @@ -226,7 +228,7 @@ class ConfigDialog(QtWidgets.QDialog): if path != playerpath: self.executablepathCombobox.addItem(path) - elif os.path.isfile(path) and os.path.normcase(os.path.normpath(path)) != os.path.normcase(os.path.normpath(foundpath)): + elif playerPathExists(path) and os.path.normcase(os.path.normpath(path)) != os.path.normcase(os.path.normpath(foundpath)): self.executablepathCombobox.addItem(path) if foundpath == "": foundpath = path diff --git a/syncplay/utils.py b/syncplay/utils.py index bfc788c..28a9d01 100755 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -417,6 +417,13 @@ def open_system_file_browser(path): else: subprocess.Popen(["xdg-open", path]) +def playerPathExists(path): + if os.path.isfile(path): + return True + elif "mpvnet.exe" in path and os.path.isfile(path.replace("mpvnet.exe","mpvnet.com")): + return True + else: + return False def getListOfPublicServers(): try: From 5b93aeb20e80d3c208a766583363e87ea61ee85b Mon Sep 17 00:00:00 2001 From: Irides Date: Thu, 13 Jan 2022 00:50:30 +0000 Subject: [PATCH 25/30] Correct persistance argument type (#496) The current documentation specifies directories, likely as a result of the original design before use of a database, but the arguments expect files. This simply clarifies the documentation to match the code itself. --- docs/syncplay-server.1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/syncplay-server.1 b/docs/syncplay-server.1 index 7865cc7..7bd1847 100644 --- a/docs/syncplay-server.1 +++ b/docs/syncplay-server.1 @@ -78,11 +78,11 @@ 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\-db-file [directory] +.B \-\-rooms\-db-file [file] 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 \-\-permanent\-rooms-file [directory] +.B \-\-permanent\-rooms-file [file] 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. From 27c74e3fe1baaf540af75b10724772424e71bc2c Mon Sep 17 00:00:00 2001 From: Bruno Russi <33870163+nipkownix@users.noreply.github.com> Date: Sat, 12 Feb 2022 10:35:53 +0000 Subject: [PATCH 26/30] Update PT-BR translation (#497) --- syncplay/messages_pt_BR.py | 82 +++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/syncplay/messages_pt_BR.py b/syncplay/messages_pt_BR.py index 705c629..3c2339f 100644 --- a/syncplay/messages_pt_BR.py +++ b/syncplay/messages_pt_BR.py @@ -58,7 +58,7 @@ pt_BR = { "file-difference-duration": "duração", "alone-in-the-room": "Você está sozinho na sala", - "different-filesize-notification": " (o tamanho do arquivo deles é diferente do seu!)", + "different-filesize-notification": " (o tamanho do arquivo dele(s) é diferente do seu!)", "userlist-playing-notification": "{} está tocando:", # Username "file-played-by-notification": "Arquivo: {} está sendo tocado por:", # File "no-file-played-notification": "{} não está tocando um arquivo", # Username @@ -68,7 +68,7 @@ pt_BR = { "controller-userlist-userflag": "Operador", "ready-userlist-userflag": "Pronto", - "update-check-failed-notification": "Não foi possível verificar automaticamente se o Syncplay {} é a versão mais recente. Deseja visitar https://syncplay.pl/ para checar manualmente por atualizações?", # Syncplay version + "update-check-failed-notification": "Não foi possível verificar automaticamente se o Syncplay {} é a versão mais recente. Deseja visitar https://syncplay.pl/ para checar manualmente por atualizações?", # Syncplay version "syncplay-uptodate-notification": "O Syncplay está atualizado", "syncplay-updateavailable-notification": "Uma nova versão do Syncplay está disponível. Deseja visitar a página de lançamentos?", @@ -82,24 +82,24 @@ pt_BR = { "commandlist-notification/list": "\tl - mostra lista de usuários", "commandlist-notification/undo": "\tu - desfaz último salto", "commandlist-notification/pause": "\tp - alterna pausa", - "commandlist-notification/seek": "\t[s][+-]time - salta para o valor de tempo dado, se + ou - não forem especificados, será o tempo absoluto em segundos ou minutos:segundos", - "commandlist-notification/offset": "\to[+-]duration - offset local playback by the given duration (in seconds or min:sec) from the server seek position - this is a deprecated feature", # TODO: Translate + "commandlist-notification/seek": "\t[s][+-]tempo - salta para o valor de tempo fornecido, se + ou - não forem especificados, será o tempo absoluto em segundos ou minutos:segundos", + "commandlist-notification/offset": "\to[+-]duração - Desloca a reprodução local pelo valor de duração fornecido (em segundos ou min:seg) em relação à posição de tepmo do servidor - essa é uma função descontinuada", "commandlist-notification/help": "\th - esta mensagem de ajuda", "commandlist-notification/toggle": "\tt - alterna o seu status de prontidão para assistir", - "commandlist-notification/create": "\tc [nome] - cria sala gerenciado usando o nome da sala atual", + "commandlist-notification/create": "\tc [nome] - cria uma sala gerenciada usando o nome da sala atual", "commandlist-notification/auth": "\ta [senha] - autentica-se como operador da sala com a senha", "commandlist-notification/chat": "\tch [mensagem] - envia uma mensagem no chat da sala", - "commandList-notification/queue": "\tqa [file/url] - adiciona arquivo ou URL para o final da playlist", - "commandList-notification/queueandselect": "\tqas [file/url] - add file or url to bottom of playlist and select it", # TO DO: Translate + "commandList-notification/queue": "\tqa [arquivo/url] - adiciona um arquivo ou URL para o final da playlist", + "commandList-notification/queueandselect": "\tqas [arquivo/url] - adiciona um arquivo ou URL para o final da playlist e o seleciona", "commandList-notification/playlist": "\tql - mostra a playlist atual", - "commandList-notification/select": "\tqs [index] - seleciona uma dada entrada na playlist", - "commandList-notification/delete": "\tqd [index] - deleta uma dada entrada na playlist", + "commandList-notification/select": "\tqs [índice] - seleciona um item da playlist", + "commandList-notification/delete": "\tqd [index] - remove um item da playlist", "syncplay-version-notification": "Versão do Syncplay: {}", # syncplay.version "more-info-notification": "Mais informações disponíveis em: {}", # projectURL "gui-data-cleared-notification": "O Syncplay limpou o caminho e o estado de dados da janela usados pela GUI.", "language-changed-msgbox-label": "O idioma será alterado quando você salvar as mudanças e abrir o Syncplay novamente.", - "promptforupdate-label": "O Syncplay pode verificar automaticamente por atualizações de tempos em tempos?", + "promptforupdate-label": "Permite o Syncplay verificar automaticamente por atualizações de tempos em tempos?", "media-player-latency-warning": "Aviso: O reprodutor de mídia demorou {} para responder. Se você tiver problemas de sincronização, feche outros programas para liberar recursos do sistema e, se isso não funcionar, tente outro reprodutor de mídia.", # Seconds to respond "mpv-unresponsive-error": "O mpv não respondeu por {} segundos, portanto parece que não está funcionando. Por favor, reinicie o Syncplay.", # Seconds to respond @@ -117,29 +117,29 @@ pt_BR = { "mpv-failed-advice": "O motivo pelo qual o mpv não pode ser iniciado pode ser devido ao uso de argumentos da linha de comando não suportados ou a uma versão não suportada do mpv.", "player-file-open-error": "O reprodutor falhou ao abrir o arquivo", "player-path-error": "O caminho até o arquivo executável do reprodutor não está configurado corretamente. Os reprodutores suportados são: mpv, mpv.net, VLC, MPC-HC, MPC-BE, mplayer2 e IINA", - "hostname-empty-error": "O endereço do servidor não pode ser vazio", - "empty-error": "{} não pode ser vazio", # Configuration + "hostname-empty-error": "O endereço do servidor não pode estar vazio", + "empty-error": "{} não pode estar vazio", # Configuration "media-player-error": "Erro do reprodutor de mídia: \"{}\"", # Error line "unable-import-gui-error": "Não foi possível importar bibliotecas da GUI. Se você não possuir o PySide instalado, instale-o para que a GUI funcione.", "unable-import-twisted-error": "Não foi possível importar o Twisted. Por favor, instale o Twisted v16.4.0 ou superior.", "arguments-missing-error": "Alguns argumentos necessários estão faltando, por favor reveja --help", - "unable-to-start-client-error": "Não foi possível iniciar o client", + "unable-to-start-client-error": "Não foi possível iniciar o cliente", "player-path-config-error": "O caminho até o arquivo executável do reprodutor não está configurado corretamente. Os reprodutores suportados são: mpv, mpv.net, VLC, MPC-HC, MPC-BE, mplayer2 e IINA.", "no-file-path-config-error": "O arquivo deve ser selecionado antes de iniciar seu reprodutor", - "no-hostname-config-error": "O endereço do servidor não pode ser vazio", + "no-hostname-config-error": "O endereço do servidor não pode estar vazio", "invalid-port-config-error": "A porta deve ser válida", - "empty-value-config-error": "{} não pode ser vazio", # Config option + "empty-value-config-error": "{} não pode estar vazio", # Config option "not-json-error": "Não é uma string codificada como JSON\n", "hello-arguments-error": "Not enough Hello arguments\n", # DO NOT TRANSLATE "version-mismatch-error": "Discrepância entre versões do client e do servidor\n", - "vlc-failed-connection": "Falha ao conectar ao VLC. Se você não instalou o syncplay.lua e está usando a versão mais recente do VLC, por favor veja https://syncplay.pl/LUA/ para mais instruções. Syncplay e VLC 4 são atualmente incompatíveis, portanto ou use VLC 3 ou use outro reprodutor, como o mpv.", + "vlc-failed-connection": "Falha ao conectar ao VLC. Se você não instalou o syncplay.lua e está usando a versão mais recente do VLC, por favor veja https://syncplay.pl/LUA/ para mais instruções. Syncplay e VLC 4 são atualmente incompatíveis, portanto use VLC 3 ou use outro reprodutor, como o mpv.", "vlc-failed-noscript": "O VLC reportou que a interface de script do syncplay.lua não foi instalada. Por favor, veja https://syncplay.pl/LUA/ para mais instruções.", "vlc-failed-versioncheck": "Esta versão do VLC não é suportada pelo Syncplay.", - "vlc-initial-warning": 'O VLC nem sempre fornece informações precisas de posição para o Syncplay, especialmente para arquivos .mp4 ou .avi. Se você experienciar problemas com busca (seeking) incorreta, por favor, tente um reprodutor de mídia alternativo, como o mpv (ou mpv.net, uma alternativa mais simples do mpv a usuários Windows).', + "vlc-initial-warning": 'O VLC nem sempre fornece informações precisas de posição para o Syncplay, especialmente para arquivos .mp4 ou .avi. Se você experienciar problemas com busca (seeking) incorreta, por favor, tente um reprodutor de mídia alternativo, como o mpv (ou mpv.net, uma alternativa mais simples do mpv a usuários de Windows).', "feature-sharedPlaylists": "playlists compartilhadas", # used for not-supported-by-server-error "feature-chat": "chat", # used for not-supported-by-server-error @@ -210,7 +210,7 @@ pt_BR = { "filename-privacy-label": "Informação do nome do arquivo:", "filesize-privacy-label": "Informação do tamanho do arquivo:", "checkforupdatesautomatically-label": "Verificar atualizações do Syncplay automaticamente", - "autosavejoinstolist-label": "Adicionar salas que você entra para a lista de salas", + "autosavejoinstolist-label": "Adicionar salas que você visitou para a sua lista de salas", "slowondesync-label": "Diminuir velocidade em dessincronizações menores (não suportado pelo MPC-HC/BE)", "rewindondesync-label": "Retroceder em dessincronização maiores (recomendado)", "fastforwardondesync-label": "Avançar se estiver ficando para trás (recomendado)", @@ -231,13 +231,13 @@ pt_BR = { "automatic-language": "Padrão ({})", # Default language "showdurationnotification-label": "Avisar sobre discrepância nas durações dos arquivos de mídia", "basics-label": "Básicos", - "readiness-label": "Play/Pause", + "readiness-label": "Tocar/Pausar", "misc-label": "Miscelânea", - "core-behaviour-title": "Comportamento da sala padrão", + "core-behaviour-title": "Comportamento principal da sala", "syncplay-internals-title": "Configurações internas do Syncplay", "syncplay-mediasearchdirectories-title": "Diretórios a buscar por mídias", "syncplay-mediasearchdirectories-label": "Diretórios a buscar por mídias (um caminho por linha)", - "sync-label": "Sincronizar", + "sync-label": "Sincronização", "sync-otherslagging-title": "Se outros estiverem ficando pra trás...", "sync-youlaggging-title": "Se você estiver ficando pra trás...", "messages-label": "Mensagens", @@ -253,21 +253,21 @@ pt_BR = { "unpause-always": "Sempre despausar", "syncplay-trusteddomains-title": "Domínios confiáveis (para serviços de streaming e conteúdo hospedado)", - "chat-title": "Entrada de mensagem do chat", - "chatinputenabled-label": "Habilitar entrada de chat via mpv", - "chatdirectinput-label": "Permitir entrada instantânea de chat (evita ter de apertar Enter para abrir o chat)", - "chatinputfont-label": "Fonte da entrada de chat", + "chat-title": "Digitação de mensagem no chat", + "chatinputenabled-label": "Habilitar digitação no chat via mpv", + "chatdirectinput-label": "Permitir digitação instantânea no chat (evita ter de apertar Enter para abrir o chat)", + "chatinputfont-label": "Fonte da digitação no chat", "chatfont-label": "Definir fonte", "chatcolour-label": "Definir cor", - "chatinputposition-label": "Posição da área de entrada de mensagens no mpv", + "chatinputposition-label": "Posição da área de digitação de mensagens no mpv", "chat-top-option": "Topo", "chat-middle-option": "Meio", - "chat-bottom-option": "Fundo", + "chat-bottom-option": "Baixo", "chatoutputheader-label": "Saída de mensagem do chat", "chatoutputfont-label": "Fonte da saída de chat", - "chatoutputenabled-label": "Habilitar saída de chat no reprodutor de mídia (apenas mpv por enquanto)", + "chatoutputenabled-label": "Habilitar saída de chat no reprodutor de mídia (apenas no mpv por enquanto)", "chatoutputposition-label": "Modo de saída", - "chat-chatroom-option": "Estilo sala de bate-papo", + "chat-chatroom-option": "Estilo da sala de bate-papo", "chat-scrolling-option": "Estilo de rolagem", "mpv-key-tab-hint": "[TAB] para alternar acesso instantâneo ao chat.", @@ -280,19 +280,19 @@ pt_BR = { "run-label": "Começar Syncplay", "storeandrun-label": "Salvar mudanças e começar Syncplay", - "contact-label": "Sinta-se livre para mandar um e-mail para dev@syncplay.pl, abrir uma issue pelo GitHub / start a discussion to make a suggestion or ask a question via GitHub,, curtir nossa página no Facebook, nos seguir no Twitter ou visitar https://syncplay.pl/. Não use o Syncplay para mandar informações sensíveis/confidenciais.", # TODO: Update translation - + "contact-label": "Sinta-se livre para mandar um e-mail para dev@syncplay.pl, abrir uma issue para reportar um bug/problema via GitHub, começar uma discussão para fazer uma sugestão ou uma pergunta via GitHub, curtir nossa página no Facebook, nos seguir no Twitter, ou visitar https://syncplay.pl/. Não use o Syncplay para mandar informações sensíveis/confidenciais.", + "joinroom-label": "Juntar-se a uma sala", "joinroom-menu-label": "Juntar-se à sala {}", "seektime-menu-label": "Saltar para o tempo", "undoseek-menu-label": "Desfazer salto", - "play-menu-label": "Play", - "pause-menu-label": "Pause", + "play-menu-label": "Tocar", + "pause-menu-label": "Pausar", "playbackbuttons-menu-label": "Mostrar botões de reprodução", "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 + "hideemptyrooms-menu-label": "Esconder salas persistentes vazias", "sendmessage-label": "Enviar", @@ -360,7 +360,7 @@ pt_BR = { "offsetinfo-msgbox-label": "Deslocamento (veja https://syncplay.pl/guide/ para instruções de uso):", "promptforstreamurl-msgbox-label": "Abrir transmissão de mídia via URL", - "promptforstreamurlinfo-msgbox-label": "Transmitir URL", + "promptforstreamurlinfo-msgbox-label": "URL da transmissão", "addfolder-label": "Adicionar pasta", @@ -438,7 +438,7 @@ pt_BR = { "chatoutputmode-chatroom-tooltip": "Exibe novas linhas de chat diretamente abaixo da linha anterior.", "chatoutputmode-scrolling-tooltip": "Exibe novas linhas de chat rolando-as da direita pra esquerda.", - "help-tooltip": "Abre o guia de usuário do Syncplay.pl.", + "help-tooltip": "Abre o guia de usuário no Syncplay.pl.", "reset-tooltip": "Redefine todas as configurações para seus respectivos padrões.", "update-server-list-tooltip": "Conecta ao syncplay.pl para atualizar a lista de servidores públicos.", @@ -459,7 +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 + "persistent-rooms-notice": "AVISO: Este servidor usa salas persistentes, o que significa que as informações da playlist são salvas entre sessões de reprodução. Se você quer criar uma sala onde a informação não será salva, coloque -temp no final do nome da sala.", # NOTE: Do not translate the word -temp # Server notifications "welcome-server-notification": "Seja bem-vindo ao servidor de Syncplay, versão {0}", # version @@ -469,7 +469,7 @@ pt_BR = { # Server arguments - "server-argument-description": 'Solução para sincronizar a reprodução de múltiplas instâncias de MPlayer e MPC-HC/BE pela rede. Instância de servidor', + "server-argument-description": 'Solução para sincronizar a reprodução de múltiplas instâncias de players de mídia pela rede. Instância de servidor', "server-argument-epilog": 'Se nenhuma opção for fornecida, os valores de _config serão utilizados', "server-port-argument": 'porta TCP do servidor', "server-password-argument": 'senha do servidor', @@ -477,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 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-rooms-argument": "caminho para o arquivo de banco de dádos a ser usado e/ou criado para salvar dados de salas persistentes. Permite que salas persistam sem espectadores e através de reinicializações.", + "server-permanent-rooms-argument": "caminho para o arquivo que lista salas permanentes que serão listadas mesmo que a sala esteja vazia (na forma de um arquivo de texto que lista uma sala por linha) - requer salas persistentes ativada", "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 é {})", @@ -492,7 +492,7 @@ pt_BR = { "not-json-server-error": "Não é uma string codificada como json: {}", # message "line-decode-server-error": "Não é uma string UTF-8", "not-known-server-error": "Você deve ser conhecido pelo servidor antes de mandar este comando", - "client-drop-server-error": "Drop do client: {} -- {}", # host, error + "client-drop-server-error": "Cliente desconectado: {} -- {}", # host, error "password-required-server-error": "Senha necessária", "wrong-password-server-error": "Senha incorreta fornecida", "hello-server-error": "Not enough Hello arguments", # DO NOT TRANSLATE From be854973aae73db962de2a2d262894fdf3c808e0 Mon Sep 17 00:00:00 2001 From: lryanner <73189549+lryanner@users.noreply.github.com> Date: Sat, 12 Feb 2022 21:44:31 +0800 Subject: [PATCH 27/30] Added Simplified Chinese Translation. (#500) * Add Chinese Translation --- buildPy2exe.py | 17 ++ syncplay/messages.py | 2 + syncplay/messages_de.py | 2 +- syncplay/messages_en.py | 2 +- syncplay/messages_es.py | 2 +- syncplay/messages_fr.py | 2 +- syncplay/messages_it.py | 2 +- syncplay/messages_pt_BR.py | 2 +- syncplay/messages_pt_PT.py | 2 +- syncplay/messages_ru.py | 2 +- syncplay/messages_tr.py | 2 +- syncplay/messages_zh_CN.py | 528 +++++++++++++++++++++++++++++++++++++ 12 files changed, 556 insertions(+), 9 deletions(-) create mode 100644 syncplay/messages_zh_CN.py diff --git a/buildPy2exe.py b/buildPy2exe.py index 1ac3880..cb80e66 100755 --- a/buildPy2exe.py +++ b/buildPy2exe.py @@ -67,6 +67,7 @@ NSIS_SCRIPT_TEMPLATE = r""" LoadLanguageFile "$${NSISDIR}\Contrib\Language files\Portuguese.nlf" LoadLanguageFile "$${NSISDIR}\Contrib\Language files\Turkish.nlf" LoadLanguageFile "$${NSISDIR}\Contrib\Language files\French.nlf" + LoadLanguageFile "$${NSISDIR}\Contrib\Language files\SimpChinese.nlf" Unicode true @@ -124,6 +125,11 @@ NSIS_SCRIPT_TEMPLATE = r""" VIAddVersionKey /LANG=$${LANG_FRENCH} "FileVersion" "$version.0" VIAddVersionKey /LANG=$${LANG_FRENCH} "LegalCopyright" "Syncplay" VIAddVersionKey /LANG=$${LANG_FRENCH} "FileDescription" "Syncplay" + + VIAddVersionKey /LANG=$${LANG_SIMPCHINESE} "ProductName" "Syncplay" + VIAddVersionKey /LANG=$${LANG_SIMPCHINESE} "FileVersion" "$version.0" + VIAddVersionKey /LANG=$${LANG_SIMPCHINESE} "LegalCopyright" "Syncplay" + VIAddVersionKey /LANG=$${LANG_SIMPCHINESE} "FileDescription" "Syncplay" LangString ^SyncplayLanguage $${LANG_ENGLISH} "en" LangString ^Associate $${LANG_ENGLISH} "Associate Syncplay with multimedia files." @@ -213,6 +219,15 @@ NSIS_SCRIPT_TEMPLATE = r""" LangString ^QuickLaunchBar $${LANG_FRENCH} "Barre de Lancement Rapide" LangString ^AutomaticUpdates $${LANG_FRENCH} "Vérifier automatiquement les mises à jour" LangString ^UninstConfig $${LANG_FRENCH} "Supprimer le fichier de configuration." + + LangString ^SyncplayLanguage $${LANG_SIMPCHINESE} "zh_CN" + LangString ^Associate $${LANG_SIMPCHINESE} "将Syncplay与多媒体文件关联。" + LangString ^Shortcut $${LANG_SIMPCHINESE} "在以下位置创建快捷方式:" + LangString ^StartMenu $${LANG_SIMPCHINESE} "开始菜单" + LangString ^Desktop $${LANG_SIMPCHINESE} "桌面" + LangString ^QuickLaunchBar $${LANG_SIMPCHINESE} "快速启动栏" + LangString ^AutomaticUpdates $${LANG_SIMPCHINESE} "自动检查更新" + LangString ^UninstConfig $${LANG_SIMPCHINESE} "删除配置文件" ; Remove text to save space LangString ^ClickInstall $${LANG_GERMAN} " " @@ -334,6 +349,8 @@ NSIS_SCRIPT_TEMPLATE = r""" Push 'Português de Portugal' Push $${LANG_TURKISH} Push 'Türkçe' + Push $${LANG_SIMPCHINESE} + Push '简体中文' Push A ; A means auto count languages LangDLL::LangDialog "Language Selection" "Please select the language of Syncplay and the installer" Pop $$LANGUAGE diff --git a/syncplay/messages.py b/syncplay/messages.py index 0b41ffc..2ae303a 100755 --- a/syncplay/messages.py +++ b/syncplay/messages.py @@ -10,6 +10,7 @@ from . import messages_pt_BR from . import messages_pt_PT from . import messages_tr from . import messages_fr +from . import messages_zh_CN import re messages = { @@ -22,6 +23,7 @@ messages = { "pt_BR": messages_pt_BR.pt_BR, "tr": messages_tr.tr, "ru": messages_ru.ru, + "zh_CN": messages_zh_CN.zh_CN, "CURRENT": None } diff --git a/syncplay/messages_de.py b/syncplay/messages_de.py index 0f7943e..79e1388 100755 --- a/syncplay/messages_de.py +++ b/syncplay/messages_de.py @@ -176,7 +176,7 @@ de = { "file-argument": 'Abzuspielende Datei', "args-argument": 'Player-Einstellungen; Wenn du Einstellungen, die mit - beginnen, nutzen willst, stelle ein einzelnes \'--\'-Argument davor', "clear-gui-data-argument": 'Setzt die Pfad- und GUI-Fenster-Daten die in den QSettings gespeichert sind zurück', - "language-argument": 'Sprache für Syncplay-Nachrichten (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', + "language-argument": 'Sprache für Syncplay-Nachrichten (de/en/ru/it/es/pt_BR/pt_PT/tr/fr/zh_CN)', "version-argument": 'gibt die aktuelle Version aus', "version-message": "Du verwendest Syncplay v. {} ({})", diff --git a/syncplay/messages_en.py b/syncplay/messages_en.py index 0de7348..97491fe 100644 --- a/syncplay/messages_en.py +++ b/syncplay/messages_en.py @@ -176,7 +176,7 @@ en = { "file-argument": 'file to play', "args-argument": 'player options, if you need to pass options starting with - prepend them with single \'--\' argument', "clear-gui-data-argument": 'resets path and window state GUI data stored as QSettings', - "language-argument": 'language for Syncplay messages (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', + "language-argument": 'language for Syncplay messages (de/en/ru/it/es/pt_BR/pt_PT/tr/fr/zh_CN)', "version-argument": 'prints your version', "version-message": "You're using Syncplay version {} ({})", diff --git a/syncplay/messages_es.py b/syncplay/messages_es.py index 3259b1d..5e86762 100644 --- a/syncplay/messages_es.py +++ b/syncplay/messages_es.py @@ -176,7 +176,7 @@ es = { "file-argument": 'archivo a reproducir', "args-argument": 'opciones del reproductor, si necesitas pasar opciones que empiezan con -, pásalas utilizando \'--\'', "clear-gui-data-argument": 'restablece ruta y los datos del estado de la ventana GUI almacenados como QSettings', - "language-argument": 'lenguaje para los mensajes de Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', + "language-argument": 'lenguaje para los mensajes de Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr/zh_CN)', "version-argument": 'imprime tu versión', "version-message": "Estás usando la versión de Syncplay {} ({})", diff --git a/syncplay/messages_fr.py b/syncplay/messages_fr.py index 14d38d8..f5c88a9 100644 --- a/syncplay/messages_fr.py +++ b/syncplay/messages_fr.py @@ -175,7 +175,7 @@ fr = { "file-argument": "fichier à lire", "args-argument": 'player options, if you need to pass options starting with - prepend them with single \'--\' argument', "clear-gui-data-argument": "réinitialise les données GUI du chemin et de l'état de la fenêtre stockées en tant que QSettings", - "language-argument": "langue pour les messages Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)", + "language-argument": "langue pour les messages Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr/zh_CN)", "version-argument": "imprime votre version", "version-message": "Vous utilisez Syncplay version {} ({})", diff --git a/syncplay/messages_it.py b/syncplay/messages_it.py index ede6eff..9e80cb4 100755 --- a/syncplay/messages_it.py +++ b/syncplay/messages_it.py @@ -176,7 +176,7 @@ it = { "file-argument": 'file da riprodurre', "args-argument": 'opzioni del player, se hai bisogno di utilizzare opzioni che iniziano con - anteponi un singolo \'--\'', "clear-gui-data-argument": 'ripristina il percorso e i dati impostati tramite interfaccia grafica e salvati come QSettings', - "language-argument": 'lingua per i messaggi di Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', + "language-argument": 'lingua per i messaggi di Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr/zh_CN)', "version-argument": 'mostra la tua versione', "version-message": "Stai usando la versione di Syncplay {} ({})", diff --git a/syncplay/messages_pt_BR.py b/syncplay/messages_pt_BR.py index 3c2339f..96f8b97 100644 --- a/syncplay/messages_pt_BR.py +++ b/syncplay/messages_pt_BR.py @@ -177,7 +177,7 @@ pt_BR = { "file-argument": 'arquivo a ser tocado', "args-argument": 'opções do reprodutor; se você precisar passar opções começando com -, as preceda com um único argumento \'--\'', "clear-gui-data-argument": 'redefine o caminho e o estado de dados da janela da GUI para as de QSettings', - "language-argument": 'idioma para mensagens do Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', + "language-argument": 'idioma para mensagens do Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr/zh_CN)', "version-argument": 'exibe sua versão', "version-message": "Você está usando o Syncplay versão {} ({})", diff --git a/syncplay/messages_pt_PT.py b/syncplay/messages_pt_PT.py index 9ba0918..d09a65e 100644 --- a/syncplay/messages_pt_PT.py +++ b/syncplay/messages_pt_PT.py @@ -176,7 +176,7 @@ pt_PT = { "file-argument": 'ficheiro a ser tocado', "args-argument": 'opções do reprodutor; se você precisar passar opções começando com -, as preceda com um único argumento \'--\'', "clear-gui-data-argument": 'redefine o caminho e o estado de dados da janela da GUI para as de QSettings', - "language-argument": 'idioma para mensagens do Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', + "language-argument": 'idioma para mensagens do Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr/zh_CN)', "version-argument": 'exibe sua versão', "version-message": "Você está usando o Syncplay versão {} ({})", diff --git a/syncplay/messages_ru.py b/syncplay/messages_ru.py index 6d71294..cb1302e 100755 --- a/syncplay/messages_ru.py +++ b/syncplay/messages_ru.py @@ -175,7 +175,7 @@ ru = { "file-argument": 'воспроизводимый файл', "args-argument": 'параметры проигрывателя; если нужно передать параметры, начинающиеся с - , то сначала пишите \'--\'', "clear-gui-data-argument": 'сбрасывает путь и данные о состоянии окна GUI, хранимые как QSettings', - "language-argument": 'язык сообщений Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', + "language-argument": 'язык сообщений Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr/zh_CN)', "version-argument": 'выводит номер версии', "version-message": "Вы используете Syncplay версии {} ({})", diff --git a/syncplay/messages_tr.py b/syncplay/messages_tr.py index f518166..b4f6cd9 100644 --- a/syncplay/messages_tr.py +++ b/syncplay/messages_tr.py @@ -176,7 +176,7 @@ tr = { "file-argument": 'oynatmak için dosya', "args-argument": 'oynatıcı seçenekleri, ile başlayan seçenekleri iletmeniz gerekiyorsa - bunların başına tek \'--\' argümanı ekleyin', "clear-gui-data-argument": 'QSettings olarak depolanan yol ve pencere durumu GUI verilerini sıfırlar', - "language-argument": 'Syncplay mesajları için dil (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)', + "language-argument": 'Syncplay mesajları için dil (de/en/ru/it/es/pt_BR/pt_PT/tr/fr/zh_CN)', "version-argument": 'versiyonunuzu yazdırır', "version-message": "Syncplay sürümünü kullanıyorsunuz {} ({})", diff --git a/syncplay/messages_zh_CN.py b/syncplay/messages_zh_CN.py new file mode 100644 index 0000000..19b4ab1 --- /dev/null +++ b/syncplay/messages_zh_CN.py @@ -0,0 +1,528 @@ +# coding:utf8 + +"""Simplified Chinese dictionary""" + +zh_CN = { + "LANGUAGE": "简体中文", + + # Client notifications + "config-cleared-notification": "设置已清除。 当你存储一个有效的配置时,更改将被保存。", + + "relative-config-notification": "已加载的相对配置文件:{}", + + "connection-attempt-notification": "正在尝试连接{}:{}", # Port, IP + "reconnection-attempt-notification": "服务器连接中断,正在尝试重连", + "disconnection-notification": "与服务器连接中断", + "connection-failed-notification": "服务器连接失败", + "connected-successful-notification": "服务器连接成功", + "retrying-notification": "%s,%d秒内将重试...", # Seconds + "reachout-successful-notification": "成功连入{}({})", + + "rewind-notification": "与{}不同步,已回放", # User + "fastforward-notification": "与{}不同步,已快进", # User + "slowdown-notification": "与{}不同步,正在慢速播放", # User + "revert-notification": "播放速度恢复一倍速", + + "pause-notification": "{}暂停了", # User + "unpause-notification": "{}重新开始播放", # User + "seek-notification": "{}从{}跳转到{}", # User, from time, to time + + "current-offset-notification": "当前偏移量:{}秒", # Offset + + "media-directory-list-updated-notification": "Syncplay的媒体文件目录已更新。", + + "room-join-notification": "{}加入了'{}'房间", # User + "left-notification": "{}离开了房间", # User + "left-paused-notification": "{}离开了房间,{}暂停了", # User who left, User who paused + "playing-notification": "{}正在播放'{}' (时长{})", # User, file, duration + "playing-notification/room-addendum": "正在'{}'房间", # Room + + "not-all-ready": "{}还未准备", # Usernames + "all-users-ready": "所有人已准备(共{}人)", # Number of ready users + "ready-to-unpause-notification": "您已准备,按下播放键可继续播放", + "set-as-ready-notification": "您已准备", + "set-as-not-ready-notification": "您已取消准备", + "autoplaying-notification": "{}秒后将自动开始...", # Number of seconds until playback will start + + "identifying-as-controller-notification": "用密码'{}'认证为管理员...", + "failed-to-identify-as-controller-notification": "{}未能成为管理员", + "authenticated-as-controller-notification": "{}成为了管理员", + "created-controlled-room-notification": "已创建房间:'{}' ,密码是'{}'。请保存好以上信息以备用!\n\n只有房主或管理员可以暂停,继续播放,跳转播放,改变播放列表。每个人都与房主或管理员的播放进度同步。\n\n最近观看过的人都可加入'{}'房间,但只有房主或管理员在加入'{}'房间时才会自动认证为管理员。", # RoomName, operatorPassword, roomName, roomName:operatorPassword + + "file-different-notification": "您正在播放的视频与{}不一致", # User + "file-differences-notification": "你们播放的视频文件{}不一致", # Differences + "room-file-differences": "播放文件不同点:{}", # File differences (filename, size, and/or duration) + "file-difference-filename": "名称", + "file-difference-filesize": "大小", + "file-difference-duration": "时长", + "alone-in-the-room": "您正在一人观看", + + "different-filesize-notification": "(其他人的视频文件大小和您的不一致!)", + "userlist-playing-notification": "{}正在播放:", # Username + "file-played-by-notification": "{}由以下用户播放:", # File + "no-file-played-notification": "{}无视频供开始播放", # Username + "notplaying-notification": "以下用户没有视频供开始播放:", + "userlist-room-notification": "房间'{}'里面有:", # Room + "userlist-file-notification": "视频文件", + "controller-userlist-userflag": "管理员", + "ready-userlist-userflag": "已准备", + + "update-check-failed-notification": "无法自动检测Syncplay{}版本是否最新。是否前往https://syncplay.pl/手动更新?", # Syncplay version + "syncplay-uptodate-notification": "Syncplay版本已最新", + "syncplay-updateavailable-notification": "Syncplay已有更新版本。是否前往最新版本页面?", + + "mplayer-file-required-notification": "如使用mplayer播放器,您需要在开始前手动加入视频文件。", + "mplayer-file-required-notification/example": "使用示例:syncplay [options] [url|path/]filename", + "mplayer2-required": "Syncplay与MPlayer 1.x版本不兼容,请使用mplayer2或mpv(推荐)播放器", + + "unrecognized-command-notification": "未知命令", + "commandlist-notification": "您可使用如下命令:", + "commandlist-notification/room": "\tr [name] - 切换房间", + "commandlist-notification/list": "\tl - 显示房间所有用户", + "commandlist-notification/undo": "\tu - 撤销上次跳转", + "commandlist-notification/pause": "\tp - 暂停/继续播放", + "commandlist-notification/seek": "\t[s][+-]time - 跳转指定时长,如果未输入 + 或 - ,将会跳转到输入的时间节点(时间节点输入格式可为秒或分:秒)", + "commandlist-notification/offset": "\to[+-]duration - 将本地的播放偏移量设置为给定时长(输入格式可为秒或分:秒),您的本地播放进度将与服务器不一致,因此极不推荐使用该设置", + "commandlist-notification/help": "\th - 帮助菜单", + "commandlist-notification/toggle": "\tt - 准备/取消准备", + "commandlist-notification/create": "\tc [name] - 以给定房间名创建一个由您管理的房间", + "commandlist-notification/auth": "\ta [password] - 用给定密码来认证为管理员", + "commandlist-notification/chat": "\tch [message] - 发送聊天信息", + "commandList-notification/queue": "\tqa [file/url] - 在播放列表最后添加文件或URL", + "commandList-notification/queueandselect": "\tqas [file/url] - 在播放列表最后添加文件或URL并选中", + "commandList-notification/playlist": "\tql - 显示当前播放列表", + "commandList-notification/select": "\tqs [index] - 选中播放列表中指定序号的视频", + "commandList-notification/delete": "\tqd [index] - 删除播放列表中指定序号的视频", + "syncplay-version-notification": "当前Syncplay版本为{}", # syncplay.version + "more-info-notification": "更多信息请参照{}", # projectURL + + "gui-data-cleared-notification": "Syncplay已经清除了GUI使用的路径和窗口状态数据。", + "language-changed-msgbox-label": "当您运行Syncplay时,界面将切换为中文", + "promptforupdate-label": "是否让Syncplay自动检查更新?", + + "media-player-latency-warning": "警告:播放器用了{}秒响应。如果您遇到播放不同步问题,请关闭其他应用以清理内存。如果仍无法解决问题,建议您更换其他播放器。", # Seconds to respond + "mpv-unresponsive-error": "mpv播放器在{}秒之内无反应,可能出现故障。请尝试重启Syncplay。", # Seconds to respond + + # Client prompts + "enter-to-exit-prompt": "Press enter to exit\n", + + # Client errors + "missing-arguments-error": "缺少一些必要的参数,使用--help命令查看详细信息", + "server-timeout-error": "与服务器的连接超时", + "mpc-slave-error": "无法在从属模式下启动MPC!", + "mpc-version-insufficient-error": "MPC版本过低,请使用mpc-hc{}以上版本", + "mpc-be-version-insufficient-error": "MPC版本过低,请使用mpc-be{}以上版本", + "mpv-version-error": "Syncplay与此版本的mpv不兼容。请使用其他版本的mpv(例如:Git HEAD)。", + "mpv-failed-advice": "mpv不能启动的原因可能是由于使用了不支持的命令行参数或不支持的mpv版本。", + "player-file-open-error": "播放器打开文件失败", + "player-path-error": "播放器路径设置不正确。支持的播放器有:mpv、mpv.net、VLC、MPC-HC、MPC-BE、mplayer2和IINA。", + "hostname-empty-error": "主机名不能是空的", + "empty-error": "{}不能是空的", # Configuration + "media-player-error": "媒体播放器错误:\"{}\"", # Error line + "unable-import-gui-error": "无法导入GUI库。如果你没有安装PySide,GUI则无法工作,请安装PySide。", + "unable-import-twisted-error": "无法导入Twisted。请安装Twisted v16.4.0或更高版本。", + + "arguments-missing-error": "缺少一些必要的参数,使用--help命令查看详细信息", + + "unable-to-start-client-error": "无法启动客户端", + + "player-path-config-error": "播放器路径设置不正确。支持的播放器有:mpv、mpv.net、VLC、MPC-HC、MPC-BE、mplayer2和IINA。", + "no-file-path-config-error": "在启动播放器之前,必须选择要播放的文件", + "no-hostname-config-error": "主机名不能是空的", + "invalid-port-config-error": "端口无效", + "empty-value-config-error": "{}不能是空的", # Config option + + "not-json-error": "不是一个json编码的字符串\n", + "hello-arguments-error": "Not enough Hello arguments\n", # DO NOT TRANSLATE + "version-mismatch-error": "客户端和服务器的版本不匹配\n", + "vlc-failed-connection": "连接VLC失败。如果你没有安装syncplay.lua并且使用最新版本的VLC,那么请参考https://syncplay.pl/LUA/。Syncplay和VLC 4目前并不兼容,所以请使用VLC 3或mpv等替代软件。", + "vlc-failed-noscript": "VLC报告没有安装syncplay.lua接口脚本。请参考https://syncplay.pl/LUA/获得指导。", + "vlc-failed-versioncheck": "Syncplay不支持这个版本的VLC。", + "vlc-initial-warning": 'VLC无法给Syncplay提供准确进度信息,尤其是在播放.mp4和.avi文件时。如果您遇到了跳转错误的问题,请使用其他播放器例如mpv(或者为Windows用户提供的mpv.net)。', + + "feature-sharedPlaylists": "共享播放列表", # used for not-supported-by-server-error + "feature-chat": "聊天", # used for not-supported-by-server-error + "feature-readiness": "准备状态", # used for not-supported-by-server-error + "feature-managedRooms": "房间管理", # used for not-supported-by-server-error + + "not-supported-by-server-error": "该服务器不支持{}功能", # feature + "shared-playlists-not-supported-by-server-error": "服务器可能不支持共享播放列表的功能。为确保其正常运行,需要一个运行Syncplay {}以上版本的服务器,但服务器是运行Syncplay {}的。", # minVersion, serverVersion + "shared-playlists-disabled-by-server-error": "在服务器配置中,共享播放列表功能已被禁用。要使用这一功能,你需要连接到其他服务器。", + + "invalid-seek-value": "无效的跳转值", + "invalid-offset-value": "无效的偏移值", + + "switch-file-not-found-error": "无法切换到视频文件'{0}'。Syncplay只在指定的媒体目录中查找文件。", # File not found + "folder-search-timeout-error": "在媒体目录中搜索媒体文件时,由于在'{}'中搜索时间过长而中止。如果你在要搜索的媒体文件夹列表中选择了一个子文件夹过多的文件夹,就会出现这种情况。为了使自动文件切换再次工作,请在菜单栏中选择\"文件->设置媒体目录\",并删除这个目录或用一个适当的子文件夹替换它。如果这个文件夹实际上是适当的,那么你可以通过选择\"文件->设置媒体目录\"并按\"确定\"来重新启用它。", # Folder + "folder-search-first-file-timeout-error": "搜索'{}'中的媒体文件时,由于访问该目录的时间太长而被中止。如果该目录位于网络驱动器,或者如果你配置了你的驱动器在一段时间不活动后停止工作,就会发生这种情况。为了能够自动切换播放文件,请进入\"文件->设置媒体目录\",并删除该目录或解决这个问题(还可尝试其他解决方式,例如,改变省电设置)。", # Folder + "added-file-not-in-media-directory-error": "你在'{}'中加载了一个文件,该媒体目录尚未保存。你可以通过选择菜单栏中的\"文件->设置媒体目录\"将其保存为指定媒体目录。", # Folder + "no-media-directories-error": "没有设置媒体目录。为了使共享播放列表和文件切换功能正常工作,请选择\"文件->设置媒体目录\",并指定Syncplay应该在哪里寻找媒体文件。", + "cannot-find-directory-error": "无法找到媒体目录'{}'。要更新您的媒体目录列表,请从菜单栏中选择\"文件->设置媒体目录\",并指定Syncplay应该在哪里寻找媒体文件。", + + "failed-to-load-server-list-error": "载入公共服务器列表失败。请在您的浏览器中访问https://www.syncplay.pl/以获得更多信息。", + + # Client arguments + "argument-description": '通过网络来同步多个媒体播放器的解决方案', + "argument-epilog": '如果没有提供可选值,将使用配置文件中的值', + "nogui-argument": '不显示GUI', + "host-argument": "服务器地址", + "name-argument": '用户名', + "debug-argument": '调试模式', + "force-gui-prompt-argument": '显示配置提示信息', + "no-store-argument": "不要将值保存在.syncplay", + "room-argument": '默认房间', + "password-argument": '服务器密码', + "player-path-argument": '播放器路径', + "file-argument": '待播放文件', + "args-argument": '播放器选项,如果你需要传递以-开头的选项,请在它们前面加上一个\'--\'开头', + "clear-gui-data-argument": '重置存储为QSettings的路径和窗口状态GUI数据。', + "language-argument": 'Syncplay消息的语言 (de/en/ru/it/es/pt_BR/pt_PT/tr/fr/zh_CN)', + + "version-argument": '显示版本信息', + "version-message": "您正在使用Syncplay version {} ({})", + + "load-playlist-from-file-argument": "从文本文件中加载播放列表(每一行代表一个文件)", + + + # Client labels + "config-window-title": "Syncplay设置", + + "connection-group-title": "连接设置", + "host-label": "服务器地址: ", + "name-label": "用户名(可选):", + "password-label": "服务器密码(若需要):", + "room-label": "默认房间: ", + "roomlist-msgbox-label": "编辑房间列表(每行一个房间)", + + "media-setting-title": "播放器设置", + "executable-path-label": "播放器路径:", + "media-path-label": "视频文件路径(可选):", + "player-arguments-label": "播放器参数(若需要):", + "browse-label": "选择...", + "update-server-list-label": "更新列表", + + "more-title": "高级设置", + "never-rewind-value": "从不", + "seconds-suffix": " 秒", + "privacy-sendraw-option": "原始数据", + "privacy-sendhashed-option": "哈希加密", + "privacy-dontsend-option": "不共享", + "filename-privacy-label": "是否共享文件名:", + "filesize-privacy-label": "是否共享文件大小:", + "checkforupdatesautomatically-label": "自动为我检查Syncplay更新信息", + "autosavejoinstolist-label": "将加入的房间添加到房间列表中", + "slowondesync-label": "进度过快时慢速播放(MPC-HC/BE播放器不支持)", + "rewindondesync-label": "进度过快时直接跳转(推荐)", + "fastforwardondesync-label": "进度过慢时直接跳转(推荐)", + "dontslowdownwithme-label": "进度过慢时不用让其他人与我同步(试验功能)", + "pausing-title": "暂停设置", + "pauseonleave-label": "当其他人离开时暂停(例如其他人掉线)", + "readiness-title": "初始准备状态", + "readyatstart-label": "自动将我设置为\"已准备\"状态", + "forceguiprompt-label": "不要总是打开Syncplay设置窗口", # (Inverted) + "showosd-label": "启用OSD信息", + + "showosdwarnings-label": "包括警告信息(例如,当文件不同时,用户没有准备好)", + "showsameroomosd-label": "包括在你的房间中的活动", + "shownoncontrollerosd-label": "包括在管理的房间中的非管理员的活动", + "showdifferentroomosd-label": "包括其他房间的活动", + "showslowdownosd-label": "包括慢速播放/恢复一倍速的通知", + "language-label": "语言设置", + "automatic-language": "默认({})", # Default language + "showdurationnotification-label": "时长不匹配时警告", + "basics-label": "基本设置", + "readiness-label": "播放/暂停设置", + "misc-label": "其他设置", + "core-behaviour-title": "主房间行为设置", + "syncplay-internals-title": "Syncplay内部设置", + "syncplay-mediasearchdirectories-title": "检索媒体文件的目录", + "syncplay-mediasearchdirectories-label": "检索媒体文件的目录(每行一个目录)", + "sync-label": "同步设置", + "sync-otherslagging-title": "如果您播放进度过快...", + "sync-youlaggging-title": "如果您播放进度过慢...", + "messages-label": "消息设置", + "messages-osd-title": "屏幕显示消息设置", + "messages-other-title": "其他显示消息设置", + "chat-label": "聊天设置", + "privacy-label": "隐私设置", # Currently unused, but will be brought back if more space is needed in Misc tab + "privacy-title": "隐私设定", + "unpause-title": "如果您点击播放按钮,将切换为已准备状态并且:", + "unpause-ifalreadyready-option": "继续播放,如果已准备", + "unpause-ifothersready-option": "继续播放,如果已准备或者房间中其他人已准备(默认)", + "unpause-ifminusersready-option": "继续播放,如果已准备或房间中其他人都已准备且准备人数达到最小开始人数", + "unpause-always": "总是继续播放", + "syncplay-trusteddomains-title": "受信任的网站(用于流媒体服务和托管内容)", + + "chat-title": "聊天信息输入设置", + "chatinputenabled-label": "允许通过mpv播放器进行聊天", + "chatdirectinput-label": "允许即时聊天输入(不需要按Enter键发送)", + "chatinputfont-label": "聊天输入字体设置", + "chatfont-label": "聊天输入字体", + "chatcolour-label": "设置字体颜色", + "chatinputposition-label": "mpv播放器中聊天输入窗口位置", + "chat-top-option": "顶部", + "chat-middle-option": "中间", + "chat-bottom-option": "底部", + "chatoutputheader-label": "聊天显示设置", + "chatoutputfont-label": "聊天显示字体", + "chatoutputenabled-label": "启用媒体播放器中的聊天显示(目前仅支持mpv播放器)", + "chatoutputposition-label": "现实模式", + "chat-chatroom-option": "聊天室模式", + "chat-scrolling-option": "弹幕模式", + + "mpv-key-tab-hint": "按下[TAB]来切换为字母快捷键模式。", + "mpv-key-hint": "按下[ENTER]来发送聊天信息。按下[ESC]来退出聊天模式。", + "alphakey-mode-warning-first-line": "你可以暂时使用旧版本mpv绑定的a-z键。", + "alphakey-mode-warning-second-line": "按[TAB]返回到Syncplay聊天模式。", + + "help-label": "帮助", + "reset-label": "恢复默认设置", + "run-label": "运行Syncplay", + "storeandrun-label": "保存设置并运行Syncplay", + + "contact-label": "欢迎发送邮件至 dev@syncplay.pl, 在GitHub上创建issue来反馈漏洞或问题,在GitHub上创建discussion来提出建议或问题, 在Facebook上喜欢我们, 在Twitter上关注我们,或者访问https://syncplay.pl/。请勿使用Syncplay发送敏感信息。", + + "joinroom-label": "加入房间", + "joinroom-menu-label": "加入房间 {}", + "seektime-menu-label": "跳转到", + "undoseek-menu-label": "撤销跳转", + "play-menu-label": "播放", + "pause-menu-label": "暂停", + "playbackbuttons-menu-label": "显示播放控制按钮", + "autoplay-menu-label": "显示自动播放按钮", + "autoplay-guipushbuttonlabel": "当所有人准备好时开始播放", + "autoplay-minimum-label": "最小开始人数:", + "hideemptyrooms-menu-label": "隐藏空房间", + + "sendmessage-label": "发送", + + "ready-guipushbuttonlabel": "我已经准备好了!", + + "roomuser-heading-label": "房间 / 用户", + "size-heading-label": "大小", + "duration-heading-label": "时长", + "filename-heading-label": "名称", + "notifications-heading-label": "消息通知", + "userlist-heading-label": "正在播放列表", + + "browseformedia-label": "浏览媒体文件", + + "file-menu-label": "&文件", # & precedes shortcut key + "openmedia-menu-label": "&打开媒体文件", + "openstreamurl-menu-label": "打开 &媒体流URL", + "setmediadirectories-menu-label": "设置媒体 &目录", + "loadplaylistfromfile-menu-label": "&从文件加载播放列表", + "saveplaylisttofile-menu-label": "&保存播放列表到文件", + "exit-menu-label": "退&出", + "advanced-menu-label": "&高级", + "window-menu-label": "&窗口", + "setoffset-menu-label": "设置&偏移量", + "createcontrolledroom-menu-label": "&创建管理的房间", + "identifyascontroller-menu-label": "&认证为管理员", + "settrusteddomains-menu-label": "设置&受信任的网站", + "addtrusteddomain-menu-label": "添加{}为受信任的网站", # Domain + + "edit-menu-label": "&编辑", + "cut-menu-label": "剪&切", + "copy-menu-label": "&复制", + "paste-menu-label": "&粘贴", + "selectall-menu-label": "&全选", + + "playback-menu-label": "&播放控制", + + "help-menu-label": "&帮助", + "userguide-menu-label": "打开用户&指南", + "update-menu-label": "检查&更新", + + "startTLS-initiated": "尝试安全连接", + "startTLS-secure-connection-ok": "安全连接已建立({})", + "startTLS-server-certificate-invalid": '安全连接建立失败。该服务器使用了无效的证书。这次通信可能被第三方截获。更多详情和故障排除请参见这里。', + "startTLS-server-certificate-invalid-DNS-ID": "Syncplay不信任这个服务器,因为它使用的是对其主机名无效的证书。", + "startTLS-not-supported-client": "客户端不支持TLS加密安全连接", + "startTLS-not-supported-server": "服务器不支持TLS加密安全连接", + + # TLS certificate dialog + "tls-information-title": "证书详情", + "tls-dialog-status-label": "Syncplay正使用加密的连接到{}.", + "tls-dialog-desc-label": "用数字证书进行加密,使信息在发送到或来自
服务器{}时保持隐私。", + "tls-dialog-connection-label": "使用Transport Layer Security (TLS)对信息进行加密。版本为{},
加密套件{}。", + "tls-dialog-certificate-label": "由{}颁发的证书有效期至{}。", + + # About dialog + "about-menu-label": "&关于Syncplay", + "about-dialog-title": "关于Syncplay", + "about-dialog-release": "Version {} release {}", + "about-dialog-license-text": "Licensed under the Apache License, Version 2.0", + "about-dialog-license-button": "License", + "about-dialog-dependencies": "Dependencies", + + "setoffset-msgbox-label": "设置偏移量", + "offsetinfo-msgbox-label": "偏移量(使用指南参见https://syncplay.pl/guide/):", + + "promptforstreamurl-msgbox-label": "打开媒体流URL", + "promptforstreamurlinfo-msgbox-label": "媒体流URL", + + "addfolder-label": "添加文件夹", + + "adduris-msgbox-label": "添加URL到播放列表(每行一个)", + "editplaylist-msgbox-label": "设置播放列表(每行一个)", + "trusteddomains-msgbox-label": "可以自动切换到的网站(每行一个)", + + "createcontrolledroom-msgbox-label": "创建管理的房间", + "controlledroominfo-msgbox-label": "输入房间名称\r\n(使用指南参见https://syncplay.pl/guide/):", + + "identifyascontroller-msgbox-label": "认证为房间管理员", + "identifyinfo-msgbox-label": "输入房间密码\r\n(使用指南参见https://syncplay.pl/guide/):", + + "public-server-msgbox-label": "为当前会话选择公共服务器", + + "megabyte-suffix": " MB", + + # Tooltips + + "host-tooltip": "要连接的主机名或IP,可包括端口(例如:syncplay.pl:8999)。只能与同一服务器/端口上的人进行同步播放。", + "name-tooltip": "您的昵称。无需注册,之后可以任意改变。如果没有指定,则生成随机昵称。", + "password-tooltip": "只有在连接到私人服务器时才需要密码。", + "room-tooltip": "连接后加入的房间,可以是任意名称,但你只能与同一房间的人进行同步播放。", + + "edit-rooms-tooltip": "编辑房间列表。", + + "executable-path-tooltip": "您选择的播放器的位置(目前仅支持mpv、mpv.net、VLC、MPC-HC/BE、mplayer2或IINA)。", + "media-path-tooltip": "要打开的视频或媒体流的位置。如果您使用mplayer2,该项为必填项。", + "player-arguments-tooltip": "额外的命令行参数/开关,以传递给该媒体播放器。", + "mediasearcdirectories-arguments-tooltip": "Syncplay将搜索媒体文件的目录,例如,当你使用点击切换功能时。Syncplay将通过子文件夹进行递归搜索。", + + "more-tooltip": "显示不常用的设置。", + "filename-privacy-tooltip": "向服务器发送当前播放文件名的隐私模式。", + "filesize-privacy-tooltip": "向服务器发送当前播放文件的大小的隐私模式。", + "privacy-sendraw-tooltip": "发送该信息而不进行混淆处理。这是大多数功能的默认选项。", + "privacy-sendhashed-tooltip": "发送信息的哈希版本,使其对其他客户不太可见。", + "privacy-dontsend-tooltip": "不要向服务器发送这些信息。这提供了最大的隐私保护。", + "checkforupdatesautomatically-tooltip": "定期到Syncplay网站查看是否有新版本的Syncplay。", + "autosavejoinstolist-tooltip": "当你加入一个服务器中的房间时,在要加入的房间列表中自动记住房间名称。", + "slowondesync-tooltip": "在需要时暂时降低播放速率,使您与其他观众同步。在MPC-HC/BE上不支持。", + "dontslowdownwithme-tooltip": "意味着如果你的播放滞后,其他人不会被减慢或倒退。这对房间管理员很有用。", + "pauseonleave-tooltip": "如果你被断开连接或有人从你的房间离开,则暂停播放。", + "readyatstart-tooltip": "在开始时将自己设定为\"准备好了\"(否则你将被设定为\"没有准备好\",直到你改变你的准备状态。)", + "forceguiprompt-tooltip": "当用Syncplay打开一个文件时,不显示设置窗口。", # (Inverted) + "nostore-tooltip": "用给定的配置运行Syncplay,但不永久存储这些变化。", # (Inverted) + "rewindondesync-tooltip": "为了达到同步在需要时跳转当前播放进度。禁用这个选项可能会导致严重的不同步现象!", + "fastforwardondesync-tooltip": "当与房间管理员不同步时,会向前跳转播放进度(如果 \"进度过慢时不用让其他人与我同步\"被启用,则是跳转到你应当的进度)。", + "showosd-tooltip": "向媒体播放器OSD发送Syncplay信息。", + "showosdwarnings-tooltip": "如果出现播放不同的文件,独自在房间里,用户没有准备好等情况,则显示警告。", + "showsameroomosd-tooltip": "显示与用户所在房间有关的活动的OSD通知。", + "shownoncontrollerosd-tooltip": "显示OSD对与在管理的房间的非管理员有关的活动的通知。", + "showdifferentroomosd-tooltip": "显示与用户不在的房间有关的活动的OSD通知。", + "showslowdownosd-tooltip": "显示因进度不同步而进行慢速播放/恢复一倍速的通知。", + "showdurationnotification-tooltip": "当一个多部分文件中的一个片段丢失时,这很有用,但可能会导致误报。", + "language-tooltip": "Syncplay中使用的语言。", + "unpause-always-tooltip": "如果你按下继续播放,会总是将你设置为已准备并继续播放,而不是只将你设置为已准备。", + "unpause-ifalreadyready-tooltip": "如果你在没有准备好的情况下按下继续播放,它将把你设定为已准备的状态--再按下继续播放就可以继续播放。", + "unpause-ifothersready-tooltip": "如果你在没有准备好时按下继续播放,只有在其他人准备好时才会继续播放。", + "unpause-ifminusersready-tooltip": "如果你在没有准备好时按下继续播放,那么只有在其他人准备好并且达到最小开始人数时,它才会取消暂停。", + "trusteddomains-arguments-tooltip": "启用共享播放列表时,可以让Syncplay自动切换到的网站。", + + "chatinputenabled-tooltip": "在mpv中启用聊天输入(按回车键聊天,按回车键发送,按Esc键取消)。", + "chatdirectinput-tooltip": "在mpv中无需按回车键即可进入聊天输入模式。在mpv中按TAB键可以暂时禁用这个功能。", + "font-label-tooltip": "在mpv中输入聊天信息时使用的字体。只在客户端使用,所以不影响其他人看到的内容。", + "set-input-font-tooltip": "在mpv中输入聊天信息时使用的字体。只在客户端使用,所以不影响其他人看到的内容。", + "set-input-colour-tooltip": "在mpv中输入聊天信息时使用的字体颜色。只在客户端使用,所以不影响其他人看到的内容。", + "chatinputposition-tooltip": "聊天输入文本在mpv中的位置,当你按下回车键进行打字输入时,会出现在mpv中。", + "chatinputposition-top-tooltip": "将聊天输入放在mpv窗口的顶部。", + "chatinputposition-middle-tooltip": "将聊天输入放在mpv窗口的中间。", + "chatinputposition-bottom-tooltip": "将聊天输入放在mpv窗口的底部。", + "chatoutputenabled-tooltip": "在OSD中显示聊天信息(如果媒体播放器支持)。", + "font-output-label-tooltip": "聊天显示字体。", + "set-output-font-tooltip": "显示聊天信息时使用的字体。", + "chatoutputmode-tooltip": "聊天信息的显示方式。", + "chatoutputmode-chatroom-tooltip": "在前一行的正下方显示新的聊天行。", + "chatoutputmode-scrolling-tooltip": "像弹幕一样从左到右滚动显示聊天信息。", + + "help-tooltip": "打开Syncplay.pl网站上的用户指南。", + "reset-tooltip": "将所有设置重置为默认配置。", + "update-server-list-tooltip": "连接到syncplay.pl来更新公共服务器的列表。", + + "sslconnection-tooltip": "安全地连接到服务器。点击查看证书详情。", + + "joinroom-tooltip": "离开当前房间,加入指定房间。", + "seektime-msgbox-label": "跳到指定的时间(格式:秒/分:秒)。使用+/-进行相对跳转。", + "ready-tooltip": "表示你是否准备好观看。", + "autoplay-tooltip": "当所有拥有准备就绪指示器的用户都准备好了,并且满足最小开始人数时,自动播放。", + "switch-to-file-tooltip": "双击即可切换到{}", # Filename + "sendmessage-tooltip": "发送消息到房间中。", + + # In-userlist notes (GUI) + "differentsize-note": "大小不同!", + "differentsizeandduration-note": "大小和时长不同!", + "differentduration-note": "时长不同!", + "nofile-note": "(没有正在播放的视频)", + + # 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.", # NOTE: Do not translate the word -temp + + # Server notifications + "welcome-server-notification": "欢迎使用Syncplay服务端,当前版本{0}", # version + "client-connected-room-server-notification": "{0}({2})连接到了房间'{1}'", # username, host, room + "client-left-server-notification": "{0}断开了服务器", # name + "no-salt-notification": "请注意:为了使该服务器实例生成的房间管理员密码在服务器重新启动时仍然有效,请在今后运行Syncplay服务器时添加以下命令行参数:--salt {}", # Salt + + + # Server arguments + "server-argument-description": '在网络上同步播放多个媒体播放器实例的解决方案。服务器实例', + "server-argument-epilog": '如果没有提供选项,将使用_config的值', + "server-port-argument": '服务器TCP端口', + "server-password-argument": '服务器密码', + "server-isolate-room-argument": '房间是否相互隔离?', + "server-salt-argument": "用于生成管理的房间密码的随机字符串", + "server-disable-ready-argument": "停用准备就绪功能", + "server-motd-argument": "取出Motd的文件的路径", + "server-rooms-argument": "使用和/或创建数据库文件的路径,以存储持久的房间数据。使房间能够在没有观察者的情况下持续存在,并通过重新启动来实现。", + "server-permanent-rooms-argument": "列出永久房间的文件的路径,即使房间是空的也会被列出(以文本文件的形式,每行列出一个房间)--要求启用永久房间。", + "server-chat-argument": "是否禁用聊天功能?", + "server-chat-maxchars-argument": "聊天信息中的最大字符数(默认为{})", # Default number of characters + "server-maxusernamelength-argument": "用户名中的最大字符数(默认为{})。", + "server-stats-db-file-argument": "使用提供的SQLite db文件启用服务器统计功能", + "server-startTLS-argument": "使用提供的路径中的证书文件启用TLS连接", + "server-messed-up-motd-unescaped-placeholders": "每日信息中有未转义的占位符。所有 $ 字符应当重复两遍 ($$).", + "server-messed-up-motd-too-long": "每日信息过长 - 最大{}个chars, 给出的长度{}", + + # Server errors + "unknown-command-server-error": "未知命令 {}", # message + "not-json-server-error": "非json格式字符串 {}", # message + "line-decode-server-error": "非utf-8编码字符串", + "not-known-server-error": "在发送这条命令之前,你必须被服务器认证", + "client-drop-server-error": "客户端错误消息: {} -- {}", # host, error + "password-required-server-error": "请输入密码", + "wrong-password-server-error": "密码不正确", + "hello-server-error": "Not enough Hello arguments", # DO NOT TRANSLATE + + # Playlists + "playlist-selection-changed-notification": "{}换碟了", # Username + "playlist-contents-changed-notification": "{}更新了播放列表", # Username + "cannot-find-file-for-playlist-switch-error": "切换播放内容时,在媒体目录中找不到{}!", # Filename + "cannot-add-duplicate-error": "无法将'{}'第二次添加到播放列表中,因为不允许重复。", # Filename + "cannot-add-unsafe-path-error": "不能自动加载{},因为它不在受信任的网站上。你可以在播放列表中双击该URL,手动切换到该URL,并通过文件->高级->设置受信任网站添加受信任网站。如果你鼠标右键一个URL,那么你可以通过右键菜单将其网站添加为受信任网站。", # Filename + "sharedplaylistenabled-label": "启用共享播放列表", + "removefromplaylist-menu-label": "从播放列表中删除", + "shuffleremainingplaylist-menu-label": "随机排列未完成的播放列表", + "shuffleentireplaylist-menu-label": "随机排列整个播放列表", + "undoplaylist-menu-label": "撤销对播放列表的上一次修改", + "addfilestoplaylist-menu-label": "将文件添加到播放列表的底部", + "addurlstoplaylist-menu-label": "将URL添加到播放列表的底部", + "editplaylist-menu-label": "编辑播放列表", + + "open-containing-folder": "打开包含该文件的文件夹", + "addyourfiletoplaylist-menu-label": "将文件添加到播放列表", + "addotherusersfiletoplaylist-menu-label": "添加{}的文件到播放列表", # [Username] + "addyourstreamstoplaylist-menu-label": "将媒体流添加到播放列表", + "addotherusersstreamstoplaylist-menu-label": "添加{}的媒体流流到播放列表", # [Username] + "openusersstream-menu-label": "打开{}的媒体流", # [username]'s + "openusersfile-menu-label": "打开{}的文件", # [username]'s + + "playlist-instruction-item-message": "把文件拖到这里,就可以把它添加到共享播放列表中。", + "sharedplaylistenabled-tooltip": "房间管理员可以将文件添加到共享播放列表中,以方便大家观看同样的东西。可以在其他设置中配置媒体目录。", + + "playlist-empty-error": "播放列表目前是空的。", + "playlist-invalid-index-error": "无效的播放列表索引", +} From 619dc3abd70bb14492093ca21c0126d23748295e Mon Sep 17 00:00:00 2001 From: powerjungle Date: Sat, 12 Feb 2022 17:05:36 +0100 Subject: [PATCH 28/30] Fixed server argument max-username-length not working (#506) --- syncplay/server.py | 6 +++--- syncplay/utils.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/syncplay/server.py b/syncplay/server.py index 149be12..4c3e2d5 100755 --- a/syncplay/server.py +++ b/syncplay/server.py @@ -125,7 +125,7 @@ class SyncFactory(Factory): def addWatcher(self, watcherProtocol, username, roomName): roomName = truncateText(roomName, constants.MAX_ROOM_NAME_LENGTH) - username = self._roomManager.findFreeUsername(username) + username = self._roomManager.findFreeUsername(username, self.maxUsernameLength) watcher = Watcher(self, watcherProtocol, username) self.setWatcherRoom(watcher, roomName, asJoin=True) @@ -488,8 +488,8 @@ class RoomManager(object): self._roomsDbHandle.deleteRoom(room.getName()) del self._rooms[room.getName()] - def findFreeUsername(self, username): - username = truncateText(username, constants.MAX_USERNAME_LENGTH) + def findFreeUsername(self, username, maxUsernameLength=constants.MAX_USERNAME_LENGTH): + username = truncateText(username, maxUsernameLength) allnames = [] for room in self._rooms.values(): for watcher in room.getWatchers(): diff --git a/syncplay/utils.py b/syncplay/utils.py index 28a9d01..77c4f46 100755 --- a/syncplay/utils.py +++ b/syncplay/utils.py @@ -232,8 +232,9 @@ def truncateText(unicodeText, maxLength): unicodeText = unicodeText.decode('utf-8') except: pass + try: - return(str(unicodeText.encode("utf-8"), "utf-8", errors="ignore")[:maxLength]) + return str(unicodeText.encode("utf-8"), "utf-8", errors="ignore")[:maxLength] except: pass return "" From 1404d58740878561adb99faaaab815a07a50b7ab Mon Sep 17 00:00:00 2001 From: Etoh Date: Sat, 12 Feb 2022 16:12:50 +0000 Subject: [PATCH 29/30] Remove mpv fallback warning spam --- syncplay/vendor/python_mpv_jsonipc/python_mpv_jsonipc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/syncplay/vendor/python_mpv_jsonipc/python_mpv_jsonipc.py b/syncplay/vendor/python_mpv_jsonipc/python_mpv_jsonipc.py index 3329b2d..87224fa 100644 --- a/syncplay/vendor/python_mpv_jsonipc/python_mpv_jsonipc.py +++ b/syncplay/vendor/python_mpv_jsonipc/python_mpv_jsonipc.py @@ -426,7 +426,6 @@ class MPV: try: command_list = [x["name"] for x in self.command("get_property", "command-list")] except MPVError: - log.warning("Using fallback command list.") command_list = FALLBACK_COMMAND_LIST for command in command_list: object.__setattr__(self, command.replace("-", "_"), self._get_wrapper(command)) From e4322e10932fd8f931d784e1ad1abffcdbe9a566 Mon Sep 17 00:00:00 2001 From: Etoh Date: Fri, 4 Mar 2022 14:07:46 +0000 Subject: [PATCH 30/30] Fix #504 disconnected monitor bug (#505) * Fix #504 disconnected monitor bug --- syncplay/ui/gui.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/syncplay/ui/gui.py b/syncplay/ui/gui.py index 7068db6..9e2b425 100755 --- a/syncplay/ui/gui.py +++ b/syncplay/ui/gui.py @@ -2057,7 +2057,12 @@ class MainWindow(QtWidgets.QMainWindow): settings = QSettings("Syncplay", "MainWindow") settings.beginGroup("MainWindow") self.resize(settings.value("size", QSize(700, 500))) - self.move(settings.value("pos", QPoint(200, 200))) + movePos = settings.value("pos", QPoint(200, 200)) + windowGeometry = QtWidgets.QApplication.desktop().availableGeometry(self) + posIsOnScreen = windowGeometry.contains(QtCore.QRect(movePos.x(), movePos.y(), 1, 1)) + if not posIsOnScreen: + movePos = QPoint(200,200) + self.move(movePos) if settings.value("showPlaybackButtons", "false") == "true": self.playbackAction.setChecked(True) self.updatePlaybackFrameVisibility()