Merge pull request #1 from Uriziel/master

Merge upstream
This commit is contained in:
nilsding 2014-12-01 06:47:06 +01:00
commit 91c6dec035
28 changed files with 1987 additions and 411 deletions

View File

@ -4,9 +4,27 @@ ifndef VLC_SUPPORT
VLC_SUPPORT = true
endif
ifeq ($(shell uname)),FreeBSD)
BSD = true
endif
ifeq ($(shell uname)),NetBSD)
BSD = true
endif
ifeq ($(shell uname)),OpenBSD)
BSD = true
endif
ifeq ($(shell uname)),DragonFly)
BSD = true
endif
ifeq ($(SINGLE_USER),false)
ifneq ($(BSD),true)
BASE_PATH = /usr
VLC_LIB_PATH = ${PREFIX}/usr/lib
else
BASE_PATH = /usr/local
VLC_LIB_PATH = ${PREFIX}/usr/local/lib
endif
else
BASE_PATH = ${HOME}/.local
VLC_LIB_PATH = ${HOME}/.local/share
@ -18,8 +36,6 @@ endif
SHARE_PATH = ${PREFIX}$(BASE_PATH)/share
common:
-mkdir -p $(LIB_PATH)/syncplay/resources/
-mkdir -p $(LIB_PATH)/syncplay/resources/lua
-mkdir -p $(LIB_PATH)/syncplay/resources/lua/intf
-mkdir -p $(APP_SHORTCUT_PATH)
-mkdir -p $(SHARE_PATH)/app-install/icons

View File

@ -16,7 +16,13 @@ import syncplay
import os
import subprocess
p = "C:\\Program Files (x86)\\NSIS\\makensis.exe" #TODO: how to move that into proper place, huh
from syncplay.messages import getMissingStrings
missingStrings = getMissingStrings()
if missingStrings is not None and missingStrings is not "":
import warnings
warnings.warn("MISSING/UNUSED STRINGS DETECTED:\n{}".format(missingStrings))
p = "C:\\Program Files (x86)\\NSIS\\Unicode\\makensis.exe" #TODO: how to move that into proper place, huh
NSIS_COMPILE = p if os.path.isfile(p) else "makensis.exe"
OUT_DIR = "syncplay v{}".format(syncplay.version)
SETUP_SCRIPT_PATH = "syncplay_setup.nsi"
@ -27,7 +33,9 @@ NSIS_SCRIPT_TEMPLATE = r"""
LoadLanguageFile "$${NSISDIR}\Contrib\Language files\English.nlf"
LoadLanguageFile "$${NSISDIR}\Contrib\Language files\Polish.nlf"
LoadLanguageFile "$${NSISDIR}\Contrib\Language files\Russian.nlf"
LoadLanguageFile "$${NSISDIR}\Contrib\Language files\German.nlf"
Name "Syncplay $version"
OutFile "Syncplay $version Setup.exe"
InstallDir $$PROGRAMFILES\Syncplay
@ -35,18 +43,23 @@ NSIS_SCRIPT_TEMPLATE = r"""
XPStyle on
Icon resources\icon.ico ;Change DIR
SetCompressor /SOLID lzma
VIProductVersion "$version.0"
VIAddVersionKey /LANG=$${LANG_ENGLISH} "ProductName" "Syncplay"
VIAddVersionKey /LANG=$${LANG_ENGLISH} "FileVersion" "$version.0"
VIAddVersionKey /LANG=$${LANG_ENGLISH} "LegalCopyright" "Syncplay"
VIAddVersionKey /LANG=$${LANG_ENGLISH} "FileDescription" "Syncplay"
VIAddVersionKey /LANG=$${LANG_POLISH} "ProductName" "Syncplay"
VIAddVersionKey /LANG=$${LANG_POLISH} "FileVersion" "$version.0"
VIAddVersionKey /LANG=$${LANG_POLISH} "LegalCopyright" "Syncplay"
VIAddVersionKey /LANG=$${LANG_POLISH} "FileDescription" "Syncplay"
VIAddVersionKey /LANG=$${LANG_RUSSIAN} "ProductName" "Syncplay"
VIAddVersionKey /LANG=$${LANG_RUSSIAN} "FileVersion" "$version.0"
VIAddVersionKey /LANG=$${LANG_RUSSIAN} "LegalCopyright" "Syncplay"
VIAddVersionKey /LANG=$${LANG_RUSSIAN} "FileDescription" "Syncplay"
LangString ^SyncplayLanguage $${LANG_ENGLISH} "en"
LangString ^Associate $${LANG_ENGLISH} "Associate Syncplay with multimedia files."
LangString ^VLC $${LANG_ENGLISH} "Install Syncplay interface for VLC 2 and above"
@ -56,7 +69,7 @@ NSIS_SCRIPT_TEMPLATE = r"""
LangString ^Desktop $${LANG_ENGLISH} "Desktop"
LangString ^QuickLaunchBar $${LANG_ENGLISH} "Quick Launch Bar"
LangString ^UninstConfig $${LANG_ENGLISH} "Delete configuration file."
LangString ^SyncplayLanguage $${LANG_POLISH} "pl"
LangString ^Associate $${LANG_POLISH} "Skojarz Syncplaya z multimediami"
LangString ^VLC $${LANG_POLISH} "Zainstaluj interface Syncplaya dla VLC 2+"
@ -66,16 +79,39 @@ NSIS_SCRIPT_TEMPLATE = r"""
LangString ^Desktop $${LANG_POLISH} "Pulpit"
LangString ^QuickLaunchBar $${LANG_POLISH} "Pasek szybkiego uruchamiania"
LangString ^UninstConfig $${LANG_POLISH} "Usun plik konfiguracyjny."
LangString ^SyncplayLanguage $${LANG_RUSSIAN} "ru"
LangString ^Associate $${LANG_RUSSIAN} "Ассоциировать Syncplay с видеофайлами"
LangString ^VLC $${LANG_RUSSIAN} "Установить интерфейс Syncplay для VLC 2+"
LangString ^BrowseVLCBtn $${LANG_RUSSIAN} "Укажите папку VLC"
LangString ^Shortcut $${LANG_RUSSIAN} "Создать ярлыки:"
LangString ^StartMenu $${LANG_RUSSIAN} "в меню Пуск"
LangString ^Desktop $${LANG_RUSSIAN} "на рабочем столе"
LangString ^QuickLaunchBar $${LANG_RUSSIAN} "в меню быстрого запуска"
LangString ^UninstConfig $${LANG_RUSSIAN} "Удалить файл настроек."
LangString ^SyncplayLanguage $${LANG_GERMAN} "de"
LangString ^Associate $${LANG_GERMAN} "Syncplay mit Multimedia-Dateien assoziieren."
LangString ^VLC $${LANG_GERMAN} "Syncplay-Interface für VLC installieren (ab VLC 2+)"
LangString ^Shortcut $${LANG_GERMAN} "Erstelle Verknüpfungen an folgenden Orten:"
LangString ^BrowseVLCBtn $${LANG_GERMAN} "VLC-Ordner wählen"
LangString ^StartMenu $${LANG_GERMAN} "Startmenü"
LangString ^Desktop $${LANG_GERMAN} "Desktop"
LangString ^QuickLaunchBar $${LANG_GERMAN} "Schnellstartleiste"
LangString ^UninstConfig $${LANG_GERMAN} "Konfigurationsdatei löschen."
; Remove text to save space
LangString ^ClickInstall $${LANG_GERMAN} " "
PageEx license
LicenseData resources\license.txt
PageExEnd
Page custom DirectoryCustom DirectoryCustomLeave
Page instFiles
UninstPage custom un.installConfirm un.installConfirmLeave
UninstPage instFiles
Var Dialog
Var Icon_Syncplay
Var Icon_Syncplay_Handle
@ -98,7 +134,7 @@ NSIS_SCRIPT_TEMPLATE = r"""
Var Label_Size
Var Label_Space
Var Text_Directory
Var Uninst_Dialog
Var Uninst_Icon
Var Uninst_Icon_Handle
@ -107,14 +143,14 @@ NSIS_SCRIPT_TEMPLATE = r"""
Var Uninst_Text_Directory
Var Uninst_CheckBox_Config
Var Uninst_CheckBox_Config_State
Var Size
Var SizeHex
Var AvailibleSpace
Var AvailibleSpaceGiB
Var Drive
Var VLC_Directory
!macro APP_ASSOCIATE EXT FileCLASS DESCRIPTION COMMANDTEXT COMMAND
WriteRegStr HKCR ".$${EXT}" "" "$${FileCLASS}"
WriteRegStr HKCR "$${FileCLASS}" "" `$${DESCRIPTION}`
@ -122,23 +158,23 @@ NSIS_SCRIPT_TEMPLATE = r"""
WriteRegStr HKCR "$${FileCLASS}\shell\open" "" `$${COMMANDTEXT}`
WriteRegStr HKCR "$${FileCLASS}\shell\open\command" "" `$${COMMAND}`
!macroend
!macro APP_UNASSOCIATE EXT FileCLASS
; Backup the previously associated File class
ReadRegStr $$R0 HKCR ".$${EXT}" `$${FileCLASS}_backup`
WriteRegStr HKCR ".$${EXT}" "" "$$R0"
DeleteRegKey HKCR `$${FileCLASS}`
!macroend
!macro ASSOCIATE EXT
!insertmacro APP_ASSOCIATE "$${EXT}" "Syncplay.$${EXT}" "$$INSTDIR\Syncplay.exe,%1%" \
"Open with Syncplay" "$$INSTDIR\Syncplay.exe $$\"%1$$\""
!macroend
!macro UNASSOCIATE EXT
!insertmacro APP_UNASSOCIATE "$${EXT}" "Syncplay.$${EXT}"
!macroend
;Prevents from running more than one instance of installer and sets default state of checkboxes
Function .onInit
System::Call 'kernel32::CreateMutexA(i 0, i 0, t "SyncplayMutex") i .r1 ?e'
@ -164,6 +200,10 @@ NSIS_SCRIPT_TEMPLATE = r"""
Push English
Push $${LANG_POLISH}
Push Polski
Push $${LANG_RUSSIAN}
Push Русский
Push $${LANG_GERMAN}
Push Deutsch
Push A ; A means auto count languages
LangDLL::LangDialog "Language Selection" "Please select the language of Syncplay and the installer"
Pop $$LANGUAGE
@ -216,13 +256,13 @@ NSIS_SCRIPT_TEMPLATE = r"""
$${NSD_CreateLabel} 8u 85u 187u 10u "$$(^Shortcut)"
Pop $$Label_Shortcut
$${NSD_CreateCheckbox} 8u 98u 50u 10u "$$(^StartMenu)"
$${NSD_CreateCheckbox} 8u 98u 60u 10u "$$(^StartMenu)"
Pop $$CheckBox_StartMenuShortcut
$${NSD_CreateCheckbox} 68u 98u 50u 10u "$$(^Desktop)"
$${NSD_CreateCheckbox} 78u 98u 70u 10u "$$(^Desktop)"
Pop $$CheckBox_DesktopShortcut
$${NSD_CreateCheckbox} 128u 98u 150u 10u "$$(^QuickLaunchBar)"
$${NSD_CreateCheckbox} 158u 98u 130u 10u "$$(^QuickLaunchBar)"
Pop $$CheckBox_QuickLaunchShortcut
$${If} $$CheckBox_Associate_State == $${BST_CHECKED}
@ -476,18 +516,18 @@ NSIS_SCRIPT_TEMPLATE = r"""
IfFileExists "$$VLC_Directory\lua\intf\syncplay.lua" 0 +2
Delete $$VLC_Directory\lua\intf\syncplay.lua
FunctionEnd
Section "Install"
SetOverwrite on
SetOutPath $$INSTDIR
WriteUninstaller uninstall.exe
$installFiles
Call InstallOptions
Call WriteRegistry
SectionEnd
Section "Uninstall"
Call un.AssociateDel
Call un.InstallOptions
@ -582,7 +622,9 @@ guiIcons = ['resources/accept.png', 'resources/arrow_undo.png', 'resources/clock
'resources/timeline_marker.png','resources/control_play_blue.png',
'resources/mpc-hc.png','resources/mpc-hc64.png','resources/mplayer.png',
'resources/mpv.png','resources/vlc.png', 'resources/house.png', 'resources/film_link.png',
'resources/eye.png', 'resources/comments.png', 'resources/cog_delete.png', 'resources/bullet_black.png'
'resources/eye.png', 'resources/comments.png', 'resources/cog_delete.png', 'resources/chevrons_right.png',
'resources/user_key.png', 'resources/lock.png', 'resources/key_go.png', 'resources/page_white_key.png',
'resources/tick.png', 'resources/lock_open.png'
]
resources = ["resources/icon.ico", "resources/syncplay.png"]
resources.extend(guiIcons)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

BIN
resources/key_go.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

BIN
resources/lock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

BIN
resources/lock_open.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

View File

@ -5,7 +5,7 @@
Principal author: Etoh
Other contributors: DerGenaue, jb
Project: http://syncplay.pl/
Version: 0.2.2
Version: 0.2.3
Note:
* This interface module is intended to be used in conjunction with Syncplay.
@ -78,16 +78,7 @@ You may also need to re-copy the syncplay.lua file when you update VLC.
--]==========================================================================]
local modulepath = config["modulepath"]
if(modulepath ~= nil) and (modulepath ~= "") then
-- Workaround for when the script is not being run from the usual VLC intf folder.
package.path = modulepath
pcall(require,"common")
else
require "common"
end
local connectorversion = "0.2.2"
local connectorversion = "0.2.3"
local durationdelay = 500000 -- Pause for get_duration command etc for increased reliability (uses microseconds)
local loopsleepduration = 5000 -- Pause for every event loop (uses microseconds)
local quitcheckfrequency = 20 -- Check whether VLC has closed every X loops
@ -119,9 +110,32 @@ local newtitle = 0
local running = true
function radixsafe_tonumber(str)
-- Version of tonumber that works with any radix character (but not thousand seperators)
-- Based on the public domain VLC common.lua us_tonumber() function
str = string.gsub(tostring(str), "[^0-9]", ".")
local s, i, d = string.match(str, "^([+-]?)(%d*)%.?(%d*)$")
if not s or not i or not d then
return nil
end
if s == "-" then
s = -1
else
s = 1
end
if i == "" then
i = "0"
end
if d == nil or d == "" then
d = "0"
end
return s * (tonumber(i) + tonumber(d)/(10^string.len(d)))
end
-- Start hosting Syncplay interface.
port = tonumber(config["port"])
port = radixsafe_tonumber(config["port"])
if (port == nil or port < 1) then port = 4123 end
function quit_vlc()
@ -256,12 +270,12 @@ function set_time ( timetoset)
if input then
local response, errormsg, realtime, titletrack
realtime = timetoset % titlemultiplier
oldtitle = tonumber(get_var("title", 0))
oldtitle = radixsafe_tonumber(get_var("title", 0))
newtitle = (timetoset - realtime) / titlemultiplier
if oldtitle ~= newtitle and newtitle > -1 then
set_var("title", tonumber(newtitle))
set_var("title", radixsafe_tonumber(newtitle))
end
errormsg = set_var("time", tonumber(realtime))
errormsg = set_var("time", radixsafe_tonumber(realtime))
return errormsg
else
return noinput
@ -393,7 +407,7 @@ function display_osd ( argument )
if input then
osdarray = get_args(argument,3)
--position, duration, message -> message, , position, duration (converted from seconds to microseconds)
local osdduration = tonumber(osdarray[2]) * 1000 * 1000
local osdduration = radixsafe_tonumber(osdarray[2]) * 1000 * 1000
vlc.osd.message(osdarray[3],channel1,osdarray[1],osdduration)
else
errormsg = noinput
@ -425,11 +439,11 @@ function do_command ( command, argument)
elseif command == "get-filepath" then response = "filepath"..msgseperator..errormerge(get_filepath())..msgterminator
elseif command == "get-filename" then response = "filename"..msgseperator..errormerge(get_filename())..msgterminator
elseif command == "get-title" then response = "title"..msgseperator..errormerge(get_var("title", 0))..msgterminator
elseif command == "set-position" then errormsg = set_time(tonumber(argument))
elseif command == "seek-within-title" then errormsg = set_var("time", tonumber(argument))
elseif command == "set-position" then errormsg = set_time(radixsafe_tonumber(argument))
elseif command == "seek-within-title" then errormsg = set_var("time", radixsafe_tonumber(argument))
elseif command == "set-playstate" then errormsg = set_playstate(argument)
elseif command == "set-rate" then errormsg = set_var("rate", tonumber(argument))
elseif command == "set-title" then errormsg = set_var("title", tonumber(argument))
elseif command == "set-rate" then errormsg = set_var("rate", radixsafe_tonumber(argument))
elseif command == "set-title" then errormsg = set_var("title", radixsafe_tonumber(argument))
elseif command == "display-osd" then errormsg = display_osd(argument)
elseif command == "load-file" then response = load_file(argument)
elseif command == "close-vlc" then quit_vlc()
@ -471,7 +485,7 @@ function set_playstate(argument)
end
if string.sub(vlc.misc.version(),1,2) == "1." then
vlc.msg.err("This version of VLC is not known to support version " .. connectorversion .. " of the Syncplay interface module on Windows. Please use VLC 2+.")
vlc.msg.err("This version of VLC is not known to support the Syncplay interface module. Please use VLC 2+.")
quit_vlc()
else
l = vlc.net.listen_tcp(host, port)

Binary file not shown.

After

Width:  |  Height:  |  Size: 616 B

BIN
resources/tick.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

BIN
resources/user_key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 B

View File

@ -1,3 +1,3 @@
version = '1.2.9'
milestone = 'Pineapple, Pulverize and Destroy!'
version = '1.3.0'
milestone = 'Akki'
projectURL = 'http://syncplay.pl/'

View File

@ -4,12 +4,12 @@ import time
import re
from twisted.internet.protocol import ClientFactory
from twisted.internet import reactor, task
from functools import wraps
from syncplay.protocols import SyncClientProtocol
from syncplay import utils, constants
from syncplay.messages import getMessage
import threading
from syncplay.messages import getMissingStrings, getMessage
from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \
PRIVACY_HIDDENFILENAME, FILENAME_STRIP_REGEX
PRIVACY_HIDDENFILENAME
import collections
class SyncClientFactory(ClientFactory):
@ -61,14 +61,21 @@ class SyncplayClient(object):
constants.SHOW_DIFFERENT_ROOM_OSD = config['showDifferentRoomOSD']
constants.SHOW_SAME_ROOM_OSD = config['showSameRoomOSD']
constants.SHOW_DURATION_NOTIFICATION = config['showDurationNotification']
constants.DEBUG_MODE = config['debug']
self.controlpasswords = {}
self.lastControlPasswordAttempt = None
self.serverVersion = "0.0.0"
self.lastLeftTime = 0
self.lastLeftUser = u""
self.protocolFactory = SyncClientFactory(self)
self.ui = UiManager(self, ui)
self.userlist = SyncplayUserlist(self.ui, self)
self._protocol = None
""":type : SyncClientProtocol|None"""
self._player = None
if config['room'] == None or config['room'] == '':
if config['room'] is None or config['room'] == '':
config['room'] = config['name'] # ticket #58
self.defaultRoom = config['room']
self.playerPositionBeforeLastSeek = 0.0
@ -99,7 +106,13 @@ class SyncplayClient(object):
self._warnings = self._WarningManager(self._player, self.userlist, self.ui)
if constants.LIST_RELATIVE_CONFIGS and self._config.has_key('loadedRelativePaths') and self._config['loadedRelativePaths']:
self.ui.showMessage(getMessage("relative-config-notification").format("; ".join(self._config['loadedRelativePaths'])), noPlayer=True, noTimestamp=True)
paths = "; ".join(self._config['loadedRelativePaths'])
self.ui.showMessage(getMessage("relative-config-notification").format(paths), noPlayer=True, noTimestamp=True)
if constants.DEBUG_MODE and constants.WARN_ABOUT_MISSING_STRINGS:
missingStrings = getMissingStrings()
if missingStrings is not None and missingStrings is not "":
self.ui.showDebugMessage("MISSING/UNUSED STRINGS DETECTED:\n{}".format(missingStrings))
def initProtocol(self, protocol):
self._protocol = protocol
@ -177,6 +190,13 @@ class SyncplayClient(object):
madeChangeOnPlayer = True
return madeChangeOnPlayer
def _fastforwardPlayerDueToTimeDifference(self, position, setBy):
hideFromOSD = not constants.SHOW_SAME_ROOM_OSD
self.setPosition(position + constants.FASTFORWARD_EXTRA_TIME)
self.ui.showMessage(getMessage("fastforward-notification").format(setBy), hideFromOSD)
madeChangeOnPlayer = True
return madeChangeOnPlayer
def _serverUnpaused(self, setBy):
hideFromOSD = not constants.SHOW_SAME_ROOM_OSD
self._player.setPaused(False)
@ -223,7 +243,7 @@ class SyncplayClient(object):
def _changePlayerStateAccordingToGlobalState(self, position, paused, doSeek, setBy):
madeChangeOnPlayer = False
pauseChanged = paused != self.getGlobalPaused()
pauseChanged = paused != self.getGlobalPaused() or paused != self.getPlayerPaused()
diff = self.getPlayerPosition() - position
if self._lastGlobalUpdate is None:
madeChangeOnPlayer = self._initPlayerState(position, paused)
@ -234,7 +254,9 @@ class SyncplayClient(object):
madeChangeOnPlayer = self._serverSeeked(position, setBy)
if diff > self._config['rewindThreshold'] and not doSeek and not self._config['rewindOnDesync'] == False:
madeChangeOnPlayer = self._rewindPlayerDueToTimeDifference(position, setBy)
if (self._player.speedSupported and not doSeek and not paused and not self._config['slowOnDesync'] == False):
if diff < (self._config['fastforwardThreshold'] * -1) and not doSeek and not self._config['fastforwardOnDesync'] == False:
madeChangeOnPlayer = self._fastforwardPlayerDueToTimeDifference(position, setBy)
if self._player.speedSupported and not doSeek and not paused and not self._config['slowOnDesync'] == False:
madeChangeOnPlayer = self._slowDownToCoverTimeDifference(diff, setBy)
if paused == False and pauseChanged:
madeChangeOnPlayer = self._serverUnpaused(setBy)
@ -317,7 +339,6 @@ class SyncplayClient(object):
size = os.path.getsize(path)
except OSError: # file not accessible (stream?)
size = 0
rawfilename = filename
filename, size = self.__executePrivacySettings(filename, size)
self.userlist.currentUser.setFile(filename, duration, size)
self.sendFile()
@ -333,6 +354,9 @@ class SyncplayClient(object):
size = 0
return filename, size
def setServerVersion(self, version):
self.serverVersion = version
def sendFile(self):
file_ = self.userlist.currentUser.file
if self._protocol and self._protocol.logged and file_:
@ -358,6 +382,17 @@ class SyncplayClient(object):
if self._protocol and self._protocol.logged and room:
self._protocol.sendRoomSetting(room)
self.getUserList()
self.reIdentifyAsController()
def reIdentifyAsController(self):
room = self.userlist.currentUser.room
if utils.RoomPasswordProvider.isControlledRoom(room):
storedRoomPassword = self.getControlledRoomPassword(room)
if storedRoomPassword:
self.identifyAsController(storedRoomPassword)
def connected(self):
self.reIdentifyAsController()
def getRoom(self):
return self.userlist.currentUser.room
@ -414,6 +449,64 @@ class SyncplayClient(object):
if promptForAction:
self.ui.promptFor(getMessage("enter-to-exit-prompt"))
def requireMinServerVersion(minVersion):
def requireMinVersionDecorator(f):
@wraps(f)
def wrapper(self, *args, **kwds):
if not utils.meetsMinVersion(self.serverVersion,minVersion):
self.ui.showErrorMessage(u"This feature is not supported by the server. The feature requires a server running Syncplay {}+, but the server is running Syncplay {}.".format(minVersion, self.serverVersion))
return
return f(self, *args, **kwds)
return wrapper
return requireMinVersionDecorator
@requireMinServerVersion(constants.CONTROLLED_ROOMS_MIN_VERSION)
def createControlledRoom(self, roomName):
controlPassword = utils.RandomStringGenerator.generate_room_password()
self.ui.showMessage(u"Attempting to create controlled room '{}' with password '{}'...".format(roomName, controlPassword))
self.lastControlPasswordAttempt = controlPassword
self._protocol.requestControlledRoom(roomName, controlPassword)
def controlledRoomCreated(self, roomName, controlPassword):
self.ui.showMessage(u"Created controlled room '{}' with password '{}'. Please save this information for future reference!".format(roomName, controlPassword))
self.setRoom(roomName)
self.sendRoom()
self._protocol.requestControlledRoom(roomName, controlPassword)
self.ui.updateRoomName(roomName)
def stripControlPassword(self, controlPassword):
if controlPassword:
return re.sub(constants.CONTROL_PASSWORD_STRIP_REGEX, "", controlPassword).upper()
else:
return ""
@requireMinServerVersion(constants.CONTROLLED_ROOMS_MIN_VERSION)
def identifyAsController(self, controlPassword):
controlPassword = self.stripControlPassword(controlPassword)
self.ui.showMessage(getMessage("identifying-as-controller-notification").format(controlPassword))
self.lastControlPasswordAttempt = controlPassword
self._protocol.requestControlledRoom(self.getRoom(), controlPassword)
def controllerIdentificationError(self, username, room):
self.ui.showErrorMessage(getMessage("failed-to-identify-as-controller-notification").format(username))
def controllerIdentificationSuccess(self, username, roomname):
self.userlist.setUserAsController(username)
if self.userlist.isRoomSame(roomname):
hideFromOSD = not constants.SHOW_SAME_ROOM_OSD
self.ui.showMessage(getMessage("authenticated-as-controller-notification").format(username), hideFromOSD)
if username == self.userlist.currentUser.username:
self.storeControlPassword(roomname, self.lastControlPasswordAttempt)
self.ui.userListChange()
def storeControlPassword(self, room, password):
if password:
self.controlpasswords[room] = password
def getControlledRoomPassword(self, room):
if self.controlpasswords.has_key(room):
return self.controlpasswords[room]
class _WarningManager(object):
def __init__(self, player, userlist, ui):
self._player = player
@ -466,6 +559,7 @@ class SyncplayUser(object):
self.username = username
self.room = room
self.file = file_
self._controller = False
def setFile(self, filename, duration, size):
file_ = {
@ -484,7 +578,10 @@ class SyncplayUser(object):
return sameName and sameSize and sameDuration
def __lt__(self, other):
return self.username.lower() < other.username.lower()
if self.isController() == other.isController():
return self.username.lower() < other.username.lower()
else:
return self.isController() > other.isController()
def __repr__(self, *args, **kwargs):
if self.file:
@ -492,6 +589,16 @@ class SyncplayUser(object):
else:
return "{}".format(self.username)
def setControllerStatus(self, isController):
self._controller = isController
def isController(self):
return self._controller
def canControl(self):
if self.isController() or not utils.RoomPasswordProvider.isControlledRoom(self.room):
return True
class SyncplayUserlist(object):
def __init__(self, ui, client):
self.currentUser = SyncplayUser()
@ -512,40 +619,46 @@ class SyncplayUserlist(object):
showOnOSD = constants.SHOW_OSD_WARNINGS
else:
showOnOSD = constants.SHOW_DIFFERENT_ROOM_OSD
if constants.SHOW_NONCONTROLLER_OSD == False and self.canControl(username) == False:
showOnOSD = False
hideFromOSD = not showOnOSD
if room and not file_:
message = getMessage("room-join-notification").format(username, room)
self.ui.showMessage(message, hideFromOSD)
elif room and file_:
duration = utils.formatTime(file_['duration'])
message = getMessage("playing-notification").format(username, file_['name'], duration)
if self.currentUser.room <> room or self.currentUser.username == username:
message += getMessage("playing-notification/room-addendum").format(room)
self.ui.showMessage(message, hideFromOSD)
if self.currentUser.file and not self.currentUser.isFileSame(file_) and self.currentUser.room == room:
message = getMessage("file-different-notification").format(username)
self.ui.showMessage(message, not constants.SHOW_OSD_WARNINGS)
differences = []
differentName = not utils.sameFilename(self.currentUser.file['name'], file_['name'])
differentSize = not utils.sameFilesize(self.currentUser.file['size'], file_['size'])
differentDuration = not utils.sameFileduration(self.currentUser.file['duration'], file_['duration'])
if differentName:
differences.append("filename")
if differentSize:
differences.append("size")
if differentDuration:
differences.append("duration")
message = getMessage("file-differences-notification") + ", ".join(differences)
self.ui.showMessage(message, not constants.SHOW_OSD_WARNINGS)
if not file_:
message = getMessage("room-join-notification").format(username, room)
self.ui.showMessage(message, hideFromOSD)
else:
duration = utils.formatTime(file_['duration'])
message = getMessage("playing-notification").format(username, file_['name'], duration)
if self.currentUser.room <> room or self.currentUser.username == username:
message += getMessage("playing-notification/room-addendum").format(room)
self.ui.showMessage(message, hideFromOSD)
if self.currentUser.file and not self.currentUser.isFileSame(file_) and self.currentUser.room == room:
message = getMessage("file-different-notification").format(username)
self.ui.showMessage(message, hideFromOSD)
differences = []
differentName = not utils.sameFilename(self.currentUser.file['name'], file_['name'])
differentSize = not utils.sameFilesize(self.currentUser.file['size'], file_['size'])
differentDuration = not utils.sameFileduration(self.currentUser.file['duration'], file_['duration'])
if differentName:
differences.append("filename")
if differentSize:
differences.append("size")
if differentDuration:
differences.append("duration")
message = getMessage("file-differences-notification") + ", ".join(differences)
self.ui.showMessage(message, hideFromOSD)
def addUser(self, username, room, file_, noMessage=False):
def addUser(self, username, room, file_, noMessage=False, isController=None):
if username == self.currentUser.username:
if isController is not None:
self.currentUser.setControllerStatus(isController)
return
user = SyncplayUser(username, room, file_)
if isController is not None:
user.setControllerStatus(isController)
self._users[username] = user
if not noMessage:
self.__showUserChangeMessage(username, room, file_)
self.userListChange()
self.userListChange(room)
def removeUser(self, username):
hideFromOSD = not constants.SHOW_DIFFERENT_ROOM_OSD
@ -572,6 +685,8 @@ class SyncplayUserlist(object):
if self._users.has_key(username):
user = self._users[username]
oldRoom = user.room if user.room else None
if user.room != room:
user.setControllerStatus(isController=False)
self.__displayModUserMessage(username, room, file_, user, oldRoom)
user.room = room
if file_:
@ -580,12 +695,20 @@ class SyncplayUserlist(object):
self.__showUserChangeMessage(username, room, file_)
else:
self.addUser(username, room, file_)
self.userListChange()
self.userListChange(room)
def setUserAsController(self, username):
if self.currentUser.username == username:
self.currentUser.setControllerStatus(True)
elif self._users.has_key(username):
user = self._users[username]
user.setControllerStatus(True)
def areAllFilesInRoomSame(self):
for user in self._users.itervalues():
if user.room == self.currentUser.room and user.file and not self.currentUser.isFileSame(user.file):
return False
if user.canControl():
return False
return True
def areYouAloneInRoom(self):
@ -600,8 +723,18 @@ class SyncplayUserlist(object):
return True
return False
def userListChange(self):
self._roomUsersChanged = True
def canControl(self, username):
if self.currentUser.username == username and self.currentUser.canControl():
return True
for user in self._users.itervalues():
if user.username == username and user.canControl():
return True
return False
def userListChange(self, room = None):
if room is not None and self.isRoomSame(room):
self._roomUsersChanged = True
self.ui.userListChange()
def roomStateConfirmed(self):
@ -637,6 +770,10 @@ class UiManager(object):
self.__ui = ui
self.lastError = ""
def showDebugMessage(self, message):
if constants.DEBUG_MODE and message.rstrip():
print "{}{}".format(time.strftime(constants.UI_TIME_FORMAT, time.localtime()),message.rstrip())
def showMessage(self, message, noPlayer=False, noTimestamp=False):
if not noPlayer: self.showOSDMessage(message)
self.__ui.showMessage(message, noTimestamp)
@ -648,6 +785,9 @@ class UiManager(object):
if constants.SHOW_OSD and self._client._player:
self._client._player.displayMessage(message, duration * 1000)
def setControllerStatus(self, username, isController):
self.__ui.setControllerStatus(username, isController)
def showErrorMessage(self, message, criticalerror=False):
if message <> self.lastError: # Avoid double call bug
self.lastError = message
@ -662,5 +802,10 @@ class UiManager(object):
def markEndOfUserlist(self):
self.__ui.markEndOfUserlist()
def updateRoomName(self, room=""):
self.__ui.updateRoomName(room)
def drop(self):
self.__ui.drop()

View File

@ -14,18 +14,24 @@ LIST_RELATIVE_CONFIGS = True # Print list of relative configs loaded
SHOW_CONTACT_INFO = True # Displays dev contact details below list in GUI
SHOW_BUTTON_LABELS = True # If disabled, only shows icons for main GUI buttons
SHOW_TOOLTIPS = True
WARN_ABOUT_MISSING_STRINGS = False # (If debug mode is enabled)
#Overriden by config
SHOW_OSD = True # Sends Syncplay messages to media player OSD
SHOW_OSD_WARNINGS = True # Show warnings if playing different file, alone in room
SHOW_SLOWDOWN_OSD = True # Show notifications of slowing down / reverting on time difference
SHOW_SAME_ROOM_OSD = True # Show OSD notifications for events relating to room user is in
SHOW_NONCONTROLLER_OSD = False # Show OSD notifications for non-controllers in controlled rooms
SHOW_DIFFERENT_ROOM_OSD = False # Show OSD notifications for events relating to room user is not in
SHOW_DURATION_NOTIFICATION = True
DEBUG_MODE = False
#Changing these might be ok
DEFAULT_REWIND_THRESHOLD = 4
MINIMUM_REWIND_THRESHOLD = 3
DEFAULT_FASTFORWARD_THRESHOLD = 5
MINIMUM_FASTFORWARD_THRESHOLD = 2
FASTFORWARD_EXTRA_TIME = 0.25
SEEK_THRESHOLD = 1
SLOWDOWN_RATE = 0.95
DEFAULT_SLOWDOWN_KICKIN_THRESHOLD = 1.5
@ -40,34 +46,39 @@ MERGE_PLAYPAUSE_BUTTONS = False
SYNC_ON_PAUSE = True # Client seek to global position - subtitles may disappear on some media players
#Usually there's no need to adjust these
FILENAME_STRIP_REGEX = u"[-~_\.\[\](): ]"
CONTROL_PASSWORD_STRIP_REGEX = u"[^a-zA-Z0-9\-]"
ROOM_NAME_STRIP_REGEX = u"^(\+)(?P<roomnamebase>.*)(:)(\w{12})$"
COMMANDS_UNDO = ["u", "undo", "revert"]
COMMANDS_LIST = ["l", "list", "users"]
COMMANDS_PAUSE = ["p", "play", "pause"]
COMMANDS_ROOM = ["r", "room"]
COMMANDS_HELP = ['help', 'h', '?', '/?', r'\?']
COMMANDS_CREATE = ['c','create']
COMMANDS_AUTH = ['a','auth']
MPC_MIN_VER = "1.6.4"
VLC_MIN_VERSION = "2.0.0"
VLC_INTERFACE_MIN_VERSION = "0.2.1"
CONTROLLED_ROOMS_MIN_VERSION = "1.3.0"
MPC_PATHS = [
r"C:\Program Files (x86)\MPC-HC\mpc-hc.exe",
r"C:\Program Files\MPC-HC\mpc-hc.exe",
r"C:\Program Files\MPC-HC\mpc-hc64.exe",
r"C:\Program Files\Media Player Classic - Home Cinema\mpc-hc.exe",
r"C:\Program Files\Media Player Classic - Home Cinema\mpc-hc64.exe",
r"C:\Program Files (x86)\Media Player Classic - Home Cinema\mpc-hc.exe",
r"C:\Program Files (x86)\K-Lite Codec Pack\Media Player Classic\mpc-hc.exe",
r"C:\Program Files\K-Lite Codec Pack\Media Player Classic\mpc-hc.exe",
r"C:\Program Files (x86)\Combined Community Codec Pack\MPC\mpc-hc.exe",
r"C:\Program Files\Combined Community Codec Pack\MPC\mpc-hc.exe",
r"C:\Program Files\MPC HomeCinema (x64)\mpc-hc64.exe",
r"c:\program files (x86)\mpc-hc\mpc-hc.exe",
r"c:\program files\mpc-hc\mpc-hc.exe",
r"c:\program files\mpc-hc\mpc-hc64.exe",
r"c:\program files\media player classic - home cinema\mpc-hc.exe",
r"c:\program files\media player classic - home cinema\mpc-hc64.exe",
r"c:\program files (x86)\media player classic - home cinema\mpc-hc.exe",
r"c:\program files (x86)\k-lite codec pack\media player classic\mpc-hc.exe",
r"c:\program files\k-lite codec pack\media Player classic\mpc-hc.exe",
r"c:\program files (x86)\combined community codec pack\mpc\mpc-hc.exe",
r"c:\program files\combined community codec pack\mpc\mpc-hc.exe",
r"c:\program files\mpc homecinema (x64)\mpc-hc64.exe",
]
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",
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"]
VLC_PATHS = [
r"C:\Program Files (x86)\VideoLAN\VLC\vlc.exe",
r"C:\Program Files\VideoLAN\VLC\vlc.exe",
r"c:\program files (x86)\videolan\vlc\vlc.exe",
r"c:\program files\videolan\vlc\vlc.exe",
"/usr/bin/vlc",
"/usr/bin/vlc-wrapper",
"/Applications/VLC.app/Contents/MacOS/VLC",
@ -89,14 +100,16 @@ MPC_LOCK_WAIT_TIME = 0.2
MPC_RETRY_WAIT_TIME = 0.01
MPC_MAX_RETRIES = 30
MPC_PAUSE_TOGGLE_DELAY = 0.05
MPV_NEWFILE_IGNORE_TIME = 1
MPV_LOCK_WAIT_TIME = 0.2
VLC_OPEN_MAX_WAIT_TIME = 15
VLC_MIN_PORT = 10000
VLC_MAX_PORT = 55000
#These are not changes you're looking for
STYLE_TABLIST = "QListWidget::item { color: black; border-style: solid; border-width: 1px; border-radius: 2px; } QListWidget::item:selected { background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(242, 248, 255, 255), stop:1 rgba(208, 229, 255, 255)); border-color: #84ACDD; } QListWidget::item:!selected { border-color: transparent; } QListWidget::item:!selected:hover { background: qlineargradient(spread:pad, x1:0, y1:1, x2:0, y2:0, stop:0 rgba(248, 248, 248, 255), stop:1 rgba(229, 229, 229, 255)); border-color: silver; }"
STYLE_SUBCHECKBOX = "QCheckBox, QLabel {{ margin-left: 8px; padding-left: 18px; background:url('{}') left no-repeat }}" #Graphic path
STYLE_SUBLABEL = "QCheckBox, QLabel {{ margin-left: 8px; padding-left: 14px; background:url('{}') left no-repeat }}" #Graphic path
STYLE_SUBCHECKBOX = "QCheckBox, QLabel {{ margin-left: 6px; padding-left: 21px; background:url('{}') left no-repeat }}" #Graphic path
STYLE_SUBLABEL = "QCheckBox, QLabel {{ margin-left: 6px; padding-left: 16px; background:url('{}') left no-repeat }}" #Graphic path
STYLE_ERRORLABEL = "QLabel { color : black; border-style: outset; border-width: 2px; border-radius: 7px; border-color: red; padding: 2px; background: #FFAAAA; }"
STYLE_SUCCESSLABEL = "QLabel { color : black; border-style: outset; border-width: 2px; border-radius: 7px; border-color: green; padding: 2px; background: #AAFFAA; }"
STYLE_NOTIFICATIONBOX = "Username { color: #367AA9; font-weight:bold; }"
@ -104,12 +117,19 @@ STYLE_USERNAME = "color: #367AA9; font-weight:bold;"
STYLE_ERRORNOTIFICATION = "color: red;"
STYLE_DIFFERENTITEM_COLOR = 'red'
STYLE_NOFILEITEM_COLOR = 'blue'
STYLE_NOTCONTROLLER_COLOR = 'grey'
MPLAYER_SLAVE_ARGS = ['-slave', '--hr-seek=always', '-nomsgcolor', '-msglevel', 'all=1:global=4:cplayer=4']
USERLIST_GUI_USERNAME_OFFSET = 21 # Pixels
MPLAYER_SLAVE_ARGS = ['-slave', '--hr-seek=always', '-nomsgcolor', '-msglevel', 'all=1:global=4:cplayer=4', '-af', 'scaletempo']
# --quiet works with both mpv 0.2 and 0.3
MPV_SLAVE_ARGS = ['--slave-broken', '--hr-seek=always', '--quiet', '--keep-open']
MPV_SLAVE_ARGS = ['--hr-seek=always', '--quiet', '--keep-open', '-af', 'scaletempo']
MPV_SLAVE_ARGS_WINDOWS = ['--slave-broken']
MPV_SLAVE_ARGS_NONWINDOWS = ['--input-terminal=no','--input-file=/dev/stdin']
MPV_SLAVE_ARGS_NEW = ['--term-playing-msg=<SyncplayUpdateFile>\nANS_filename=${filename}\nANS_length=${=length}\nANS_path=${path}\n</SyncplayUpdateFile>']
MPV_NEW_VERSION = False
VLC_SLAVE_ARGS = ['--extraintf=luaintf', '--lua-intf=syncplay', '--no-quiet', '--no-input-fast-seek',
'--play-and-pause']
'--play-and-pause', '--start-time=0']
VLC_SLAVE_NONOSX_ARGS = ['--no-one-instance', '--no-one-instance-when-started-from-file']
MPLAYER_ANSWER_REGEX = "^ANS_([a-zA-Z_-]+)=(.+)$|^(Exiting)\.\.\. \((.+)\)$"
VLC_ANSWER_REGEX = r"(?:^(?P<command>[a-zA-Z_]+)(?:\: )?(?P<argument>.*))"
@ -126,4 +146,6 @@ INVERTED_STATE_MARKER = "*"
ERROR_MESSAGE_MARKER = "*"
LOAD_SAVE_MANUALLY_MARKER = "!"
CONFIG_NAME_MARKER = ":"
CONFIG_VALUE_MARKER = "="
CONFIG_VALUE_MARKER = "="
USERITEM_CONTROLLER_ROLE = 0
USERITEM_READY_ROLE = 1

View File

@ -2,7 +2,8 @@
from syncplay import constants
en = {
"LANGUAGE" : "English",
# Client notifications
"config-cleared-notification" : "Settings cleared. Changes will be saved when you store a valid configuration.",
@ -16,6 +17,7 @@ en = {
"retrying-notification" : "%s, Retrying in %d seconds...", # Seconds
"rewind-notification" : "Rewinded due to time difference with <{}>", # User
"fastforward-notification" : "Fast-forwarded due to time difference with <{}>", # User
"slowdown-notification" : "Slowing down due to time difference with <{}>", # User
"revert-notification" : "Reverting speed back to normal",
@ -31,8 +33,12 @@ en = {
"playing-notification" : "<{}> is playing '{}' ({})", # User, file, duration
"playing-notification/room-addendum" : " in room: '{}'", # Room
"identifying-as-controller-notification" : u"Identifying as room controller with password '{}'...",
"failed-to-identify-as-controller-notification" : u"<{}> failed to identify as a room controller.",
"authenticated-as-controller-notification" : u"<{}> authenticated as a room controller",
"file-different-notification" : "File you are playing appears to be different from <{}>'s", # User
"file-differences-notification" : "Your file differs in the following way(s): ",
"file-differences-notification" : "Your file differs in the following way(s): ", # controlPassword
"room-files-not-same" : "Not all files played in the room are the same",
"alone-in-the-room": "You're alone in the room",
@ -53,10 +59,13 @@ en = {
"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/help" : "\th - this help",
"commandlist-notification/create" : "\tc [name] - create controlled room using name of current room",
"commandlist-notification/auth" : "\ta [password] - authenticate as room controller with controller password",
"syncplay-version-notification" : "Syncplay version: {}", # syncplay.version
"more-info-notification" : "More info available at: {}", # projectURL
"gui-data-cleared-notification" : "Syncplay has cleared the path and window state data used by the GUI.",
"language-changed-msgbox-label" : "Language will be changed when you run Syncplay.",
"vlc-version-mismatch": "Warning: You are running VLC version {}, but Syncplay is designed to run on VLC {} and above.", # VLC version, VLC min version
"vlc-interface-version-mismatch": "Warning: You are running version {} of the Syncplay interface module for VLC, but Syncplay is designed to run with version {} and above.", # VLC interface version, VLC interface min version
@ -71,6 +80,7 @@ en = {
"server-timeout-error" : "Connection with server timed out",
"mpc-slave-error" : "Unable to start MPC in slave mode!",
"mpc-version-insufficient-error" : "MPC version not sufficient, please use `mpc-hc` >= `{}`",
"mpv-version-error" : "Syncplay is not compatible with this version of mpv. Please use a different version of mpv (e.g. Git HEAD).",
"player-file-open-error" : "Player failed opening file",
"player-path-error" : "Player path is not set properly",
"hostname-empty-error" : "Hostname can't be empty",
@ -82,6 +92,12 @@ en = {
"unable-to-start-client-error" : "Unable to start client",
"player-path-config-error": "Player path is not set properly",
"no-file-path-config-error" :"File must be selected before starting your player",
"no-hostname-config-error": "Hostname can't be empty",
"invalid-port-config-error" : "Port must be valid",
"empty-value-config-error" : "{} can't be empty", # Config option
"not-json-error" : "Not a json encoded string\n",
"hello-arguments-error" : "Not enough Hello arguments\n",
"version-mismatch-error" : "Mismatch between versions of client and server\n",
@ -105,7 +121,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 (en/pl)',
"language-argument" :'language for Syncplay messages (de/en/pl/ru)',
"version-argument" : 'prints your version',
"version-message" : "You're using Syncplay version {} ({})",
@ -127,6 +143,7 @@ en = {
"more-title" : "Show more settings",
"slowdown-threshold-label" : "Slow down threshold:",
"rewind-threshold-label" : "Rewind threshold:",
"fastforward-threshold-label" : "Fast-forward threshold:",
"never-rewind-value" : "Never",
"seconds-suffix" : " secs",
"privacy-sendraw-option" : "Send raw",
@ -135,7 +152,7 @@ en = {
"filename-privacy-label" : "Filename information:",
"filesize-privacy-label" : "File size information:",
"slowondesync-label" : "Slow down on minor desync (not supported on MPC-HC)",
"dontslowdownwithme-label" : "Never slow down or rewind others",
"dontslowdownwithme-label" : "Never slow down or rewind others (experimental)",
"pauseonleave-label" : "Pause when user leaves (e.g. if they are disconnected)",
"forceguiprompt-label" : "Don't always show this dialog", # (Inverted)
"nostore-label" : "Don't store this configuration", # (Inverted)
@ -143,16 +160,21 @@ en = {
"showosdwarnings-label" : "Include warnings (e.g. when files are different)",
"showsameroomosd-label" : "Include events in your room",
"shownoncontrollerosd-label" : "Include events from non-controllers in controlled rooms",
"showdifferentroomosd-label" : "Include events in other rooms",
"showslowdownosd-label" :"Include slowing down / reverting notification",
"showcontactinfo-label" : "Show contact info box",
"showbuttonlabels-label" : "Show labels on buttons",
"showtooltips-label" : "Show tooltips",
"language-label" : "Language",
"showdurationnotification-label" : "Warn about media duration mismatches",
"basics-label" : "Basics",
"sync-label" : "Sync",
"sync-lagging-title" : "If others are lagging behind...",
"sync-other-title" : "Other sync options",
"messages-label" : "Messages",
"messages-osd-title" : "On-screen Display settings",
"messages-other-title" : "Other display settings",
"privacy-label" : "Privacy",
"privacy-title" : "Privacy settings",
"help-label" : "Help",
"reset-label" : "Restore defaults",
@ -166,10 +188,12 @@ en = {
"undoseek-guibuttonlabel" : "Undo seek",
"togglepause-guibuttonlabel" : "Toggle pause",
"play-guibuttonlabel" : "Play",
"pause-guibuttonlabel" : "Play",
"pause-guibuttonlabel" : "Pause",
"roomuser-heading-label" : "Room / User",
"fileplayed-heading-label" : "File being played",
"size-heading-label" : "Size",
"duration-heading-label" : "Length",
"filename-heading-label" : "Filename",
"notifications-heading-label" : "Notifications",
"userlist-heading-label" : "List of who is playing what",
"othercommands-heading-label" : "Other commands",
@ -183,12 +207,24 @@ en = {
"exit-menu-label" : "E&xit",
"advanced-menu-label" : "&Advanced",
"setoffset-menu-label" : "Set &offset",
"createcontrolledroom-menu-label" : "&Create controlled room",
"identifyascontroller-menu-label" : "&Identify as room controller",
"help-menu-label" : "&Help",
"userguide-menu-label" : "Open user &guide",
"setoffset-msgbox-label" : "Set offset",
"offsetinfo-msgbox-label" : "Offset (see http://syncplay.pl/guide/ for usage instructions):",
"createcontrolledroom-msgbox-label" : "Create controlled room",
"controlledroominfo-msgbox-label" : "Enter name of controlled room\r\n(see http://syncplay.pl/guide/ for usage instructions):",
"identifyascontroller-msgbox-label" : "Identify as Room Controller",
"identifyinfo-msgbox-label" : "Enter controller password for this room\r\n(see http://syncplay.pl/guide/ for usage instructions):",
"megabyte-suffix" : " MB",
# Tooltips
"host-tooltip" : "Hostname or IP to connect to, optionally including port (e.g. syncplay.pl:8999). Only synchronised with people on same server/port.",
@ -202,27 +238,30 @@ en = {
"more-tooltip" : "Display less frequently used settings.",
"slowdown-threshold-tooltip" : "Time ahead of slowest client before temporarily reducing playback speed (default: {} secs).".format(constants.DEFAULT_SLOWDOWN_KICKIN_THRESHOLD),
"rewind-threshold-tooltip" : "Time ahead slowest client before seeking to get back in sync (default: {} secs).".format(constants.DEFAULT_REWIND_THRESHOLD),
"fastforward-threshold-tooltip" : "Time behind room controller before seeking to get back in sync (default: {} secs).".format(constants.DEFAULT_FASTFORWARD_THRESHOLD),
"filename-privacy-tooltip" : "Privacy mode for sending currently playing filename to server.",
"filesize-privacy-tooltip" : "Privacy mode for sending size of currently playing file to server.",
"privacy-sendraw-tooltip" : "Send this information without obfuscation. This is the default option with most functionality.",
"privacy-sendhashed-tooltip" : "Send a hashed version of the information, making it less visible to other clients.",
"privacy-dontsend-tooltip" : "Do not send this information to the server. This provides for maximum privacy.",
"slowondesync-tooltip" : "Reduce playback rate temporarily when needed to bring you back in sync with other viewers. Not supported on MPC-HC.",
"dontslowdownwithme-tooltip" : "Means others do not get slowed down or rewinded if your playback is lagging.",
"dontslowdownwithme-tooltip" : "Means others do not get slowed down or rewinded if your playback is lagging. Useful for room controllers.",
"pauseonleave-tooltip" : "Pause playback if you get disconnected or someone leaves from your room.",
"rewindondesync-label" : "Rewind on major desync (highly recommended)",
"fastforwardondesync-label" : "Fast-forward if lagging behind (recommended)",
"forceguiprompt-tooltip" : "Configuration dialogue is not shown when opening a file with Syncplay.", # (Inverted)
"nostore-tooltip" : "Run Syncplay with the given configuration, but do not permanently store the changes.", # (Inverted)
"rewindondesync-tooltip" : "Jump back when needed to get back in sync. Disabling this option can result in major desyncs!",
"fastforwardondesync-tooltip" : "Jump forward when out of sync with room controller (or your pretend position if 'Never slow down or rewind others' enabled).",
"showosd-tooltip" : "Sends Syncplay messages to media player OSD.",
"showosdwarnings-tooltip" : "Show warnings if playing different file, alone in room.",
"showsameroomosd-tooltip" : "Show OSD notifications for events relating to room user is in.",
"shownoncontrollerosd-tooltip" : "Show OSD notifications for events relating to non-controllers who are in controllerd rooms.",
"showdifferentroomosd-tooltip" : "Show OSD notifications for events relating to room user is not in.",
"showslowdownosd-tooltip" :"Show notifications of slowing down / reverting on time difference.",
"showcontactinfo-tooltip" : "Show information box about contacting Syncplay developers in main Syncplay window.",
"showbuttonlabels-tooltip" : "Show the text alongside the icons for buttons in the main UI.",
"showtooltips-tooltip" : "Show tooltip help messages when you mouseover an input element in Syncplay.",
"showdurationnotification-tooltip" : "Useful for when a segment in a multi-part file is missing, but can result in false positives.",
"language-tooltip" : u"Language to be used by Syncplay.",
"help-tooltip" : "Opens the Syncplay.pl user guide.",
"reset-tooltip" : "Reset all settings to the default configuration.",
@ -247,6 +286,7 @@ en = {
"welcome-server-notification" : "Welcome to Syncplay server, ver. {0}", # version
"client-connected-room-server-notification" : "{0}({2}) connected to room '{1}'", # username, host, room
"client-left-server-notification" : "{0} left server", # name
"no-salt-notification" : "PLEASE NOTE: To allow room control passwords generated by this server instance to still work when the server is restarted, please add the following command line argument when running the Syncplay server in the future: --salt {}", #Salt
# Server arguments
@ -255,11 +295,10 @@ en = {
"server-port-argument" : 'server TCP port',
"server-password-argument" : 'server password',
"server-isolate-room-argument" : 'should rooms be isolated?',
"server-salt-argument" : "random string used to generate controlled room passwords",
"server-motd-argument": "path to file from which motd will be fetched",
"server-messed-up-motd-unescaped-placeholders": "Message of the Day has unescaped placeholders. All $ signs should be doubled ($$).",
"server-messed-up-motd-too-long": "Message of the Day is too long - maximum of {} chars, {} given.",
"server-irc-verbose": "Should server actively report changes in rooms",
"server-irc-config": "Path to irc bot config files",
# Server errors
"unknown-command-server-error" : "Unknown command {}", # message
@ -269,13 +308,12 @@ en = {
"password-required-server-error" : "Password required",
"wrong-password-server-error" : "Wrong password supplied",
"hello-server-error" : "Not enough Hello arguments",
"version-mismatch-server-error" : "Mismatch between versions of client and server"
}
pl = {
"LANGUAGE" : "Polski", # (Polish)
# Client notifications
"connection-attempt-notification" : u"Próba połączenia z {}:{}", # Port, IP
"reconnection-attempt-notification" : u"Połączenie z serwerem zostało przerwane, ponowne łączenie",
@ -311,14 +349,604 @@ pl = {
"server-timeout-error" : u"Przekroczono czas oczekiwania na odpowiedź serwera"
}
ru = {
"LANGUAGE" : u"Русский", # (Russian)
# Client notifications
"config-cleared-notification" : u"Настройки сброшены. Изменения вступят в силу при сохранении корректной конфигурации.",
"relative-config-notification" : u"Загружен(ы) файл(ы) относительной конфигурации: {}",
"connection-attempt-notification" : u"Подключение к {}:{}", # Port, IP
"reconnection-attempt-notification" : u"Соединение с сервером потеряно, переподключение",
"disconnection-notification" : u"Отключились от сервера",
"connection-failed-notification" : u"Не удалось подключиться к серверу",
"connected-successful-notification" : u"Соединение с сервером установлено",
"retrying-notification" : u"%s, следующая попытка через %d секунд(ы)...", # Seconds
"rewind-notification" : u"Перемотано из-за разницы во времени с <{}>", # User
"slowdown-notification" : u"Воспроизведение замедлено из-за разницы во времени с <{}>", # User
"revert-notification" : u"Возвращаемся к нормальной скорости воспроизведения",
"pause-notification" : u"<{}> приостановил(а) воспроизведение", # User
"unpause-notification" : u"<{}> возобновил(а) воспроизведение", # User
"seek-notification" : u"<{}> перемотал с {} на {}", # User, from time, to time
"current-offset-notification" : u"Текущее смещение: {} секунд(ы)", # Offset
"room-join-notification" : u"<{}> зашел(зашла) в комнату: '{}'", # User
"left-notification" : u"<{}> покинул(а) комнату", # User
"left-paused-notification" : u"<{}> покинул(а) комнату, <{}> приостановил(а) воспроизведение", # User who left, User who paused
"playing-notification" : u"<{}> включил '{}' ({})", # User, file, duration
"playing-notification/room-addendum" : u" в комнате: '{}'", # Room
"file-different-notification" : u"Вероятно, файл, который Вы смотрите, отличается от того, который смотрит <{}>.", # User
"file-differences-notification" : u"Ваш файл отличается: ",
"room-files-not-same" : u"Не все пользователи в этой комнате смотрят один и тот же файл.",
"alone-in-the-room" : u"В этой комнате кроме Вас никого нет.",
"different-filesize-notification" : u" (размер Вашего файла не совпадает с размером их файла!)",
"file-played-by-notification" : u"Файл: {} просматривают:", # File
"notplaying-notification" : u"Люди, которые не смотрят ничего:",
"userlist-room-notification" : u"В комнате '{}':", # Room
"mplayer-file-required-notification" : u"Для использования Syncplay с mplayer необходимо передать файл в качестве параметра",
"mplayer-file-required-notification/example" : u"Пример использования: syncplay [options] [url|path/]filename",
"mplayer2-required" : u"Syncplay не совместим с MPlayer 1.x, пожалуйста, используйте mplayer2 или mpv",
"unrecognized-command-notification" : u"Неизвестная команда.",
"commandlist-notification" : u"Доступные команды:",
"commandlist-notification/room" : u"\tr [name] - сменить комнату",
"commandlist-notification/list" : u"\tl - показать список пользователей",
"commandlist-notification/undo" : u"\tu - отменить последнюю перемотку",
"commandlist-notification/pause" : u"\tp - вкл./выкл. паузу",
"commandlist-notification/seek" : u"\t[s][+-]time - перемотать к заданному моменту времени, если не указан + или -, то время считается абсолютным (от начала файла) в секундах или мин:сек",
"commandlist-notification/help" : u"\th - помощь",
"syncplay-version-notification" : u"Версия Syncplay: {}", # syncplay.version
"more-info-notification" : u"Больше информации на {}", # projectURL
"gui-data-cleared-notification" : u"Syncplay очистил путь и информацию о состоянии окна, использованного GUI.",
"vlc-version-mismatch" : u"Внимание: Вы используете VLC устаревшей версии {}. К сожалению, Syncplay способен работать с VLC {} и выше.", # VLC version, VLC min version
"vlc-interface-version-mismatch" : u"Внимание: В используете модуль интерфейса Syncplay устаревшей версии {} для VLC. К сожалению, Syncplay способен работать с версией {} и выше.", # VLC interface version, VLC interface min version
"vlc-interface-oldversion-ignored" : u"Внимание: Syncplay обнаружил, что старая версия модуля интерфейса Syncplay для VLC уже установлена в директорию VLC. По существу, если Вы используете VLC 2.0, то предпочтение будет отдано файлу syncplay.lua, содержащемуся в директории Syncplay, но в таком случае другие пользовательские скрипты и расширения интерфейса не будут работать. Пожалуйста, обратитесь к Руководству Пользователя Syncplay (http://syncplay.pl/guide/) за инструкциями о том, как установить syncplay.lua.",
"vlc-interface-not-installed" : u"Внимание: Модуль интерфейса Syncplay для VLC не обнаружен в директории VLC. По существу, если Вы используете VLC 2.0, то VLC будет использовать модуль syncplay.lua из директории Syncplay, но в таком случае другие пользовательские скрипты и расширения интерфейса не будут работать. Пожалуйста, обратитесь к Руководству Пользователя Syncplay (http://syncplay.pl/guide/) за инструкциями о том, как установить syncplay.lua.",
# Client prompts
"enter-to-exit-prompt" : u"Для выхода нажмите Enter\n",
# Client errors
"missing-arguments-error" : u"Некоторые необходимые аргументы отсутствуют, обратитесь к --help",
"server-timeout-error" : u"Подключение к серверу превысило лимит времени",
"mpc-slave-error" : u"Невозможно запустить MPC в slave режиме!",
"mpc-version-insufficient-error" : u"Версия MPC слишком старая, пожалуйста, используйте `mpc-hc` >= `{}`",
"mpv-version-error" : u"Syncplay не совместим с данной версией mpv. Пожалуйста, используйте другую версию mpv (лучше свежайшую).",
"player-file-open-error" : u"Проигрыватель не может открыть файл.",
"player-path-error" : u"Путь к проигрывателю задан неверно.",
"hostname-empty-error" : u"Имя пользователя не может быть пустым.",
"empty-error" : u"{} не может быть пустым.", # Configuration
"media-player-error" : u"Ошибка Media player: \"{}\"", # Error line
"unable-import-gui-error" : u"Невозможно импортировать библиотеки GUI (графического интерфейса). Необходимо установить PySide, иначе графический интерфейс не будет работать.",
"arguments-missing-error" : u"Некоторые необходимые аргументы отсутствуют, обратитесь к --help",
"unable-to-start-client-error" : u"Невозможно запустить клиент",
"not-json-error" : u"Не является закодированной json-строкой\n",
"hello-arguments-error" : u"Не хватает аргументов Hello\n",
"version-mismatch-error" : u"Конфликт версий между клиентом и сервером\n",
"vlc-failed-connection" : u"Ошибка подключения к VLC. Если у Вас не установлен syncplay.lua, то обратитесь к http://syncplay.pl/LUA/ за инструкциями.",
"vlc-failed-noscript" : u"VLC сообщает, что скрипт интерфейса syncplay.lua не установлен. Пожалуйста, обратитесь к http://syncplay.pl/LUA/ за инструкциями.",
"vlc-failed-versioncheck" : u"Данная версия VLC не поддерживается Syncplay. Пожалуйста, используйте VLC версии 2 или выше.",
"vlc-failed-other" : u"Во время загрузки скрипта интерфейса syncplay.lua в VLC произошла следующая ошибка: {}", # Syncplay Error
# Client arguments
"argument-description" : u'Решение для синхронного воспроизведения в VLC, MPlayer или MPC-HC через Интернет.',
"argument-epilog" : u'Если параметр не будет передан, то будет использоваться значение, указанное в _config.',
"nogui-argument" : u'не использовать GUI',
"host-argument" : u'адрес сервера',
"name-argument" : u'желательное имя пользователя',
"debug-argument" : u'режим отладки',
"force-gui-prompt-argument" : u'показать окно настройки',
"no-store-argument" : u'не сохранять данные в .syncplay',
"room-argument" : u'начальная комната',
"password-argument" : u'пароль для доступа к серверу',
"player-path-argument" : u'путь к исполняемому файлу Вашего проигрывателя',
"file-argument" : u'воспроизводимый файл',
"args-argument" : u'параметры проигрывателя; если нужно передать параметры, начинающиеся с - , то сначала пишите \'--\'',
"clear-gui-data-argument" : u'сбрасывает путь и данные о состоянии окна GUI, хранимые как QSettings',
"language-argument" : u'язык сообщений Syncplay (en/pl/ru)',
"version-argument" : u'выводит номер версии',
"version-message" : u"Вы используете Syncplay версии {} ({})",
# Client labels
"config-window-title" : u"Настройка Syncplay",
"connection-group-title" : u"Параметры подключения",
"host-label" : u"Адрес сервера: ",
"name-label" : u"Имя пользователя (не обязательно):",
"password-label" : u"Пароль к серверу (если требуется):",
"room-label" : u"Начальная комната: ",
"media-setting-title" : u"Параметры проигрывателя",
"executable-path-label" : u"Путь к проигрывателю:",
"media-path-label" : u"Путь к видеофайлу:",
"browse-label" : u"Выбрать",
"more-title" : u"Больше настроек",
"slowdown-threshold-label" : u"Предел для замедления:",
"rewind-threshold-label" : u"Предел для перемотки:",
"never-rewind-value" : u"Никогда",
"seconds-suffix" : u" секунд(ы)",
"privacy-sendraw-option" : u"отпр. как есть",
"privacy-sendhashed-option" : u"отпр. хэш",
"privacy-dontsend-option" : u"не отпр.",
"filename-privacy-label" : u"Имя файла:",
"filesize-privacy-label" : u"Размер файла:",
"slowondesync-label" : u"Замедлять при небольших рассинхронизациях (не поддерживаетя в MPC-HC)",
"rewindondesync-label" : u"Перемотка при больших рассинхронизациях (настоятельно рекомендуется)",
"dontslowdownwithme-label" : u"Никогда не замедлять или перематывать видео другим",
"pauseonleave-label" : u"Приостанавливать, когда кто-то уходит (например, отключился)",
"forceguiprompt-label" : u"Не показывать больше этот диалог", # (Inverted)
"nostore-label" : u"Не сохранять текущую конфигурацию", # (Inverted)
"showosd-label" : u"Включить экранные сообщения (поверх видео)",
"showosdwarnings-label" : u"Показывать предупреждения (напр., когда файлы не совпадают)",
"showsameroomosd-label" : u"Показывать события Вашей комнаты",
"showdifferentroomosd-label" : u"Показывать события других комнат",
"showslowdownosd-label" : u"Показывать уведомления о замедлении/перемотке",
"showcontactinfo-label" : u"Отображать контактную информацию разработчиков",
"showdurationnotification-label" : u"Предупреждать о несовпадении продолжительности видео",
"basics-label" : u"Основное",
"sync-label" : u"Синхронизация",
"messages-label" : u"Сообщения",
"privacy-label" : u"Приватность",
"help-label" : u"Помощь",
"reset-label" : u"Сброс настроек",
"run-label" : u"Запустить Syncplay",
"storeandrun-label" : u"Сохранить настройки и зап. Syncplay",
"contact-label" : u"Есть идея, нашли ошибку или хотите оставить отзыв? Пишите на <a href=\"mailto:dev@syncplay.pl\">dev@syncplay.pl</a>, в <a href=\"https://webchat.freenode.net/?channels=#syncplay\">IRC канал #Syncplay</a> на irc.freenode.net или <a href=\"https://github.com/Uriziel/syncplay/issues/new\">задавайте вопросы через GitHub</a>. Кроме того, заходите на <a href=\"http://syncplay.pl/\">http://syncplay.pl/</a> за инорфмацией, помощью и обновлениями!",
"joinroom-guibuttonlabel" : u"Зайти в комнату",
"seektime-guibuttonlabel" : u"Перемотать",
"undoseek-guibuttonlabel" : u"Отменить перемотку",
"togglepause-guibuttonlabel" : u"Вкл./выкл. паузу",
"play-guibuttonlabel" : u"Play",
"pause-guibuttonlabel" : u"Пауза",
"roomuser-heading-label" : u"Комната / Пользователь",
"notifications-heading-label" : u"Уведомления",
"userlist-heading-label" : u"Кто что смотрит",
"othercommands-heading-label" : u"Другие команды",
"room-heading-label" : u"Комната",
"seek-heading-label" : u"Перемотка",
"browseformedia-label" : u"Выбрать видеофайл",
"file-menu-label" : u"&Файл", # & precedes shortcut key
"openmedia-menu-label" : u"&Открыть видеофайл",
"exit-menu-label" : u"&Выход",
"advanced-menu-label" : u"&Дополнительно",
"setoffset-menu-label" : u"Установить &смещение",
"help-menu-label" : u"&Помощь",
"userguide-menu-label" : u"&Руководство Пользователя",
"setoffset-msgbox-label" : u"Установить смещение",
"offsetinfo-msgbox-label" : u"Смещение (см. как использовать на http://syncplay.pl/guide/):",
# Tooltips
"host-tooltip" : u"Имя или IP-адрес, к которому будет произведено подключение, может содержать номер порта (напр., syncplay.pl:8999). Синхронизация возможна только в рамках одного сервера/порта.",
"name-tooltip" : u"Имя, под которым Вы будете известны. Регистриция не требуется, так что имя пользователя можно легко сменить в любой момент. Будет сгенерировано случайным образом, если не указать.",
"password-tooltip" : u"Пароли нужны для подключения к приватным серверам.",
"room-tooltip" : u"Комната, в которую Вы попадете сразу после подключения. Можно не указывать. Синхронизация возможна только между людьми в одной и той же комнате.",
"executable-path-tooltip" : u"Расположение Вашего видеопроигрывателя (MPC-HC, VLC, mplayer2 или mpv).",
"media-path-tooltip" : u"Расположение видеофайла или потока для просмотра. Обязательно для mpv и mplayer2.",
"more-tooltip" : u"Показать дополнительные настройки.",
"slowdown-threshold-tooltip" : u"Отставание самого медленного клиента, необходимое для временного уменьшения скорости видео (по умолчанию: {} сек.).".format(constants.DEFAULT_SLOWDOWN_KICKIN_THRESHOLD),
"rewind-threshold-tooltip" : u"Отставание самого медленного клиента, необходимое для перемотки назад в целях синхронизации (по умолчанию: {} сек.).".format(constants.DEFAULT_REWIND_THRESHOLD),
"filename-privacy-tooltip" : u"Режим приватности для передачи имени воспроизводимого файла на сервер.",
"filesize-privacy-tooltip" : u"Режим приватности для передачи размера воспроизводимого файла на сервер.",
"privacy-sendraw-tooltip" : u"Отправляет эту информацию без шифрования. Рекомендуемая опция с наибольшей функциональностью.",
"privacy-sendhashed-tooltip" : u"Отправляет хэш-сумму этой информации, делая ее невидимой для других пользователей.",
"privacy-dontsend-tooltip" : u"Не отправлять эту информацию на сервер. Предоставляет наибольшую приватность.",
"slowondesync-tooltip" : u"Временно уменьшить скорость воспроизведения в целях синхронизации с другими зрителями. Не поддерживается в MPC-HC.",
"dontslowdownwithme-tooltip" : u"Ваши лаги не будут влиять на других зрителей.",
"pauseonleave-tooltip" : u"Приостановить воспроизведение, если Вы покинули комнату или кто-то из зрителей отключился от сервера.",
"forceguiprompt-tooltip" : u"Окно настройки не будет отображаться при открытии файла в Syncplay.", # (Inverted)
"nostore-tooltip" : u"Запустить Syncplay с данной конфигурацией, но не сохранять изменения навсегда.", # (Inverted)
"rewindondesync-tooltip" : u"Перематывать назад, когда это необходимо для синхронизации. Отключение этой опции может привести к большим рассинхронизациям!",
"showosd-tooltip" : u"Отправлять сообщения Syncplay в видеопроигрыватель и отображать их поверх видео (OSD - On Screen Display).",
"showosdwarnings-tooltip" : u"Показывать OSC-предупреждения, если проигрываются разные файлы или если Вы в комнате больше никого нет.",
"showsameroomosd-tooltip" : u"Показывать OSD-уведомления о событиях, относящихся к комнате, в которой Вы находитесь.",
"showdifferentroomosd-tooltip" : u"Показывать OSD-уведомления о событиях, относящихся к любым другим комнатам.",
"showslowdownosd-tooltip" : u"Показывать уведомления о замедлении или перемотке в целях синхронизации.",
"showcontactinfo-tooltip" : u"Показывать информационных блок с контактной информацией разработчиков Syncplay на главном окне Syncplay.",
"showdurationnotification-tooltip" : u"Полезно, когда сегмент составного файла отсутствует. Возможны ложные срабатывания.",
"help-tooltip" : u"Открыть Руководство Пользователя на Syncplay.pl.",
"reset-tooltip" : u"Сбрасывает все настройки Syncplay в начальное состояние.",
"togglepause-tooltip" : u"Приостановить/продолжить просмотр.",
"play-tooltip" : u"Продолжить просмотр.",
"pause-tooltip" : u"Приостановить просмотр.",
"undoseek-tooltip" : u"Перейти к тому месту, которое Вы просматривали до перемотки.",
"joinroom-tooltip" : u"Покинуть комнату и зайти в другую, указанную комнату.",
"seektime-tooltip" : u"Перемотать к определенному моменту времени (указывать в секундах или мин:сек). Используйте +/-, чтобы перемотать вперед/назад относительно настоящего момента.",
# In-userlist notes (GUI)
"differentsize-note" : u"Размер файла не совпадает!",
"differentsizeandduration-note" : u"Размер и продолжительность файла не совпадают!",
"differentduration-note" : u"Продолжительность файла не совпадает!",
"nofile-note" : u"(Ничего не воспроизводим)",
# Server messages to client
"new-syncplay-available-motd-message" : u"<NOTICE> Вы используете Syncplay версии {}. Доступна более новая версия на http://syncplay.pl/ . </NOTICE>", # ClientVersion
# Server arguments
"server-argument-description" : u'Решение для синхронного воспроизведения в VLC, MPlayer или MPC-HC через Интернет. Серверная часть',
"server-argument-epilog" : u'Если параметр не будет передан, то будет использоваться значение, указанное в _config.',
"server-port-argument" : u'номер TCP порта сервера',
"server-password-argument" : u'пароль к серверу',
"server-isolate-room-argument" : u'должны ли комнаты быть изолированными?',
"server-motd-argument" : u"путь к файлу, из которого будет извлекаться MOTD-сообщение",
"server-messed-up-motd-unescaped-placeholders" : u"MOTD-сообщение содержит неэкранированные спец.символы. Все знаки $ должны быть продублированы ($$).",
"server-messed-up-motd-too-long" : u"MOTD-сообщение слишком длинное: максимальная длина - {} символ(ов), текущая длина - {} символ(ов).",
# Server errors
"unknown-command-server-error" : u"Неизвестная команда: {}", # message
"not-json-server-error" : u"Не является закодированной json-строкой: {}", # message
"not-known-server-error" : u"Данную команду могут выполнять только авторизованные пользователи.",
"client-drop-server-error" : u"Клиент отключен с ошибкой: {} -- {}", # host, error
"password-required-server-error" : u"Необходимо указать пароль.",
"wrong-password-server-error" : u"Указан неверный пароль.",
"hello-server-error" : u"Не хватает аргументов Hello.",
}
de = {
"LANGUAGE" : u"Deutsch", # (German)
# Client notifications
"config-cleared-notification" : u"Settings cleared. Changes will be saved when you store a valid configuration.", # TODO: Translate into German
"relative-config-notification" : u"Relative Konfigurationsdatei(en) geladen: {}",
"connection-attempt-notification" : u"Versuche zu verbinden nach {}:{}", # Port, IP
"reconnection-attempt-notification" : u"Verbindung zum Server verloren, versuche erneut",
"disconnection-notification" : u"Verbindung zum Server beendet",
"connection-failed-notification" : u"Verbindung zum Server fehlgeschlagen",
"connected-successful-notification" : u"Erfolgreich mit Server verbunden",
"retrying-notification" : u"%s, versuche erneut in %d Sekunden...", # Seconds
"rewind-notification" : u"Zurückgespult wegen Zeitdifferenz mit <{}>", # User
"fastforward-notification" : u"Fast-forwarded due to time difference with <{}>", # User # TODO: Translate into German
"slowdown-notification" : u"Verlangsamt wegen Zeitdifferenz mit <{}>", # User
"revert-notification" : u"Normalgeschwindigkeit",
"pause-notification" : u"<{}> pausierte", # User
"unpause-notification" : u"<{}> startete", # User
"seek-notification" : u"<{}> sprang von {} nach {}", # User, from time, to time
"current-offset-notification" : u"Aktueller Offset: {} Sekunden", # Offset
"room-join-notification" : u"<{}> hat den Raum '{}' betreten", # User
"left-notification" : u"<{}> ist gegangen", # User
"left-paused-notification" : u"<{}> left, <{}> paused", # User who left, User who paused # TODO: Translate into German
"playing-notification" : u"<{}> spielt '{}' ({})", # User, file, duration
"playing-notification/room-addendum" : u" in Raum: '{}'", # Room
"identifying-as-controller-notification" : u"Identifying as room controller with password '{}'...", # TODO: Translate into German
"failed-to-identify-as-controller-notification" : u"<{}> failed to identify as a room controller.", # TODO: Translate into German
"authenticated-as-controller-notification" : u"<{}> authenticated as a room controller", # TODO: Translate into German
"file-different-notification" : u"Deine Datei scheint sich von <{}>s zu unterscheiden", # User
"file-differences-notification" : u"Deine Datei unterscheidet sich auf folgende Art: ", # controlPassword
"room-files-not-same" : u"Nicht alle Dateien im Raum sind gleich",
"alone-in-the-room": u"Du bist alleine im Raum",
"different-filesize-notification" : u" (ihre Dateigröße ist anders als deine!)",
"file-played-by-notification" : u"Datei: {} wird gespielt von:", # File
"notplaying-notification" : u"Personen im Raum, die keine Dateien spielen:",
"userlist-room-notification" : u"In Raum '{}':", # Room
"mplayer-file-required-notification" : u"Syncplay für mplayer benötigt eine Datei-Angabe beim Start",
"mplayer-file-required-notification/example" : u"Nutzungsbeispiel: syncplay [optionen] [url|pfad/]dateiname",
"mplayer2-required" : u"Syncplay ist inkompatibel zu MPlayer 1.x, bitte nutze MPlayer2 oder mpv",
"unrecognized-command-notification" : u"Unbekannter Befehl",
"commandlist-notification" : u"Verfügbare Befehle:",
"commandlist-notification/room" : u"\tr [Name] - Raum ändern",
"commandlist-notification/list" : u"\tl - Nutzerliste anzeigen",
"commandlist-notification/undo" : u"\tu - Letzter Zeitsprung rückgängig",
"commandlist-notification/pause" : u"\tp - Pausieren / weiter",
"commandlist-notification/seek" : u"\t[s][+-]Zeit - zu einer bestimmten Zeit spulen, ohne + oder - wird als absolute Zeit gewertet; Angabe in Sekungen oder Minuten:Sekunden",
"commandlist-notification/help" : u"\th - Diese Hilfe",
"commandlist-notification/create" : u"\tc [name] - create controlled room using name of current room", # TODO: Translate into German
"commandlist-notification/auth" : u"\ta [password] - authenticate as room controller with controller password", # TODO: Translate into German
"syncplay-version-notification" : u"Syncplay Version: {}", # syncplay.version
"more-info-notification" : u"Weitere Informationen auf: {}", # projectURL
"gui-data-cleared-notification" : u"Syncplay hat die Pfad und Fensterdaten der Syncplay-GUI zurückgesetzt.",
"language-changed-msgbox-label" : u"Language will be changed when you run Syncplay.", # TODO: Translate into German
"vlc-version-mismatch": u"Warnung: Du nutzt VLC Version {}, aber Syncplay wurde für VLC ab Version {} entwickelt.", # VLC version, VLC min version
"vlc-interface-version-mismatch": u"Warnung: Du nutzt Version {} des VLC-Syncplay Interface-Moduls, Syncplay benötigt aber mindestens Version {}.", # VLC interface version, VLC interface min version
"vlc-interface-oldversion-ignored": u"Warnung: Syncplay hat bemerkt, dass eine alte Version des Syncplay Interface-Moduls für VLC im VLC-Verzeichnis installiert ist. Daher wird, wenn du VLC 2.0 nutzt, die syncplay.lua die mit Syncplay mitgeliefert wurde, verwendet. Dies bedeutet allerdings, dass keine anderen Interface-Skripts und Erweiterungen geladen werden. In der Syncplay-Anleitung unter http://syncplay.pl/guide/ [Englisch] findest du Details zur Installation des syncplay.lua-Skripts.",
"vlc-interface-not-installed": u"Warnung: Es wurde kein Syncplay Interface-Modul für VLC im VLC-Verzeichnis gefunden. Daher wird, wenn du VLC 2.0 nutzt, die syncplay.lua die mit Syncplay mitgeliefert wurde, verwendet. Dies bedeutet allerdings, dass keine anderen Interface-Skripts und Erweiterungen geladen werden. In der Syncplay-Anleitung unter http://syncplay.pl/guide/ [Englisch] findest du Details zur Installation des syncplay.lua-Skripts.",
# Client prompts
"enter-to-exit-prompt" : u"Enter drücken zum Beenden\n",
# Client errors
"missing-arguments-error" : u"Notwendige Argumente fehlen, siehe --help",
"server-timeout-error" : u"Timeout: Verbindung zum Server fehlgeschlagen",
"mpc-slave-error" : u"Kann MPC nicht im Slave-Modus starten!",
"mpc-version-insufficient-error" : u"MPC-Version nicht ausreichend, bitte nutze `mpc-hc` >= `{}`",
"mpv-version-error" : "Syncplay is not compatible with this version of mpv. Please use a different version of mpv (e.g. Git HEAD).", # TODO: Translate into German
"player-file-open-error" : u"Fehler beim Öffnen der Datei durch den Player",
"player-path-error" : u"Ungültiger Player-Pfad",
"hostname-empty-error" : u"Hostname darf nicht leer sein",
"empty-error" : u"{} darf nicht leer sein", # Configuration
"media-player-error": u"Media player error: \"{}\"", # Error line # TODO: Translate into German
"unable-import-gui-error": u"Could not import GUI libraries. If you do not have PySide installed then you will need to install it for the GUI to work.", # TODO: Translate into German
"arguments-missing-error" : u"Notwendige Argumente fehlen, siehe --help",
"unable-to-start-client-error" : u"Client kann nicht gestartet werden",
"player-path-config-error": u"Player path is not set properly", # TODO: Translate into German
"no-file-path-config-error": u"File must be selected before starting your player", # TODO: Translate into German
"no-hostname-config-error": u"Hostname can't be empty", # TODO: Translate into German
"invalid-port-config-error" : u"Port must be valid", # TODO: Translate into German
"empty-value-config-error" : u"{} can't be empty", # Config option # TODO: Translate into German
"not-json-error" : u"Kein JSON-String\n",
"hello-arguments-error" : u"Zu wenige Hello-Argumente\n",
"version-mismatch-error" : u"Verschiedene Versionen auf Client und Server\n",
"vlc-failed-connection": u"Kann nicht zu VLC verbinden. Wenn du syncplay.lua nicht installiert hast, findest du auf http://syncplay.pl/LUA/ für eine Anleitung.",
"vlc-failed-noscript": u"Laut VLC ist das syncplay.lua Interface-Skript nicht installiert. Auf http://syncplay.pl/LUA/ findest du eine Anleitung.",
"vlc-failed-versioncheck": u"Diese VLC-Version wird von Syncplay nicht unterstützt. Bitte nutze VLC 2.0",
"vlc-failed-other" : u"Beim Laden des syncplay.lua Interface-Skripts durch VLC trat folgender Fehler auf: {}", # Syncplay Error
# Client arguments
"argument-description" : u'Anwendung, um mehrere MPlayer, MPC-HC und VLC-Instanzen über das Internet zu synchronisieren.',
"argument-epilog" : u'Wenn keine Optionen angegeben sind, werden die _config-Werte verwendet',
"nogui-argument" : u'Keine GUI anzeigen',
"host-argument" : u'Server\'-Addresse',
"name-argument" : u'Gewünschter Nutzername',
"debug-argument" : u'Debug-Modus',
"force-gui-prompt-argument" : u'Einstellungsfenster anzeigen',
"no-store-argument" : u'keine Werte in .syncplay speichern',
"room-argument" : u'Standard-Raum',
"password-argument" : u'Server-Passwort',
"player-path-argument" : u'Pfad zum Player',
"file-argument" : u'Zu spielende Datei',
"args-argument" : u'Player-Einstellungen; Wenn du Einstellungen, die mit - beginnen, nutzen willst, stelle ein einzelnes \'--\'-Argument davor',
"clear-gui-data-argument" : u'Setzt die Pfad- und GUI-Fenster-Daten, die in den QSettings gespeichert sind, zurück',
"language-argument" : u'language for Syncplay messages (de/en/pl/ru)', # TODO: Translate into German
"version-argument" : u'prints your version', # TODO: Translate into German
"version-message" : u"You're using Syncplay version {} ({})", # TODO: Translate into German
# Client labels
"config-window-title" : u"Syncplay Konfiguration",
"connection-group-title" : u"Verbindungseinstellungen",
"host-label" : u"Server-Adresse: ",
"name-label" : u"Benutzername (optional):",
"password-label" : u"Server-Passwort (falls nötig):",
"room-label" : u"Standard-Raum: ",
"media-setting-title" : u"Media-Player Einstellungen",
"executable-path-label" : u"Pfad zum Media-Player:",
"media-path-label" : u"Pfad zur Datei:",
"browse-label" : u"Durchsuchen",
"more-title" : u"Mehr Einstellungen zeigen",
"slowdown-threshold-label" : u"Slow down threshold:", # TODO: Translate into German
"rewind-threshold-label" : u"Rewind threshold:", # TODO: Translate into German
"fastforward-threshold-label" : u"Fast-forward threshold:", # TODO: Translate into German
"never-rewind-value" : u"Never", # TODO: Translate into German
"seconds-suffix" : u" secs", # TODO: Translate into German (if needed)
"privacy-sendraw-option" : u"Klartext senden",
"privacy-sendhashed-option" : u"Hash senden",
"privacy-dontsend-option" : u"Nicht senden",
"filename-privacy-label" : u"Dateiname:",
"filesize-privacy-label" : u"Dateigröße:",
"slowondesync-label" : u"Verlangsamen wenn nicht synchron", # TODO: Update new wording into German (should translate to "Slow down on minor desync (not supported on MPC-HC)")
"dontslowdownwithme-label" : u"Nie verlangsamen oder andere zurückspulen", # TODO: Update new wording into German (should state "Experimental" in brackets at the end)
"pauseonleave-label" : u"Pausieren wenn ein Benutzer austritt",
"forceguiprompt-label" : u"Don't always show this dialog", # TODO: Translate into German (previous was "Diesen Dialog immer anzeigen" but the logic is now inverted)
"nostore-label" : u"Diese Konfiguration nicht speichern",
"showosd-label" : u"Enable OSD Messages", # TODO: Translate into German
"showosdwarnings-label" : u"Include warnings (e.g. when files are different)", # TODO: Translate into German
"showsameroomosd-label" : u"Include events in your room", # TODO: Translate into German
"shownoncontrollerosd-label" : u"Include events from non-controllers in controlled rooms", # TODO: Translate into German
"showdifferentroomosd-label" : u"Include events in other rooms", # TODO: Translate into German
"showslowdownosd-label" : u"Include slowing down / reverting notification", # TODO: Translate into German
"showcontactinfo-label" : u"Show contact info box", # TODO: Translate into German
"language-label" : u"Language", # TODO: Translate into German
"showdurationnotification-label" : u"Warn about media duration mismatches", # TODO: Translate into German
"basics-label" : u"Basics", # TODO: Translate into German
"sync-label" : u"Sync", # TODO: Translate into German
"sync-lagging-title" : u"If others are lagging behind...", # TODO: Translate into German
"sync-other-title" : u"Other sync options", # TODO: Translate into German
"messages-label" : u"Messages", # TODO: Translate into German
"messages-osd-title" : u"On-screen Display settings", # TODO: Translate into German
"messages-other-title" : u"Other display settings", # TODO: Translate into German
"privacy-label" : u"Privacy", # TODO: Translate into German
"privacy-title" : u"Privacy settings", # TODO: Translate into German
"help-label" : u"Hilfe",
"reset-label" : u"Restore defaults", # TODO: Translate into German
"run-label" : u"Syncplay starten",
"storeandrun-label" : u"Konfiguration speichern und Syncplay starten",
"contact-label" : u"Have an idea, bug report or feedback? E-mail <a href=\"mailto:dev@syncplay.pl\">dev@syncplay.pl</a>, chat via the <a href=\"https://webchat.freenode.net/?channels=#syncplay\">#Syncplay IRC channel</a> on irc.freenode.net or <a href=\"https://github.com/Uriziel/syncplay/issues/new\">raise an issue via GitHub</a>. Also check out <a href=\"http://syncplay.pl/\">http://syncplay.pl/</a> for info, help and updates.", #TODO: Translate into German
"joinroom-guibuttonlabel" : u"Join room", # TODO: Translate into German
"seektime-guibuttonlabel" : u"Seek to time", # TODO: Translate into German
"undoseek-guibuttonlabel" : u"Undo seek", # TODO: Translate into German
"togglepause-guibuttonlabel" : u"Toggle pause", # TODO: Translate into German
"play-guibuttonlabel" : u"Play", # TODO: Translate into German
"pause-guibuttonlabel" : u"Pause", # TODO: Translate into German
"roomuser-heading-label" : u"Raum / Benutzer",
"size-heading-label" : u"Size", # TODO: Translate into German (Note: This is size as in filesize)
"duration-heading-label" : u"Length", # TODO: Translate into German (Note: This is length as in file duration)
"filename-heading-label" : u"Filename", # TODO: Translate into German
"notifications-heading-label" : u"Notifications", # TODO: Translate into German
"userlist-heading-label" : u"List of who is playing what", # TODO: Translate into German
"othercommands-heading-label" : u"Other commands", # TODO: Translate into German
"room-heading-label" : u"Room", # TODO: Translate into German
"seek-heading-label" : u"Seek", # TODO: Translate into German
"browseformedia-label" : u"Browse for media files", # TODO: Translate into German
"file-menu-label" : u"&File", # & precedes shortcut key # TODO: Translate into German
"openmedia-menu-label" : u"&Open media file", # TODO: Translate into German
"exit-menu-label" : u"E&xit", # TODO: Translate into German
"advanced-menu-label" : u"&Advanced", # TODO: Translate into German
"setoffset-menu-label" : u"Set &offset", # TODO: Translate into German
"createcontrolledroom-menu-label" : u"&Create controlled room", # TODO: Translate into German
"identifyascontroller-menu-label" : u"&Identify as room controller", # TODO: Translate into German
"help-menu-label" : u"&Help", # TODO: Translate into German
"userguide-menu-label" : u"Open user &guide", # TODO: Translate into German
"setoffset-msgbox-label" : u"Set offset", # TODO: Translate into German
"offsetinfo-msgbox-label" : u"Offset (see http://syncplay.pl/guide/ for usage instructions):", # TODO: Translate into German
"createcontrolledroom-msgbox-label" : u"Create controlled room", # TODO: Translate into German
"controlledroominfo-msgbox-label" : u"Enter name of controlled room\r\n(see http://syncplay.pl/guide/ for usage instructions):", # TODO: Translate into German
"identifyascontroller-msgbox-label" : u"Identify as Room Controller", # TODO: Translate into German
"identifyinfo-msgbox-label" : u"Enter controller password for this room\r\n(see http://syncplay.pl/guide/ for usage instructions):", # TODO: Translate into German
"megabyte-suffix" : u" MB", # TODO: Translate into German (or is MB fine?)
# Tooltips
"host-tooltip" : u"Hostname oder IP zu der verbunden werden soll. Ptional mit Port (z.B.. syncplay.pl:8999). Synchronisation findet nur mit Personen auf dem selben Server und Port statt.",
"name-tooltip" : u"Dein Benutzername. Keine Registrierung, kann einfach geändert werden. Bei fehlender Angabe wird ein zufälliger Name generiert.",
"password-tooltip" : u"Passwörter sind nur bei Verbindung zu privaten Servern nötig.",
"room-tooltip" : u"Der Raum, der betreten werden soll, kann ein x-beliebiger sein. Allerdings werden nur Clients im selben Raum synchronisiert.",
"executable-path-tooltip" : u"Pfad zum ausgewählten, unterstützten Mediaplayer (MPC-HC, VLC, mplayer2 or mpv).",
"media-path-tooltip" : u"Pfad zum wiederzugebenden Video oder Stream. Notwendig für mpv und mplayer2.",
"more-tooltip" : u"Weitere Einstellungen anzeigen.",
"slowdown-threshold-tooltip" : u"Time ahead of slowest client before temporarily reducing playback speed (default: {} secs).".format(constants.DEFAULT_SLOWDOWN_KICKIN_THRESHOLD), # Todo: Translate into German
"rewind-threshold-tooltip" : u"Time ahead slowest client before seeking to get back in sync (default: {} secs).".format(constants.DEFAULT_REWIND_THRESHOLD), # Todo: Translate into German
"fastforward-threshold-tooltip" : u"Time behind room controller before seeking to get back in sync (default: {} secs).".format(constants.DEFAULT_FASTFORWARD_THRESHOLD), # Todo: Translate into German
"filename-privacy-tooltip" : u"Privat-Modus beim senden des Namens der aktuellen Datei zum Server.",
"filesize-privacy-tooltip" : u"Privat-Modus beim senden der Größe der aktuellen Datei zum Server.",
"privacy-sendraw-tooltip" : u"Die Information im Klartext übertragen. Dies ist die Standard-Einstellung mit der besten Funktionalität.",
"privacy-sendhashed-tooltip" : u"Die Informationen gehasht übertragen, um sie für andere Clients schwerer lesbar zu machen.",
"privacy-dontsend-tooltip" : u"Diese Information nicht übertragen. Dies garantiert den größtmöglichen Datanschutz.",
"slowondesync-tooltip" : u"Reduziert die Abspielgeschwindigkeit zeitweise, um dich wieder synchron zu den anderen Clients zu machen.",
"dontslowdownwithme-tooltip" : u"Lässt andere nicht langsamer werden oder zurückspringen, wenn deine Wiedergabe hängt.",
"pauseonleave-tooltip" : u"Wiedergabe anhalten, wenn deine Verbindung verloren geht oder jemand den Raum verlässt.",
"rewindondesync-label" : u"Zurückspulen bei großer Zeitdifferent (empfohlen)",
"fastforwardondesync-label" : u"Fast-forward if lagging behind (recommended)", # TODO: Translate into German
"forceguiprompt-tooltip" : u"Configuration dialogue is not shown when opening a file with Syncplay.", # TODO: Translate into German (was "Der Konfigurations-Dislog wird immer angezeigt. Sogar, wenn du eine Datei mit Syncplay öffnest." but logic is now inverted)
"nostore-tooltip" : u"Syncplay mit den angegebenen Einstellungen starten, diese aber nicht fauerhaft speichern.",
"rewindondesync-tooltip" : u"In der Zeit zurückspringen zum wiederherstellen der Synchronität. Empfohlen.",
"fastforwardondesync-tooltip" : u"Jump forward when out of sync with room controller (or your pretend position if 'Never slow down or rewind others' enabled).", # Translate into German (if you want)
"showosd-tooltip" : u"Sends Syncplay messages to media player OSD.", # Translate into German (if you want)
"showosdwarnings-tooltip" : u"Show warnings if playing different file, alone in room.", # TODO: Translate into German (if you want)
"showsameroomosd-tooltip" : u"Show OSD notifications for events relating to room user is in.", # TODO: Translate into German (if you want)
"shownoncontrollerosd-tooltip" : u"Show OSD notifications for events relating to non-controllers who are in controllerd rooms.", # Translate into German (if you want)
"showdifferentroomosd-tooltip" : u"Show OSD notifications for events relating to room user is not in.", # TODO: Translate into German (if you want)
"showslowdownosd-tooltip" : u"Show notifications of slowing down / reverting on time difference.", # TODO: Translate into German (if you want)
"showcontactinfo-tooltip" : u"Show information box about contacting Syncplay developers in main Syncplay window.", # TODO: Translate into German (if you want)
"showdurationnotification-tooltip" : u"Useful for when a segment in a multi-part file is missing, but can result in false positives.", # TODO: Translate into German (if you want)
"language-tooltip" : u"Language to be used by Syncplay.", # TODO: Translate into German (if you want)
"help-tooltip" : u"Öffnet Hilfe auf syncplay.pl [Englisch]",
"reset-tooltip" : u"Reset all settings to the default configuration.", # TODO: Translate into German
"togglepause-tooltip" : u"Pause/unpause media.", # TODO: Translate into German (if you want)
"play-tooltip" : u"Unpause media.", # TODO: Translate into German (if you want)
"pause-tooltip" : u"Pause media.", # TODO: Translate into German (if you want)
"undoseek-tooltip" : u"Seek to where you were before the most recent seek.", # TODO: Translate into German(if you want)
"joinroom-tooltip" : u"Leave current room and joins specified room.", # TODO: Translate into German(if you want)
"seektime-tooltip" : u"Jump to specified time (in seconds / min:sec). Use +/- for relative seek.", # TODO: Translate into German
# In-userlist notes (GUI)
"differentsize-note" : u"Different size!", # TODO: Translate into German
"differentsizeandduration-note" : u"Different size and duration!", # TODO: Translate into German
"differentduration-note" : u"Different duration!", # TODO: Translate into German
"nofile-note" : u"(No file being played)", # TODO: Translate into German
# Server messages to client
"new-syncplay-available-motd-message" : u"<NOTICE> Du nutzt Syncplay Version {}, aber es gibt eine neuere Version auf http://syncplay.pl</NOTICE>", # ClientVersion
# Server notifications
"welcome-server-notification" : u"Willkommen zum Syncplay-Server, v. {0}", # version
"client-connected-room-server-notification" : u"{0}({2}) hat den Raum '{1}' betreten", # username, host, room
"client-left-server-notification" : u"{0} hat den Server verlassen", # name
"no-salt-notification" : "PLEASE NOTE: To allow room control passwords generated by this server instance to still work when the server is restarted, please add the following command line argument when running the Syncplay server in the future: --salt {}", #Salt # TODO: Translate into German
# Server arguments
"server-argument-description" : u'Anwendung, um mehrere MPlayer, MPC-HC und VLC-Instanzen über das Internet zu synchronisieren. Server',
"server-argument-epilog" : u'Wenn keine Optionen angegeben sind, werden die _config-Werte verwendet',
"server-port-argument" : u'Server TCP-Port',
"server-password-argument" : u'Server Passwort',
"server-isolate-room-argument" : u'Sollen die Räume isoliert sein?',
"server-salt-argument" : u"random string used to generate controlled room passwords", # TODO: Translate into German
"server-motd-argument": u"Pfad zur Datei, von der die Nachricht des Tages geladen wird",
"server-messed-up-motd-unescaped-placeholders": u"Die Nachricht des Tages hat unmaskierte Platzhalter. Alle $-Zeichen sollten verdoppelt werden ($$).",
"server-messed-up-motd-too-long": u"Die Nachricht des Tages ist zu lang - Maximal {} Zeichen, aktuell {}.",
# Server errors
"unknown-command-server-error" : u"Unbekannter Befehl {}", # message
"not-json-server-error" : u"Kein JSON-String {}", # message
"not-known-server-error" : u"Der Server muss dich kennen, bevor du diesen Befehl nutzen kannst",
"client-drop-server-error" : u"Client verloren: {} -- {}", # host, error
"password-required-server-error" : u"Passwort nötig",
"wrong-password-server-error" : u"Ungültiges Passwort",
"hello-server-error" : u"Zu wenige Hello-Argumente"
}
messages = {
"en": en,
"pl": pl,
"current": None
"ru": ru,
"de": de,
"CURRENT": None
}
def getLanguages():
langList = {}
for lang in messages:
if lang != "CURRENT":
langList[lang] = getMessage("LANGUAGE", lang)
return langList
def setLanguage(lang):
messages["current"] = lang
messages["CURRENT"] = lang
def getMissingStrings():
missingStrings = ""
for lang in messages:
if lang != "en" and lang != "CURRENT":
for message in messages["en"]:
if not messages[lang].has_key(message):
missingStrings = missingStrings + "({}) Missing: {}\n".format(lang, message)
for message in messages[lang]:
if not messages["en"].has_key(message):
missingStrings = missingStrings + "({}) Unused: {}\n".format(lang, message)
return missingStrings
def getMessage(type_, locale=None):
if constants.SHOW_BUTTON_LABELS == False:
@ -327,7 +955,7 @@ def getMessage(type_, locale=None):
if constants.SHOW_TOOLTIPS == False:
if "-tooltip" in type_:
return ""
lang = messages["current"]
lang = messages["CURRENT"]
if locale and messages.has_key(locale):
if messages[locale].has_key(type_):
return unicode(messages[locale][type_])

View File

@ -49,7 +49,7 @@ class BasePlayer(object):
'''
@type filePath: string
'''
def openFile(self, filePath):
def openFile(self, filePath, resetPosition=False):
raise NotImplementedError()
@ -82,8 +82,25 @@ class BasePlayer(object):
@staticmethod
def getExpandedPath(path):
raise NotImplementedError()
'''
Opens a custom media browse dialog, and then changes to that media if appropriate
'''
@staticmethod
def openCustomOpenDialog(self):
raise NotImplementedError()
'''
@type playerPath: string
@type filePath: string
@return errorMessage: string
Checks if the player has any problems with the given player/file path
'''
@staticmethod
def getPlayerPathErrors(playerPath, filePath):
raise NotImplementedError()
class DummyPlayer(BasePlayer):
@staticmethod
@ -101,3 +118,7 @@ class DummyPlayer(BasePlayer):
@staticmethod
def getExpandedPath(path):
return path
@staticmethod
def getPlayerPathErrors(playerPath, filePath):
return None

View File

@ -306,6 +306,7 @@ class MpcHcApi:
class MPCHCAPIPlayer(BasePlayer):
speedSupported = False
customOpenDialog = False
def __init__(self, client):
from twisted.internet import reactor
@ -331,6 +332,10 @@ class MPCHCAPIPlayer(BasePlayer):
self.__versionUpdate.set()
self._mpcApi.sendRawCommand(MpcHcApi.CMD_CLOSEAPP, "")
@staticmethod
def getPlayerPathErrors(playerPath, filePath):
return None
@staticmethod
def run(client, playerPath, filePath, args):
args.extend(['/open', '/new'])
@ -385,7 +390,7 @@ class MPCHCAPIPlayer(BasePlayer):
if filePath:
self.openFile(filePath)
def openFile(self, filePath):
def openFile(self, filePath, resetPosition=False):
self._mpcApi.openFile(filePath)
def displayMessage(self, message, duration = (constants.OSD_DURATION*1000)):

View File

@ -9,8 +9,8 @@ import os
class MplayerPlayer(BasePlayer):
speedSupported = True
customOpenDialog = False
RE_ANSWER = re.compile(constants.MPLAYER_ANSWER_REGEX)
SLAVE_ARGS = constants.MPLAYER_SLAVE_ARGS
POSITION_QUERY = 'time_pos'
OSD_QUERY = 'osd_show_text'
@ -23,6 +23,9 @@ class MplayerPlayer(BasePlayer):
self._duration = None
self._filename = None
self._filepath = None
self.quitReason = None
self.lastLoadedTime = None
self.fileLoaded = False
try:
self._listener = self.__Listener(self, playerPath, filePath, args)
except ValueError:
@ -86,8 +89,12 @@ class MplayerPlayer(BasePlayer):
def setSpeed(self, value):
self._setProperty('speed', "{:.2f}".format(value))
def openFile(self, filePath):
def _loadFile(self, filePath):
self._listener.sendLine(u'loadfile {}'.format(self._quoteArg(filePath)))
def openFile(self, filePath, resetPosition=False):
self._filepath = filePath
self._loadFile(filePath)
self._onFileUpdate()
if self._paused != self._client.getGlobalPaused():
self.setPaused(self._client.getGlobalPaused())
@ -124,19 +131,42 @@ class MplayerPlayer(BasePlayer):
arg = arg.replace('"', '\\"')
return u'"{}"'.format(arg)
def _fileIsLoaded(self):
return True
def _handleUnknownLine(self, line):
pass
def _storePosition(self, value):
self._position = value
def _storePauseState(self, value):
self._paused = value
def lineReceived(self, line):
if line:
self._client.ui.showDebugMessage("player << {}".format(line))
if "Failed to get value of property" in line:
if "filename" in line:
self._getFilename()
elif "length" in line:
self._getLength()
elif "path" in line:
self._getFilepath()
return
match = self.RE_ANSWER.match(line)
if not match:
self._handleUnknownLine(line)
return
name, value =[m for m in match.groups() if m]
name = name.lower()
if name == self.POSITION_QUERY:
self._position = float(value)
self._storePosition(float(value))
self._positionAsk.set()
elif name == "pause":
self._paused = bool(value == 'yes')
self._storePauseState(bool(value == 'yes'))
self._pausedAsk.set()
elif name == "length":
self._duration = float(value)
@ -149,7 +179,9 @@ class MplayerPlayer(BasePlayer):
self._filenameAsk.set()
elif name == "exiting":
if value != 'Quit':
self.reactor.callFromThread(self._client.ui.showErrorMessage, getMessage("media-player-error").format(value), True)
if self.quitReason is None:
self.quitReason = getMessage("media-player-error").format(value)
self.reactor.callFromThread(self._client.ui.showErrorMessage, self.quitReason, True)
self.drop()
@staticmethod
@ -170,12 +202,21 @@ class MplayerPlayer(BasePlayer):
def getIconPath(path):
return constants.MPLAYER_ICONPATH
@staticmethod
def getStartupArgs(path):
return constants.MPLAYER_SLAVE_ARGS
@staticmethod
def isValidPlayerPath(path):
if "mplayer" in path and MplayerPlayer.getExpandedPath(path):
return True
return False
@staticmethod
def getPlayerPathErrors(playerPath, filePath):
if not filePath:
return getMessage("no-file-path-config-error")
@staticmethod
def getExpandedPath(playerPath):
if not os.path.isfile(playerPath):
@ -219,7 +260,7 @@ class MplayerPlayer(BasePlayer):
filePath = os.path.realpath(filePath)
call = [playerPath, filePath]
call.extend(playerController.SLAVE_ARGS)
call.extend(playerController.getStartupArgs(playerPath))
if args:
call.extend(args)
# At least mpv may output escape sequences which result in syncplay
@ -259,7 +300,10 @@ class MplayerPlayer(BasePlayer):
def sendLine(self, line):
try:
line = (line.decode('utf8') + u"\n").encode('utf8')
if not isinstance(line, unicode):
line = line.decode('utf8')
line = (line + u"\n").encode('utf8')
self.__playerController._client.ui.showDebugMessage("player >> {}".format(line))
self.__process.stdin.write(line)
except IOError:
pass

View File

@ -1,23 +1,35 @@
import re
import subprocess
from syncplay.players.mplayer import MplayerPlayer
from syncplay.messages import getMessage
from syncplay import constants
import os
import os, sys, time
class MpvPlayer(MplayerPlayer):
SLAVE_ARGS = constants.MPV_SLAVE_ARGS
POSITION_QUERY = 'time-pos'
OSD_QUERY = 'show_text'
def _setProperty(self, property_, value):
self._listener.sendLine("no-osd set {} {}".format(property_, value))
def setPaused(self, value):
if self._paused <> value:
self._paused = not self._paused
self._listener.sendLine('cycle pause')
RE_VERSION = re.compile('.*mpv (\d)\.(\d)\.\d.*')
@staticmethod
def run(client, playerPath, filePath, args):
return MpvPlayer(client, MpvPlayer.getExpandedPath(playerPath), filePath, args)
try:
ver = MpvPlayer.RE_VERSION.search(subprocess.check_output([playerPath, '--version']))
except:
ver = None
constants.MPV_NEW_VERSION = ver is None or int(ver.group(1)) > 0 or int(ver.group(2)) >= 5
if constants.MPV_NEW_VERSION:
return NewMpvPlayer(client, MpvPlayer.getExpandedPath(playerPath), filePath, args)
else:
return OldMpvPlayer(client, MpvPlayer.getExpandedPath(playerPath), filePath, args)
@staticmethod
def getStartupArgs(path):
args = constants.MPV_SLAVE_ARGS
if constants.MPV_NEW_VERSION or sys.platform.startswith('win'):
args.extend(constants.MPV_SLAVE_ARGS_NEW)
if sys.platform.startswith('win') or not constants.MPV_NEW_VERSION:
args.extend(constants.MPV_SLAVE_ARGS_WINDOWS)
else:
args.extend(constants.MPV_SLAVE_ARGS_NONWINDOWS)
return args
@staticmethod
def getDefaultPlayerPathsList():
@ -53,3 +65,112 @@ class MpvPlayer(MplayerPlayer):
@staticmethod
def getIconPath(path):
return constants.MPV_ICONPATH
class OldMpvPlayer(MpvPlayer):
POSITION_QUERY = 'time-pos'
OSD_QUERY = 'show_text'
def _setProperty(self, property_, value):
self._listener.sendLine("no-osd set {} {}".format(property_, value))
def setPaused(self, value):
if self._paused <> value:
self._paused = not self._paused
self._listener.sendLine('cycle pause')
def mpvVersionErrorCheck(self, line):
if "Error parsing option" in line or "Error parsing commandline option" in line:
self.quitReason = getMessage("mpv-version-error")
elif "Could not open pipe at '/dev/stdin'" in line:
self.reactor.callFromThread(self._client.ui.showErrorMessage, getMessage("mpv-version-error"), True)
self.drop()
def _handleUnknownLine(self, line):
self.mpvVersionErrorCheck(line)
if "Playing: " in line:
newpath = line[9:]
oldpath = self._filepath
if newpath != oldpath and oldpath is not None:
self.reactor.callFromThread(self._onFileUpdate)
if self._paused != self._client.getGlobalPaused():
self.setPaused(self._client.getGlobalPaused())
self.setPosition(self._client.getGlobalPosition())
class NewMpvPlayer(OldMpvPlayer):
lastResetTime = None
def _storePosition(self, value):
if self._recentlyReset():
self._position = 0
elif self._fileIsLoaded():
self._position = value
else:
self._position = self._client.getGlobalPosition()
def _storePauseState(self, value):
if self._fileIsLoaded():
self._paused = value
else:
self._paused = self._client.getGlobalPaused()
def askForStatus(self):
self._positionAsk.clear()
self._pausedAsk.clear()
self._getPaused()
self._getPosition()
self._positionAsk.wait(constants.MPV_LOCK_WAIT_TIME)
self._pausedAsk.wait(constants.MPV_LOCK_WAIT_TIME)
self._client.updatePlayerStatus(self._paused, self._position)
def _preparePlayer(self):
self.setPaused(True)
self.reactor.callLater(0, self._client.initPlayer, self)
def _clearFileLoaded(self):
self.fileLoaded = False
self.lastLoadedTime = None
def _loadFile(self, filePath):
self._clearFileLoaded()
self._listener.sendLine(u'loadfile {}'.format(self._quoteArg(filePath)))
def openFile(self, filePath, resetPosition=False):
if resetPosition:
self.lastResetTime = time.time()
self._loadFile(filePath)
if self._paused != self._client.getGlobalPaused():
self.setPaused(self._client.getGlobalPaused())
self.setPosition(self._client.getGlobalPosition())
def _handleUnknownLine(self, line):
self.mpvVersionErrorCheck(line)
if line == "<SyncplayUpdateFile>" or "Playing:" in line:
self._clearFileLoaded()
elif line == "</SyncplayUpdateFile>":
self._onFileUpdate()
def _recentlyReset(self):
if not self.lastResetTime:
return False
elif time.time() < self.lastResetTime + constants.MPV_NEWFILE_IGNORE_TIME:
return True
else:
return False
def _onFileUpdate(self):
self.fileLoaded = True
self.lastLoadedTime = time.time()
self.reactor.callFromThread(self._client.updateFile, self._filename, self._duration, self._filepath)
if not (self._recentlyReset()):
self.reactor.callFromThread(self.setPosition, self._client.getGlobalPosition())
if self._paused != self._client.getGlobalPaused():
self.reactor.callFromThread(self._client.getGlobalPaused)
def _fileIsLoaded(self):
if self.fileLoaded == True and self.lastLoadedTime != None and time.time() > (self.lastLoadedTime + constants.MPV_NEWFILE_IGNORE_TIME):
return True
else:
return False

View File

@ -10,10 +10,10 @@ import socket
import asynchat, asyncore
import urllib
from syncplay.messages import getMessage
import time
class VlcPlayer(BasePlayer):
speedSupported = True
customOpenDialog = False
RE_ANSWER = re.compile(constants.VLC_ANSWER_REGEX)
SLAVE_ARGS = constants.VLC_SLAVE_ARGS
if not sys.platform.startswith('darwin'):
@ -30,6 +30,13 @@ class VlcPlayer(BasePlayer):
self._filename = None
self._filepath = None
self._filechanged = False
try: # Hack to fix locale issue without importing locale library
self.radixChar = "{:n}".format(1.5)[1:2]
if self.radixChar == "" or self.radixChar == "1" or self.radixChar == "5":
raise ValueError
except:
self._client.ui.showErrorMessage("Failed to determine locale. As a fallback Syncplay is using the following radix character: \".\".")
self.radixChar = "."
self._durationAsk = threading.Event()
self._filenameAsk = threading.Event()
@ -91,8 +98,7 @@ class VlcPlayer(BasePlayer):
self._listener.sendLine("set-rate: {:.2n}".format(value))
def setPosition(self, value):
self._position = value
self._listener.sendLine("set-position: {}".format(value))
self._listener.sendLine("set-position: {}".format(value).replace(".",self.radixChar))
def setPaused(self, value):
self._paused = value
@ -112,7 +118,7 @@ class VlcPlayer(BasePlayer):
def _isASCII (self, s):
return all(ord(c) < 128 for c in s)
def openFile(self, filePath):
def openFile(self, filePath, resetPosition=False):
if self._isASCII(filePath):
self._listener.sendLine('load-file: {}'.format(filePath.encode('ascii', 'ignore')))
else:
@ -125,6 +131,7 @@ class VlcPlayer(BasePlayer):
self._listener.sendLine("get-filename")
def lineReceived(self, line):
self._client.ui.showDebugMessage("player >> {}".format(line))
match, name, value = self.RE_ANSWER.match(line), "", ""
if match:
name, value = match.group('command'), match.group('argument')
@ -193,6 +200,10 @@ class VlcPlayer(BasePlayer):
return True
return False
@staticmethod
def getPlayerPathErrors(playerPath, filePath):
return None
@staticmethod
def getIconPath(path):
return constants.VLC_ICONPATH
@ -322,15 +333,14 @@ class VlcPlayer(BasePlayer):
self.__playerController.drop()
def found_terminator(self):
# print "received: {}".format("".join(self._ibuffer))
self.__playerController.lineReceived("".join(self._ibuffer))
self._ibuffer = []
def sendLine(self, line):
if self.connected:
# print "send: {}".format(line)
try:
self.push(line + "\n")
self._client.ui.showDebugMessage("player >> {}".format(line))
except:
pass
if line == "close-vlc":

View File

@ -30,6 +30,7 @@ class JSONCommandProtocol(LineReceiver):
if not line:
return
try:
self.showDebugMessage("client/server << {}".format(line))
messages = json.loads(line)
except:
self.dropWithError(getMessage("not-json-server-error").format(line))
@ -39,6 +40,7 @@ class JSONCommandProtocol(LineReceiver):
def sendMessage(self, dict_):
line = json.dumps(dict_)
self.sendLine(line)
self.showDebugMessage("client/server >> {}".format(line))
def drop(self):
self.transport.loseConnection()
@ -55,6 +57,9 @@ class SyncClientProtocol(JSONCommandProtocol):
self.logged = False
self._pingService = PingService()
def showDebugMessage(self, line):
self._client.ui.showDebugMessage(line)
def connectionMade(self):
self._client.initProtocol(self)
self.sendHello()
@ -71,6 +76,7 @@ class SyncClientProtocol(JSONCommandProtocol):
username = hello["username"] if hello.has_key("username") else None
roomName = hello["room"]["name"] if hello.has_key("room") else None
version = hello["version"] if hello.has_key("version") else None
version = hello["realversion"] if hello.has_key("realversion") else version # Used for 1.2.X compatibility
motd = hello["motd"] if hello.has_key("motd") else None
return username, roomName, version, motd
@ -78,8 +84,6 @@ class SyncClientProtocol(JSONCommandProtocol):
username, roomName, version, motd = self._extractHelloArguments(hello)
if not username or not roomName or not version:
self.dropWithError(getMessage("hello-server-error").format(hello))
elif version.split(".")[0:2] != syncplay.version.split(".")[0:2]:
self.dropWithError(getMessage("version-mismatch-server-error".format(hello)))
else:
self._client.setUsername(username)
self._client.setRoom(roomName)
@ -87,7 +91,9 @@ class SyncClientProtocol(JSONCommandProtocol):
if motd:
self._client.ui.showMessage(motd, True, True)
self._client.ui.showMessage(getMessage("connected-successful-notification"))
self._client.connected()
self._client.sendFile()
self._client.setServerVersion(version)
def sendHello(self):
hello = {}
@ -96,7 +102,8 @@ class SyncClientProtocol(JSONCommandProtocol):
if password: hello["password"] = password
room = self._client.getRoom()
if room: hello["room"] = {"name" :room}
hello["version"] = syncplay.version
hello["version"] = "1.2.255" # Used so newer clients work on 1.2.X server
hello["realversion"] = syncplay.version
self.sendMessage({"Hello": hello})
def _SetUser(self, users):
@ -114,13 +121,21 @@ class SyncClientProtocol(JSONCommandProtocol):
self._client.userlist.modUser(username, room, file_)
def handleSet(self, settings):
for set_ in settings.iteritems():
command = set_[0]
for (command, values) in settings.iteritems():
if command == "room":
roomName = set_[1]["name"] if set_[1].has_key("name") else None
roomName = values["name"] if values.has_key("name") else None
self._client.setRoom(roomName)
elif command == "user":
self._SetUser(set_[1])
self._SetUser(values)
elif command == "controllerAuth":
if values['success']:
self._client.controllerIdentificationSuccess(values["user"], values["room"])
else:
self._client.controllerIdentificationError(values["user"], values["room"])
elif command == "newControlledRoom":
controlPassword = values['password']
roomName = values['roomName']
self._client.controlledRoomCreated(roomName, controlPassword)
def sendSet(self, setting):
self.sendMessage({"Set": setting})
@ -142,7 +157,8 @@ class SyncClientProtocol(JSONCommandProtocol):
for user in room[1].iteritems():
userName = user[0]
file_ = user[1]['file'] if user[1]['file'] <> {} else None
self._client.userlist.addUser(userName, roomName, file_, noMessage=True)
isController = user[1]['controller'] if 'controller' in user[1] else False
self._client.userlist.addUser(userName, roomName, file_, noMessage=True, isController=isController)
self._client.userlist.showUserList()
def sendList(self):
@ -210,8 +226,16 @@ class SyncClientProtocol(JSONCommandProtocol):
state["ignoringOnTheFly"]["client"] = self.clientIgnoringOnTheFly
self.sendMessage({"State": state})
def requestControlledRoom(self, room, password):
self.sendSet({
"controllerAuth": {
"room": room,
"password": password
}
})
def handleError(self, error):
self.dropWithError(error["message"]) # TODO: more processing and fallbacking
self.dropWithError(error["message"])
def sendError(self, message):
self.sendMessage({"Error": {"message": message}})
@ -241,6 +265,9 @@ class SyncServerProtocol(JSONCommandProtocol):
return f(self, *args, **kwds)
return wrapper
def showDebugMessage(self, line):
pass
def dropWithError(self, error):
print getMessage("client-drop-server-error").format(self.transport.getPeer().host, error)
self.sendError(error)
@ -253,7 +280,7 @@ class SyncServerProtocol(JSONCommandProtocol):
return self._logged
def _extractHelloArguments(self, hello):
roomName, roomPassword = None, None
roomName = None
username = hello["username"] if hello.has_key("username") else None
username = username.strip()
serverPassword = hello["password"] if hello.has_key("password") else None
@ -261,9 +288,9 @@ class SyncServerProtocol(JSONCommandProtocol):
if room:
roomName = room["name"] if room.has_key("name") else None
roomName = roomName.strip()
roomPassword = room["password"] if room.has_key("password") else None
version = hello["version"] if hello.has_key("version") else None
return username, serverPassword, roomName, roomPassword, version
version = hello["realversion"] if hello.has_key("realversion") else version
return username, serverPassword, roomName, version
def _checkPassword(self, serverPassword):
if self._factory.password:
@ -276,15 +303,14 @@ class SyncServerProtocol(JSONCommandProtocol):
return True
def handleHello(self, hello):
username, serverPassword, roomName, roomPassword, version = self._extractHelloArguments(hello)
username, serverPassword, roomName, version = self._extractHelloArguments(hello)
if not username or not roomName or not version:
self.dropWithError(getMessage("hello-server-error"))
elif version.split(".")[0:2] != syncplay.version.split(".")[0:2]:
self.dropWithError(getMessage("version-mismatch-server-error"))
return
else:
if not self._checkPassword(serverPassword):
return
self._factory.addWatcher(self, username, roomName, roomPassword)
self._factory.addWatcher(self, username, roomName)
self._logged = True
self.sendHello(version)
@ -298,7 +324,8 @@ class SyncServerProtocol(JSONCommandProtocol):
userIp = self.transport.getPeer().host
room = self._watcher.getRoom()
if room: hello["room"] = {"name": room.getName()}
hello["version"] = syncplay.version
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)
self.sendMessage({"Hello": hello})
@ -311,14 +338,34 @@ class SyncServerProtocol(JSONCommandProtocol):
self._factory.setWatcherRoom(self._watcher, roomName)
elif command == "file":
self._watcher.setFile(set_[1])
elif command == "controllerAuth":
password = set_[1]["password"] if set_[1].has_key("password") else None
room = set_[1]["room"] if set_[1].has_key("room") else None
self._factory.authRoomController(self._watcher, password, room)
def sendSet(self, setting):
self.sendMessage({"Set": setting})
def sendNewControlledRoom(self, roomName, password):
self.sendSet({
"newControlledRoom": {
"password": password,
"roomName": roomName
}
})
def sendControlledRoomAuthStatus(self, success, username, roomname):
self.sendSet({
"controllerAuth": {
"user": username,
"room": roomname,
"success": success
}
})
def sendUserSetting(self, username, room, file_, event):
room = {"name": room.getName()}
user = {}
user[username] = {}
user = {username: {}}
user[username]["room"] = room
if file_:
user[username]["file"] = file_
@ -331,7 +378,11 @@ class SyncServerProtocol(JSONCommandProtocol):
if room:
if room.getName() not in userlist:
userlist[room.getName()] = {}
userFile = { "position": 0, "file": watcher.getFile() if watcher.getFile() else {} }
userFile = {
"position": 0,
"file": watcher.getFile() if watcher.getFile() else {},
"controller": watcher.isController()
}
userlist[room.getName()][watcher.getName()] = userFile
def sendList(self):
@ -354,8 +405,8 @@ class SyncServerProtocol(JSONCommandProtocol):
"position": position if position else 0,
"paused": paused,
"doSeek": doSeek,
"setBy": setBy.getName()
}
"setBy": setBy.getName() if setBy else None
}
ping = {
"latencyCalculation": self._pingService.newTimestamp(),
"serverRtt": self._pingService.getRtt()

View File

@ -1,4 +1,5 @@
import hashlib
import random
from twisted.internet import task, reactor
from twisted.internet.protocol import Factory
import syncplay
@ -10,13 +11,18 @@ import codecs
import os
from string import Template
import argparse
from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomStringGenerator, meetsMinVersion
class SyncFactory(Factory):
def __init__(self, password='', motdFilePath=None, isolateRooms=False):
def __init__(self, password='', motdFilePath=None, isolateRooms=False, salt=None):
print getMessage("welcome-server-notification").format(syncplay.version)
if password:
password = hashlib.md5(password).hexdigest()
self.password = password
if salt is None:
salt = RandomStringGenerator.generate_server_salt()
print getMessage("no-salt-notification").format(salt)
self._salt = salt
self._motdFilePath = motdFilePath
if not isolateRooms:
self._roomManager = RoomManager()
@ -36,7 +42,7 @@ class SyncFactory(Factory):
def getMotd(self, userIp, username, room, clientVersion):
oldClient = False
if constants.WARN_OLD_CLIENTS:
if int(clientVersion.replace(".", "")) < int(constants.RECENT_CLIENT_THRESHOLD.replace(".", "")):
if not meetsMinVersion(clientVersion, constants.RECENT_CLIENT_THRESHOLD):
oldClient = True
if self._motdFilePath and os.path.isfile(self._motdFilePath):
tmpl = codecs.open(self._motdFilePath, "r", "utf-8-sig").read()
@ -54,7 +60,7 @@ class SyncFactory(Factory):
else:
return ""
def addWatcher(self, watcherProtocol, username, roomName, roomPassword):
def addWatcher(self, watcherProtocol, username, roomName):
username = self._roomManager.findFreeUsername(username)
watcher = Watcher(self, watcherProtocol, username)
self.setWatcherRoom(watcher, roomName, asJoin=True)
@ -65,13 +71,16 @@ class SyncFactory(Factory):
self.sendJoinMessage(watcher)
else:
self.sendRoomSwitchMessage(watcher)
if RoomPasswordProvider.isControlledRoom(roomName):
for controller in watcher.getRoom().getControllers():
watcher.sendControlledRoomAuthStatus(True, controller, roomName)
def sendRoomSwitchMessage(self, watcher):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None)
self._roomManager.broadcast(watcher, l)
def removeWatcher(self, watcher):
if watcher.getRoom():
if watcher and watcher.getRoom():
self.sendLeftMessage(watcher)
self._roomManager.removeWatcher(watcher)
@ -83,21 +92,40 @@ class SyncFactory(Factory):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"joined": True}) if w != watcher else None
self._roomManager.broadcast(watcher, l)
def sendFileUpdate(self, watcher, file_):
def sendFileUpdate(self, watcher):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), watcher.getFile(), None)
self._roomManager.broadcast(watcher, l)
def forcePositionUpdate(self, room, watcher, doSeek):
def forcePositionUpdate(self, watcher, doSeek, watcherPauseState):
room = watcher.getRoom()
paused, position = room.isPaused(), watcher.getPosition()
setBy = watcher
room.setPosition(watcher.getPosition(), setBy)
l = lambda w: w.sendState(position, paused, doSeek, setBy, True)
self._roomManager.broadcastRoom(watcher, l)
if room.canControl(watcher):
paused, position = room.isPaused(), watcher.getPosition()
setBy = watcher
l = lambda w: w.sendState(position, paused, doSeek, setBy, True)
room.setPosition(watcher.getPosition(), setBy)
self._roomManager.broadcastRoom(watcher, l)
else:
watcher.sendState(room.getPosition(), watcherPauseState, False, watcher, True) # Fixes BC break with 1.2.x
watcher.sendState(room.getPosition(), room.isPaused(), True, room.getSetBy(), True)
def getAllWatchersForUser(self, forUser):
return self._roomManager.getAllWatchersForUser(forUser)
def authRoomController(self, watcher, password, roomBaseName=None):
room = watcher.getRoom()
roomName = roomBaseName if roomBaseName else room.getName()
try:
success = RoomPasswordProvider.check(roomName, password, self._salt)
if success:
watcher.getRoom().addController(watcher)
self._roomManager.broadcast(watcher, lambda w: w.sendControlledRoomAuthStatus(success, watcher.getName(), room._name))
except NotControlledRoom:
newName = RoomPasswordProvider.getControlledRoomName(roomName, password, self._salt)
watcher.sendNewControlledRoom(newName, password)
except ValueError:
self._roomManager.broadcastRoom(watcher, lambda w: w.sendControlledRoomAuthStatus(False, watcher.getName(), room._name))
class RoomManager(object):
def __init__(self):
self._rooms = {}
@ -135,7 +163,10 @@ class RoomManager(object):
if roomName in self._rooms:
return self._rooms[roomName]
else:
room = Room(roomName)
if RoomPasswordProvider.isControlledRoom(roomName):
room = ControlledRoom(roomName)
else:
room = Room(roomName)
self._rooms[roomName] = room
return room
@ -228,6 +259,44 @@ class Room(object):
def getSetBy(self):
return self._setBy
def canControl(self, watcher):
return True
class ControlledRoom(Room):
def __init__(self, name):
Room.__init__(self, name)
self._controllers = {}
def getPosition(self):
if self._controllers:
watcher = min(self._controllers.values())
self._setBy = watcher
return watcher.getPosition()
else:
return 0
def addController(self, watcher):
self._controllers[watcher.getName()] = watcher
def removeWatcher(self, watcher):
Room.removeWatcher(self, watcher)
if watcher.getName() in self._controllers:
del self._controllers[watcher.getName()]
def setPaused(self, paused=Room.STATE_PAUSED, setBy=None):
if self.canControl(setBy):
Room.setPaused(self, paused, setBy)
def setPosition(self, position, setBy=None):
if self.canControl(setBy):
Room.setPosition(self, position, setBy)
def canControl(self, watcher):
return watcher.getName() in self._controllers
def getControllers(self):
return self._controllers
class Watcher(object):
def __init__(self, server, connector, name):
self._server = server
@ -243,7 +312,7 @@ class Watcher(object):
def setFile(self, file_):
self._file = file_
self._server.sendFileUpdate(self, file_)
self._server.sendFileUpdate(self)
def setRoom(self, room):
self._room = room
@ -277,16 +346,22 @@ class Watcher(object):
def sendSetting(self, user, room, file_, event):
self._connector.sendUserSetting(user, room, file_, event)
def sendNewControlledRoom(self, roomBaseName, password):
self._connector.sendNewControlledRoom(roomBaseName, password)
def sendControlledRoomAuthStatus(self, success, username, room):
self._connector.sendControlledRoomAuthStatus(success, username, room)
def __lt__(self, b):
if self.getPosition() is None or self._file is None:
return False
if b.getPosition is None or b._file is None:
if b.getPosition() is None or b.getFile() is None:
return True
return self.getPosition() < b.getPosition()
def _scheduleSendState(self):
self._sendStateTimer = task.LoopingCall(self._askForStateUpdate)
self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL, True)
self._sendStateTimer.start(constants.SERVER_STATE_INTERVAL)
def _askForStateUpdate(self, doSeek=False, forcedUpdate=False):
self._server.sendState(self, doSeek, forcedUpdate)
@ -313,18 +388,25 @@ class Watcher(object):
return False
return self._room.isPaused() and not paused or not self._room.isPaused() and paused
def _updatePositionByAge(self, messageAge, paused, position):
if not paused:
position += messageAge
return position
def updateState(self, position, paused, doSeek, messageAge):
pauseChanged = self.__hasPauseChanged(paused)
self._lastUpdatedOn = time.time()
if pauseChanged:
self.getRoom().setPaused(Room.STATE_PAUSED if paused else Room.STATE_PLAYING, self)
if position is not None:
if not paused:
position += messageAge
position = self._updatePositionByAge(messageAge, paused, position)
self.setPosition(position)
if doSeek or pauseChanged:
self._server.forcePositionUpdate(self._room, self, doSeek)
self._server.forcePositionUpdate(self, doSeek, paused)
def isController(self):
return RoomPasswordProvider.isControlledRoom(self._room.getName()) \
and self._room.canControl(self)
class ConfigurationGetter(object):
def getConfiguration(self):
@ -340,4 +422,5 @@ class ConfigurationGetter(object):
self._argparser.add_argument('--port', metavar='port', type=str, nargs='?', help=getMessage("server-port-argument"))
self._argparser.add_argument('--password', metavar='password', type=str, nargs='?', help=getMessage("server-password-argument"))
self._argparser.add_argument('--isolate-rooms', action='store_true', help=getMessage("server-isolate-room-argument"))
self._argparser.add_argument('--motd-file', metavar='file', type=str, nargs='?', help=getMessage("server-motd-argument"))
self._argparser.add_argument('--salt', metavar='salt', type=str, nargs='?', help=getMessage("server-salt-argument"))
self._argparser.add_argument('--motd-file', metavar='file', type=str, nargs='?', help=getMessage("server-motd-argument"))

View File

@ -36,8 +36,10 @@ class ConfigurationGetter(object):
"playerClass": None,
"slowdownThreshold": constants.DEFAULT_SLOWDOWN_KICKIN_THRESHOLD,
"rewindThreshold": constants.DEFAULT_REWIND_THRESHOLD,
"fastforwardThreshold": constants.DEFAULT_FASTFORWARD_THRESHOLD,
"rewindOnDesync": True,
"slowOnDesync": True,
"fastforwardOnDesync": True,
"dontSlowDownWithMe": False,
"filenamePrivacyMode": constants.PRIVACY_SENDRAW_MODE,
"filesizePrivacyMode": constants.PRIVACY_SENDRAW_MODE,
@ -50,9 +52,8 @@ class ConfigurationGetter(object):
"showSlowdownOSD" : True,
"showDifferentRoomOSD" : False,
"showSameRoomOSD" : True,
"showNonControllerOSD" : False,
"showContactInfo" : True,
"showButtonLabels" : True,
"showTooltips" : True,
"showDurationNotification" : True
}
@ -87,21 +88,21 @@ class ConfigurationGetter(object):
"showSlowdownOSD",
"showDifferentRoomOSD",
"showSameRoomOSD",
"showNonControllerOSD",
"showContactInfo" ,
"showButtonLabels",
"showTooltips",
"showDurationNotification"
]
self._numeric = [
"slowdownThreshold",
"rewindThreshold"
"rewindThreshold",
"fastforwardThreshold",
]
self._iniStructure = {
"server_data": ["host", "port", "password"],
"client_settings": ["name", "room", "playerPath", "slowdownThreshold", "rewindThreshold", "slowOnDesync", "rewindOnDesync", "dontSlowDownWithMe", "forceGuiPrompt", "filenamePrivacyMode", "filesizePrivacyMode", "pauseOnLeave"],
"gui": ["showOSD", "showOSDWarnings", "showSlowdownOSD", "showDifferentRoomOSD", "showSameRoomOSD", "showContactInfo" , "showButtonLabels", "showTooltips", "showDurationNotification"],
"client_settings": ["name", "room", "playerPath", "slowdownThreshold", "rewindThreshold", "fastforwardThreshold", "slowOnDesync", "rewindOnDesync", "fastforwardOnDesync", "dontSlowDownWithMe", "forceGuiPrompt", "filenamePrivacyMode", "filesizePrivacyMode", "pauseOnLeave"],
"gui": ["showOSD", "showOSDWarnings", "showSlowdownOSD", "showDifferentRoomOSD", "showSameRoomOSD", "showNonControllerOSD", "showContactInfo" , "showDurationNotification"],
"general": ["language"]
}
@ -143,20 +144,20 @@ class ConfigurationGetter(object):
if player:
self._config["playerClass"] = player
else:
raise InvalidConfigValue("Player path is not set properly")
if player.__name__ in ['MpvPlayer', 'MplayerPlayer']:
if not self._config['file']:
raise InvalidConfigValue("File must be selected before starting your player")
raise InvalidConfigValue(getMessage("player-path-config-error"))
playerPathErrors = player.getPlayerPathErrors(self._config["playerPath"], self._config['file'] if self._config['file'] else None)
if playerPathErrors:
raise InvalidConfigValue(playerPathErrors)
elif key == "host":
self._config["host"], self._config["port"] = self._splitPortAndHost(self._config["host"])
hostNotValid = (self._config["host"] == "" or self._config["host"] is None)
portNotValid = (_isPortValid(self._config["port"]) == False)
if hostNotValid:
raise InvalidConfigValue("Hostname can't be empty")
raise InvalidConfigValue(getMessage("no-hostname-config-error"))
elif portNotValid:
raise InvalidConfigValue("Port must be valid")
raise InvalidConfigValue(getMessage("invalid-port-config-error"))
elif self._config[key] == "" or self._config[key] is None:
raise InvalidConfigValue("{} can't be empty".format(key.capitalize()))
raise InvalidConfigValue(getMessage("empty-value-config-error").format(key.capitalize()))
def _overrideConfigWithArgs(self, args):
for key, val in vars(args).items():
@ -385,4 +386,4 @@ class SafeConfigParserUnicode(SafeConfigParser):
if (value is not None) or (self._optcre == self.OPTCRE):
key = " = ".join((key, unicode(value).replace('\n', '\n\t')))
fp.write("%s\n" % key)
fp.write("\n")
fp.write("\n")

View File

@ -5,7 +5,7 @@ from syncplay.players.playerFactory import PlayerFactory
import os
import sys
from syncplay.messages import getMessage
from syncplay.messages import getMessage, getLanguages, setLanguage
from syncplay import constants
class GuiConfiguration:
@ -15,7 +15,6 @@ class GuiConfiguration:
self._availablePlayerPaths = []
self.error = error
def run(self):
if QCoreApplication.instance() is None:
self.app = QtGui.QApplication(sys.argv)
@ -71,36 +70,63 @@ class ConfigDialog(QtGui.QDialog):
def openHelp(self):
self.QtGui.QDesktopServices.openUrl("http://syncplay.pl/guide/client/")
def _isURL(self, path):
if path is None:
return False
if "http://" in path:
return True
def safenormcaseandpath(self, path):
if self._isURL(path):
return path
else:
return os.path.normcase(os.path.normpath(path))
def _tryToFillPlayerPath(self, playerpath, playerpathlist):
settings = QSettings("Syncplay", "PlayerList")
settings.beginGroup("PlayerList")
savedPlayers = settings.value("PlayerList", [])
if not isinstance(savedPlayers, list):
savedPlayers = []
playerpathlist = list(set(os.path.normcase(os.path.normpath(path)) for path in set(playerpathlist + savedPlayers)))
else:
for i, savedPlayer in enumerate(savedPlayers):
savedPlayers[i] = self.safenormcaseandpath(savedPlayer)
playerpathlist = list(set(playerpathlist + savedPlayers))
settings.endGroup()
foundpath = ""
if playerpath != None and playerpath != "":
if not os.path.isfile(playerpath):
expandedpath = PlayerFactory().getExpandedPlayerPathByPath(playerpath)
if expandedpath != None and os.path.isfile(expandedpath):
playerpath = expandedpath
if os.path.isfile(playerpath):
if self._isURL(playerpath):
foundpath = playerpath
self.executablepathCombobox.addItem(foundpath)
else:
if not os.path.isfile(playerpath):
expandedpath = PlayerFactory().getExpandedPlayerPathByPath(playerpath)
if expandedpath != None and os.path.isfile(expandedpath):
playerpath = expandedpath
if os.path.isfile(playerpath):
foundpath = playerpath
self.executablepathCombobox.addItem(foundpath)
for path in playerpathlist:
if os.path.isfile(path) and os.path.normcase(os.path.normpath(path)) != os.path.normcase(os.path.normpath(foundpath)):
if self._isURL(path):
if foundpath == "":
foundpath = path
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)):
self.executablepathCombobox.addItem(path)
if foundpath == "":
foundpath = path
if foundpath != "":
settings.beginGroup("PlayerList")
playerpathlist.append(os.path.normcase(os.path.normpath(foundpath)))
settings.setValue("PlayerList", list(set(os.path.normcase(os.path.normpath(path)) for path in set(playerpathlist))))
playerpathlist.append(self.safenormcaseandpath(foundpath))
settings.setValue("PlayerList", list(set(playerpathlist)))
settings.endGroup()
return foundpath
@ -113,6 +139,9 @@ class ConfigDialog(QtGui.QDialog):
else:
self.executableiconLabel.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage()))
def languageChanged(self):
setLanguage(unicode(self.languageCombobox.itemData(self.languageCombobox.currentIndex())))
QtGui.QMessageBox.information(self, "Syncplay", getMessage("language-changed-msgbox-label"))
def browsePlayerpath(self):
options = QtGui.QFileDialog.Options()
@ -194,7 +223,8 @@ class ConfigDialog(QtGui.QDialog):
self.config['host'] = self.hostTextbox.text() if ":" in self.hostTextbox.text() else self.hostTextbox.text() + ":" + unicode(constants.DEFAULT_PORT)
else:
self.config['host'] = None
self.config['playerPath'] = unicode(self.executablepathCombobox.currentText())
self.config['playerPath'] = unicode(self.safenormcaseandpath(self.executablepathCombobox.currentText()))
self.config['language'] = unicode(self.languageCombobox.itemData(self.languageCombobox.currentIndex()))
if self.mediapathTextbox.text() == "":
self.config['file'] = None
elif os.path.isfile(os.path.abspath(self.mediapathTextbox.text())):
@ -206,8 +236,11 @@ class ConfigDialog(QtGui.QDialog):
self.slowdownThresholdSpinbox.value = constants.DEFAULT_SLOWDOWN_KICKIN_THRESHOLD
if not self.rewindThresholdSpinbox.text:
self.rewindThresholdSpinbox.value = constants.DEFAULT_REWIND_THRESHOLD
if not self.fastforwardThresholdSpinbox.text:
self.fastforwardThresholdSpinbox.value = constants.DEFAULT_FASTFORWARD_THRESHOLD
self.config['slowdownThreshold'] = self.slowdownThresholdSpinbox.value()
self.config['rewindThreshold'] = self.rewindThresholdSpinbox.value()
self.config['fastforwardThreshold'] = self.fastforwardThresholdSpinbox.value()
self.pressedclosebutton = True
self.close()
@ -216,8 +249,10 @@ class ConfigDialog(QtGui.QDialog):
def closeEvent(self, event):
if self.pressedclosebutton == False:
sys.exit()
raise GuiConfiguration.WindowClosed
event.accept()
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
sys.exit()
def dragEnterEvent(self, event):
data = event.mimeData()
@ -229,10 +264,7 @@ class ConfigDialog(QtGui.QDialog):
data = event.mimeData()
urls = data.urls()
if urls and urls[0].scheme() == 'file':
if sys.platform.startswith('win'):
dropfilepath = unicode(urls[0].path())[1:] # Removes starting slash
else:
dropfilepath = unicode(urls[0].path())
dropfilepath = os.path.abspath(unicode(event.mimeData().urls()[0].toLocalFile()))
if dropfilepath[-4:].lower() == ".exe":
self.executablepathCombobox.setEditText(dropfilepath)
else:
@ -347,6 +379,7 @@ class ConfigDialog(QtGui.QDialog):
self.connectionSettingsLayout.addWidget(self.defaultroomLabel, 3, 0)
self.connectionSettingsLayout.addWidget(self.defaultroomTextbox, 3, 1)
self.connectionSettingsGroup.setLayout(self.connectionSettingsLayout)
self.connectionSettingsGroup.setMaximumHeight(self.connectionSettingsGroup.minimumSizeHint().height())
self.mediaplayerSettingsGroup = QtGui.QGroupBox(getMessage("media-setting-title"))
self.executableiconImage = QtGui.QImage()
@ -398,8 +431,11 @@ class ConfigDialog(QtGui.QDialog):
self.errorLabel.setAlignment(Qt.AlignCenter)
self.basicOptionsLayout.addWidget(self.errorLabel, 0, 0)
self.connectionSettingsGroup.setMaximumHeight(self.connectionSettingsGroup.minimumSizeHint().height())
self.basicOptionsLayout.setAlignment(Qt.AlignTop)
self.basicOptionsLayout.addWidget(self.connectionSettingsGroup)
self.basicOptionsLayout.addSpacing(12)
self.basicOptionsLayout.addSpacing(17)
self.mediaplayerSettingsGroup.setMaximumHeight(self.mediaplayerSettingsGroup.minimumSizeHint().height())
self.basicOptionsLayout.addWidget(self.mediaplayerSettingsGroup)
self.basicOptionsFrame.setLayout(self.basicOptionsLayout)
@ -409,7 +445,7 @@ class ConfigDialog(QtGui.QDialog):
self.syncSettingsFrame = QtGui.QFrame()
self.syncSettingsLayout = QtGui.QVBoxLayout()
self.desyncSettingsGroup = QtGui.QGroupBox("If others are lagging behind...")
self.desyncSettingsGroup = QtGui.QGroupBox(getMessage("sync-lagging-title"))
self.desyncOptionsFrame = QtGui.QFrame()
self.desyncSettingsOptionsLayout = QtGui.QHBoxLayout()
config = self.config
@ -418,6 +454,8 @@ class ConfigDialog(QtGui.QDialog):
self.slowdownCheckbox.setObjectName("slowOnDesync")
self.rewindCheckbox = QCheckBox(getMessage("rewindondesync-label"))
self.rewindCheckbox.setObjectName("rewindOnDesync")
self.fastforwardCheckbox = QCheckBox(getMessage("fastforwardondesync-label"))
self.fastforwardCheckbox.setObjectName("fastforwardOnDesync")
self.spaceLabel = QLabel()
self.spaceLabel.setFixedHeight(5)
@ -429,7 +467,7 @@ class ConfigDialog(QtGui.QDialog):
self.desyncFrame.setMidLineWidth(0)
self.slowdownThresholdLabel = QLabel(getMessage("slowdown-threshold-label"), self)
self.slowdownThresholdLabel.setStyleSheet(constants.STYLE_SUBLABEL.format(self.posixresourcespath + "bullet_black.png"))
self.slowdownThresholdLabel.setStyleSheet(constants.STYLE_SUBLABEL.format(self.posixresourcespath + "chevrons_right.png"))
self.slowdownThresholdSpinbox = QDoubleSpinBox()
try:
@ -446,7 +484,7 @@ class ConfigDialog(QtGui.QDialog):
self.slowdownThresholdSpinbox.adjustSize()
self.rewindThresholdLabel = QLabel(getMessage("rewind-threshold-label"), self)
self.rewindThresholdLabel.setStyleSheet(constants.STYLE_SUBLABEL.format(self.posixresourcespath + "bullet_black.png"))
self.rewindThresholdLabel.setStyleSheet(constants.STYLE_SUBLABEL.format(self.posixresourcespath + "chevrons_right.png"))
self.rewindThresholdSpinbox = QDoubleSpinBox()
try:
rewindThreshold = float(config['rewindThreshold'])
@ -461,6 +499,22 @@ class ConfigDialog(QtGui.QDialog):
self.rewindThresholdSpinbox.setSuffix(getMessage("seconds-suffix"))
self.rewindThresholdSpinbox.adjustSize()
self.fastforwardThresholdLabel = QLabel(getMessage("fastforward-threshold-label"), self)
self.fastforwardThresholdLabel.setStyleSheet(constants.STYLE_SUBLABEL.format(self.posixresourcespath + "chevrons_right.png"))
self.fastforwardThresholdSpinbox = QDoubleSpinBox()
try:
fastforwardThreshold = float(config['fastforwardThreshold'])
self.fastforwardThresholdSpinbox.setValue(fastforwardThreshold)
if fastforwardThreshold < constants.MINIMUM_FASTFORWARD_THRESHOLD:
constants.MINIMUM_FASTFORWARD_THRESHOLD = fastforwardThreshold
except ValueError:
self.fastforwardThresholdSpinbox.setValue(constants.DEFAULT_FASTFORWARD_THRESHOLD)
self.fastforwardThresholdSpinbox.setSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum)
self.fastforwardThresholdSpinbox.setMinimum(constants.MINIMUM_FASTFORWARD_THRESHOLD)
self.fastforwardThresholdSpinbox.setSingleStep(0.1)
self.fastforwardThresholdSpinbox.setSuffix(getMessage("seconds-suffix"))
self.fastforwardThresholdSpinbox.adjustSize()
self.slowdownThresholdLabel.setObjectName("slowdown-threshold")
self.slowdownThresholdSpinbox.setObjectName("slowdown-threshold")
self.rewindThresholdLabel.setObjectName("rewind-threshold")
@ -483,19 +537,29 @@ class ConfigDialog(QtGui.QDialog):
self.syncSettingsLayout.addWidget(self.desyncSettingsGroup)
self.desyncFrame.setLayout(self.syncSettingsLayout)
self.othersyncSettingsGroup = QtGui.QGroupBox("Other sync options")
self.othersyncSettingsGroup = QtGui.QGroupBox(getMessage("sync-other-title"))
self.othersyncOptionsFrame = QtGui.QFrame()
self.othersyncSettingsLayout = QtGui.QGridLayout()
self.dontslowwithmeCheckbox = QCheckBox(getMessage("dontslowdownwithme-label"))
self.pauseonleaveCheckbox = QCheckBox(getMessage("pauseonleave-label"))
self.othersyncSettingsLayout.addWidget(self.dontslowwithmeCheckbox)
self.othersyncSettingsLayout.addWidget(self.pauseonleaveCheckbox)
self.dontslowwithmeCheckbox.setObjectName("dontSlowDownWithMe")
self.othersyncSettingsLayout.addWidget(self.pauseonleaveCheckbox, 1, 0, 1, 2, Qt.AlignLeft)
self.pauseonleaveCheckbox.setObjectName("pauseOnLeave")
self.dontslowwithmeCheckbox = QCheckBox(getMessage("dontslowdownwithme-label"))
self.dontslowwithmeCheckbox.setObjectName("dontSlowDownWithMe")
self.othersyncSettingsLayout.addWidget(self.dontslowwithmeCheckbox, 2, 0, 1, 2, Qt.AlignLeft)
self.fastforwardThresholdLabel.setObjectName("fastforward-threshold")
self.fastforwardThresholdSpinbox.setObjectName("fastforward-threshold")
self.othersyncSettingsLayout.addWidget(self.fastforwardCheckbox, 3, 0,1,2, Qt.AlignLeft)
self.othersyncSettingsLayout.addWidget(self.fastforwardThresholdLabel, 4, 0, 1, 1, Qt.AlignLeft)
self.othersyncSettingsLayout.addWidget(self.fastforwardThresholdSpinbox, 4, 1, Qt.AlignLeft)
self.subitems['fastforwardOnDesync'] = ["fastforward-threshold"]
self.othersyncSettingsGroup.setLayout(self.othersyncSettingsLayout)
self.othersyncSettingsGroup.setMaximumHeight(self.othersyncSettingsGroup.minimumSizeHint().height())
self.syncSettingsLayout.addWidget(self.othersyncSettingsGroup)
self.syncSettingsFrame.setLayout(self.syncSettingsLayout)
@ -508,7 +572,7 @@ class ConfigDialog(QtGui.QDialog):
self.messageLayout = QtGui.QVBoxLayout()
# OSD
self.osdSettingsGroup = QtGui.QGroupBox("On-screen Display settings")
self.osdSettingsGroup = QtGui.QGroupBox(getMessage("messages-osd-title"))
self.osdSettingsLayout = QtGui.QVBoxLayout()
self.osdSettingsFrame = QtGui.QFrame()
@ -518,54 +582,68 @@ class ConfigDialog(QtGui.QDialog):
self.showSameRoomOSDCheckbox = QCheckBox(getMessage("showsameroomosd-label"))
self.showSameRoomOSDCheckbox.setObjectName("showSameRoomOSD")
self.showSameRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "bullet_black.png"))
self.showSameRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.showSameRoomOSDCheckbox)
self.showNonControllerOSDCheckbox = QCheckBox(getMessage("shownoncontrollerosd-label"))
self.showNonControllerOSDCheckbox.setObjectName("showNonControllerOSD")
self.showNonControllerOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.showNonControllerOSDCheckbox)
self.showDifferentRoomOSDCheckbox = QCheckBox(getMessage("showdifferentroomosd-label"))
self.showDifferentRoomOSDCheckbox.setObjectName("showDifferentRoomOSD")
self.showDifferentRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "bullet_black.png"))
self.showDifferentRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.showDifferentRoomOSDCheckbox)
self.slowdownOSDCheckbox = QCheckBox(getMessage("showslowdownosd-label"))
self.slowdownOSDCheckbox.setObjectName("showSlowdownOSD")
self.slowdownOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "bullet_black.png"))
self.slowdownOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.slowdownOSDCheckbox)
self.showOSDWarningsCheckbox = QCheckBox(getMessage("showosdwarnings-label"))
self.showOSDWarningsCheckbox.setObjectName("showOSDWarnings")
self.showOSDWarningsCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "bullet_black.png"))
self.showOSDWarningsCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.showOSDWarningsCheckbox)
self.subitems['showOSD'] = ["showSameRoomOSD", "showDifferentRoomOSD", "showSlowdownOSD", "showOSDWarnings"]
self.osdSettingsGroup.setLayout(self.osdSettingsLayout)
self.osdSettingsGroup.setMaximumHeight(self.osdSettingsGroup.minimumSizeHint().height())
self.osdSettingsLayout.setAlignment(Qt.AlignTop)
self.messageLayout.addWidget(self.osdSettingsGroup)
# Other display
self.displaySettingsGroup = QtGui.QGroupBox("Other display settings")
self.displaySettingsLayout = QtGui.QVBoxLayout()
self.displaySettingsGroup = QtGui.QGroupBox(getMessage("messages-other-title"))
self.displaySettingsLayout = QtGui.QGridLayout()
self.displaySettingsLayout.setAlignment(Qt.AlignTop)
self.displaySettingsFrame = QtGui.QFrame()
self.showDurationNotificationCheckbox = QCheckBox(getMessage("showdurationnotification-label"))
self.showDurationNotificationCheckbox.setObjectName("showDurationNotification")
self.displaySettingsLayout.addWidget(self.showDurationNotificationCheckbox)
self.displaySettingsLayout.addWidget(self.showDurationNotificationCheckbox, 0, 0, 1, 2)
self.showcontactinfoCheckbox = QCheckBox(getMessage("showcontactinfo-label"))
self.showcontactinfoCheckbox.setObjectName("showContactInfo")
self.displaySettingsLayout.addWidget(self.showcontactinfoCheckbox)
self.displaySettingsLayout.addWidget(self.showcontactinfoCheckbox, 1, 0, 1, 2)
self.showButtonLabelsCheckbox = QCheckBox(getMessage("showbuttonlabels-label"))
self.showButtonLabelsCheckbox.setObjectName("showButtonLabels")
self.displaySettingsLayout.addWidget(self.showButtonLabelsCheckbox)
self.languageLabel = QLabel(getMessage("language-label"), self)
self.languageCombobox = QtGui.QComboBox(self)
self.languages = getLanguages()
for lang in self.languages:
self.languageCombobox.addItem(self.languages[lang], lang)
if lang == self.config['language']:
self.languageCombobox.setCurrentIndex(self.languageCombobox.count()-1)
self.languageCombobox.currentIndexChanged.connect(self.languageChanged)
self.displaySettingsLayout.addWidget(self.languageLabel, 2, 0, 1, 1)
self.displaySettingsLayout.addWidget(self.languageCombobox, 2, 1, 1, 1)
self.showTooltipsCheckbox = QCheckBox(getMessage("showtooltips-label"))
self.showTooltipsCheckbox.setObjectName("showTooltips")
self.displaySettingsLayout.addWidget(self.showTooltipsCheckbox)
self.languageLabel.setObjectName("language")
self.languageCombobox.setObjectName("language")
self.displaySettingsGroup.setLayout(self.displaySettingsLayout)
self.displaySettingsGroup.setMaximumHeight(self.displaySettingsGroup.minimumSizeHint().height())
self.displaySettingsLayout.setAlignment(Qt.AlignTop)
self.messageLayout.addWidget(self.displaySettingsGroup)
@ -574,7 +652,7 @@ class ConfigDialog(QtGui.QDialog):
self.stackedLayout.addWidget(self.messageFrame)
def addPrivacyTab(self):
self.privacySettingsGroup = QtGui.QGroupBox("Privacy settings")
self.privacySettingsGroup = QtGui.QGroupBox(getMessage("privacy-title"))
self.privacySettingsLayout = QtGui.QVBoxLayout()
self.privacySettingsFrame = QtGui.QFrame()
self.privacyFrame = QtGui.QFrame()
@ -703,6 +781,7 @@ class ConfigDialog(QtGui.QDialog):
self.ensureTabListIsVisible()
self.setFixedWidth(self.minimumSizeHint().width())
self.executablepathCombobox.setFixedWidth(self.mediapathTextbox.width())
self.languageLabel.setFixedWidth(self.languageLabel.width())
def clearGUIData(self, leaveMore=False):
settings = QSettings("Syncplay", "PlayerList")

View File

@ -35,7 +35,10 @@ class ConsoleUI(threading.Thread):
self._executeCommand(data)
except EOFError:
pass
def updateRoomName(self, room=""):
pass
def promptFor(self, prompt=">", message=""):
if message <> "":
print(message)
@ -49,14 +52,17 @@ class ConsoleUI(threading.Thread):
message = u"In room '{}':".format(room)
self.showMessage(message, True)
for user in rooms[room]:
username = "*<{}>*".format(user.username) if user == currentUser else "<{}>".format(user.username)
userflags = u""
if user.isController():
userflags = userflags + u"(Controller) "
username = userflags + u"*<{}>*".format(user.username) if user == currentUser else userflags + u"<{}>".format(user.username)
if user.file:
message = u"{} is playing:".format(username)
self.showMessage(message, True)
message = u" File: '{}' ({})".format(user.file['name'], formatTime(user.file['duration']))
if currentUser.file:
if user.file['name'] == currentUser.file['name'] and user.file['size'] != currentUser.file['size']:
message += " (their file size is different from yours!)"
message += u" (their file size is different from yours!)"
self.showMessage(message, True)
else:
message = u"{} is not playing a file".format(username)
@ -74,7 +80,7 @@ class ConsoleUI(threading.Thread):
def showDebugMessage(self, message):
print(message)
def showErrorMessage(self, message, criticalerror = False):
print("ERROR:\t" + message)
@ -134,6 +140,15 @@ class ConsoleUI(threading.Thread):
self._syncplayClient.setRoom(room)
self._syncplayClient.sendRoom()
elif command.group('command') in constants.COMMANDS_CREATE:
roombasename = command.group('parameter')
if roombasename == None:
roombasename = self._syncplayClient.getRoom()
roombasename = utils.stripRoomName(roombasename)
self._syncplayClient.createControlledRoom(roombasename)
elif command.group('command') in constants.COMMANDS_AUTH:
controlpassword = command.group('parameter')
self._syncplayClient.identifyAsController(controlpassword)
else:
if self._tryAdvancedCommands(data):
return
@ -146,6 +161,8 @@ class ConsoleUI(threading.Thread):
self.showMessage(getMessage("commandlist-notification/pause"), True)
self.showMessage(getMessage("commandlist-notification/seek"), True)
self.showMessage(getMessage("commandlist-notification/help"), True)
self.showMessage(getMessage("commandlist-notification/create"), True)
self.showMessage(getMessage("commandlist-notification/auth"), True)
self.showMessage(getMessage("syncplay-version-notification").format(syncplay.version), True)
self.showMessage(getMessage("more-info-notification").format(syncplay.projectURL), True)

View File

@ -1,15 +1,74 @@
from PySide import QtGui #@UnresolvedImport
from PySide.QtCore import Qt, QSettings, QSize, QPoint #@UnresolvedImport
from PySide import QtGui
from PySide.QtCore import Qt, QSettings, QSize, QPoint
from syncplay import utils, constants, version
from syncplay.messages import getMessage
import sys
import time
import re
import os
import threading
from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration
from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration, RoomPasswordProvider, formatSize
class UserlistItemDelegate(QtGui.QStyledItemDelegate):
def __init__(self):
QtGui.QStyledItemDelegate.__init__(self)
def sizeHint(self, option, index):
size = QtGui.QStyledItemDelegate.sizeHint(self, option, index)
if (index.column() == 0):
size.setWidth(size.width() + constants.USERLIST_GUI_USERNAME_OFFSET)
return size
def paint(self, itemQPainter, optionQStyleOptionViewItem, indexQModelIndex):
column = indexQModelIndex.column()
if column == 0:
currentQAbstractItemModel = indexQModelIndex.model()
itemQModelIndex = currentQAbstractItemModel.index(indexQModelIndex.row(), 0, indexQModelIndex.parent())
if sys.platform.startswith('win'):
resourcespath = utils.findWorkingDir() + "\\resources\\"
else:
resourcespath = utils.findWorkingDir() + "/resources/"
controlIconQPixmap = QtGui.QPixmap(resourcespath + "user_key.png")
tickIconQPixmap = QtGui.QPixmap(resourcespath + "tick.png")
crossIconQPixmap = QtGui.QPixmap(resourcespath + "cross.png")
roomController = currentQAbstractItemModel.data(itemQModelIndex, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE)
userReady = currentQAbstractItemModel.data(itemQModelIndex, Qt.UserRole + constants.USERITEM_READY_ROLE)
if roomController and not controlIconQPixmap.isNull():
itemQPainter.drawPixmap (
optionQStyleOptionViewItem.rect.x()+6,
optionQStyleOptionViewItem.rect.y(),
controlIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio))
if userReady and not tickIconQPixmap.isNull():
itemQPainter.drawPixmap (
(optionQStyleOptionViewItem.rect.x()-10),
optionQStyleOptionViewItem.rect.y(),
tickIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio))
elif userReady == False and not crossIconQPixmap.isNull():
itemQPainter.drawPixmap (
(optionQStyleOptionViewItem.rect.x()-10),
optionQStyleOptionViewItem.rect.y(),
crossIconQPixmap.scaled(16, 16, Qt.KeepAspectRatio))
isUserRow = indexQModelIndex.parent() != indexQModelIndex.parent().parent()
if isUserRow:
optionQStyleOptionViewItem.rect.setX(optionQStyleOptionViewItem.rect.x()+constants.USERLIST_GUI_USERNAME_OFFSET)
QtGui.QStyledItemDelegate.paint(self, itemQPainter, optionQStyleOptionViewItem, indexQModelIndex)
class MainWindow(QtGui.QMainWindow):
class topSplitter(QtGui.QSplitter):
def createHandle(self):
return self.topSplitterHandle(self.orientation(), self)
class topSplitterHandle(QtGui.QSplitterHandle):
def mouseReleaseEvent(self, event):
QtGui.QSplitterHandle.mouseReleaseEvent(self, event)
self.parent().parent().parent().updateListGeometry()
def mouseMoveEvent(self, event):
QtGui.QSplitterHandle.mouseMoveEvent(self, event)
self.parent().parent().parent().updateListGeometry()
def addClient(self, client):
self._syncplayClient = client
self.roomInput.setText(self._syncplayClient.getRoom())
@ -17,29 +76,14 @@ class MainWindow(QtGui.QMainWindow):
try:
if self.contactLabel and not self.config['showContactInfo']:
self.contactLabel.hide()
if not self.config['showButtonLabels']:
if constants.MERGE_PLAYPAUSE_BUTTONS:
self.playpauseButton.setText("")
else:
self.playButton.setText("")
self.playButton.setFixedWidth(self.playButton.minimumSizeHint().width())
self.pauseButton.setText("")
self.pauseButton.setFixedWidth(self.pauseButton.minimumSizeHint().width())
self.roomButton.setText("")
self.roomButton.setFixedWidth(self.roomButton.minimumSizeHint().width())
self.seekButton.setText("")
self.seekButton.setFixedWidth(self.seekButton.minimumSizeHint().width())
self.unseekButton.setText("")
self.unseekButton.setFixedWidth(self.unseekButton.minimumSizeHint().width())
self.roomGroup.setFixedWidth(self.roomGroup.sizeHint().width())
self.seekGroup.setFixedWidth(self.seekGroup.minimumSizeHint().width())
self.miscGroup.setFixedWidth(self.miscGroup.minimumSizeHint().width())
if not constants.SHOW_BUTTON_LABELS:
self.hideRoomSeekLabels()
self.hideMiscLabels()
except ():
pass
def promptFor(self, prompt=">", message=""):
#TODO: Prompt user
# TODO: Prompt user
return None
def showMessage(self, message, noTimestamp=False):
@ -52,89 +96,132 @@ class MainWindow(QtGui.QMainWindow):
self.newMessage(message + "<br />")
else:
self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "<br />")
def showUserList(self, currentUser, rooms):
self._usertreebuffer = QtGui.QStandardItemModel()
self._usertreebuffer.setColumnCount(2)
self._usertreebuffer.setHorizontalHeaderLabels((getMessage("roomuser-heading-label"),getMessage("fileplayed-heading-label")))
self._usertreebuffer.setHorizontalHeaderLabels(
(getMessage("roomuser-heading-label"), getMessage("size-heading-label"), getMessage("duration-heading-label"), getMessage("filename-heading-label") ))
usertreeRoot = self._usertreebuffer.invisibleRootItem()
for room in rooms:
roomitem = QtGui.QStandardItem(room)
font = QtGui.QFont()
font.setItalic(True)
if room == currentUser.room:
font = QtGui.QFont()
font.setWeight(QtGui.QFont.Bold)
roomitem.setFont(font)
blankitem = QtGui.QStandardItem("")
roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable)
blankitem.setFlags(blankitem.flags() & ~Qt.ItemIsEditable)
usertreeRoot.appendRow((roomitem, blankitem))
roomitem.setFont(font)
roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable)
usertreeRoot.appendRow(roomitem)
isControlledRoom = RoomPasswordProvider.isControlledRoom(room)
if isControlledRoom:
if room == currentUser.room and currentUser.isController():
roomitem.setIcon(QtGui.QIcon(self.resourcespath + 'lock_open.png'))
else:
roomitem.setIcon(QtGui.QIcon(self.resourcespath + 'lock.png'))
else:
roomitem.setIcon(QtGui.QIcon(self.resourcespath + 'chevrons_right.png'))
for user in rooms[room]:
useritem = QtGui.QStandardItem(user.username)
fileitem = QtGui.QStandardItem("")
isController = user.isController()
useritem.setData(isController, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE)
if user.file:
fileitem = QtGui.QStandardItem(u"{} ({})".format(user.file['name'], formatTime(user.file['duration'])))
filesizeitem = QtGui.QStandardItem(formatSize(user.file['size']))
filedurationitem = QtGui.QStandardItem("({})".format(formatTime(user.file['duration'])))
filenameitem = QtGui.QStandardItem((user.file['name']))
if currentUser.file:
sameName = sameFilename(user.file['name'], currentUser.file['name'])
sameSize = sameFilesize(user.file['size'], currentUser.file['size'])
sameDuration = sameFileduration(user.file['duration'], currentUser.file['duration'])
sameRoom = room == currentUser.room
differentName = not sameName
differentSize = not sameSize
differentDuration = not sameDuration
if sameName or sameRoom:
if differentSize and sameDuration:
fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentsize-note")))
elif differentSize and differentDuration:
fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentsizeandduration-note")))
elif differentDuration:
fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentduration-note")))
if sameRoom and (differentName or differentSize or differentDuration):
fileitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
underlinefont = QtGui.QFont()
underlinefont.setUnderline(True)
if sameRoom:
if not sameName:
filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
filenameitem.setFont(underlinefont)
if not sameSize:
if currentUser.file is not None and formatSize(user.file['size']) == formatSize(currentUser.file['size']):
filesizeitem = QtGui.QStandardItem(formatSize(user.file['size'],precise=True))
filesizeitem.setFont(underlinefont)
filesizeitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
if not sameDuration:
filedurationitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
filedurationitem.setFont(underlinefont)
else:
fileitem = QtGui.QStandardItem(getMessage("nofile-note"))
filenameitem = QtGui.QStandardItem(getMessage("nofile-note"))
filedurationitem = QtGui.QStandardItem("")
filesizeitem = QtGui.QStandardItem("")
if room == currentUser.room:
fileitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOFILEITEM_COLOR)))
filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOFILEITEM_COLOR)))
font = QtGui.QFont()
if currentUser.username == user.username:
font = QtGui.QFont()
font.setWeight(QtGui.QFont.Bold)
useritem.setFont(font)
useritem.setFlags(useritem.flags() & ~Qt.ItemIsEditable)
fileitem.setFlags(fileitem.flags() & ~Qt.ItemIsEditable)
roomitem.appendRow((useritem, fileitem))
if isControlledRoom and not isController:
useritem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOTCONTROLLER_COLOR)))
useritem.setFont(font)
useritem.setFlags(useritem.flags() & ~Qt.ItemIsEditable)
filenameitem.setFlags(filenameitem.flags() & ~Qt.ItemIsEditable)
filesizeitem.setFlags(filesizeitem.flags() & ~Qt.ItemIsEditable)
filedurationitem.setFlags(filedurationitem.flags() & ~Qt.ItemIsEditable)
roomitem.appendRow((useritem, filesizeitem, filedurationitem, filenameitem))
self.listTreeModel = self._usertreebuffer
self.listTreeView.setModel(self.listTreeModel)
self.listTreeView.setItemDelegate(UserlistItemDelegate())
self.listTreeView.setItemsExpandable(False)
self.listTreeView.setRootIsDecorated(False)
self.listTreeView.expandAll()
self.listTreeView.resizeColumnToContents(0)
self.listTreeView.resizeColumnToContents(1)
self.updateListGeometry()
def updateListGeometry(self):
try:
roomtocheck = 0
while self.listTreeModel.item(roomtocheck):
self.listTreeView.setFirstColumnSpanned(roomtocheck, self.listTreeView.rootIndex(), True)
roomtocheck += 1
self.listTreeView.header().setStretchLastSection(False)
self.listTreeView.header().setResizeMode(0, QtGui.QHeaderView.ResizeToContents)
self.listTreeView.header().setResizeMode(1, QtGui.QHeaderView.ResizeToContents)
self.listTreeView.header().setResizeMode(2, QtGui.QHeaderView.ResizeToContents)
self.listTreeView.header().setResizeMode(3, QtGui.QHeaderView.ResizeToContents)
NarrowTabsWidth = self.listTreeView.header().sectionSize(0)+self.listTreeView.header().sectionSize(1)+self.listTreeView.header().sectionSize(2)
if self.listTreeView.header().width() < (NarrowTabsWidth+self.listTreeView.header().sectionSize(3)):
self.listTreeView.header().resizeSection(3,self.listTreeView.header().width()-NarrowTabsWidth)
else:
self.listTreeView.header().setResizeMode(3, QtGui.QHeaderView.Stretch)
self.listTreeView.expandAll()
except:
pass
def roomClicked(self, item):
while item.parent().row() != -1:
item = item.parent()
self.joinRoom(item.sibling(item.row(), 0).data())
def userListChange(self):
self._syncplayClient.showUserList()
def updateRoomName(self, room=""):
self.roomInput.setText(room)
def showDebugMessage(self, message):
print(message)
def showErrorMessage(self, message, criticalerror = False):
def showErrorMessage(self, message, criticalerror=False):
message = unicode(message)
if criticalerror:
QtGui.QMessageBox.critical(self,"Syncplay", message)
QtGui.QMessageBox.critical(self, "Syncplay", message)
message = message.replace("&", "&amp;").replace('"', "&quot;").replace("<", "&lt;").replace(">", "&gt;")
message = message.replace("\n", "<br />")
message = "<span style=\"{}\">".format(constants.STYLE_ERRORNOTIFICATION) + message + "</span>"
self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "<br />")
def joinRoom(self, room = None):
def joinRoom(self, room=None):
if room == None:
room = self.roomInput.text()
if room == "":
if self._syncplayClient.userlist.currentUser.file:
if self._syncplayClient.userlist.currentUser.file:
room = self._syncplayClient.userlist.currentUser.file["name"]
else:
room = self._syncplayClient.defaultRoom
@ -151,46 +238,113 @@ class MainWindow(QtGui.QMainWindow):
if t is None:
return
if sign:
t = self._syncplayClient.getGlobalPosition() + sign * t
t = self._syncplayClient.getGlobalPosition() + sign * t
self._syncplayClient.setPosition(t)
else:
self.showErrorMessage("Invalid seek value")
def undoSeek(self):
tmp_pos = self._syncplayClient.getPlayerPosition()
self._syncplayClient.setPosition(self._syncplayClient.playerPositionBeforeLastSeek)
self._syncplayClient.playerPositionBeforeLastSeek = tmp_pos
def togglePause(self):
self._syncplayClient.setPaused(not self._syncplayClient.getPlayerPaused())
def play(self):
self._syncplayClient.setPaused(False)
def pause(self):
self._syncplayClient.setPaused(True)
def exitSyncplay(self):
self._syncplayClient.stop()
def closeEvent(self, event):
self.exitSyncplay()
self.saveSettings()
def setupSizes(self):
self.hideRoomSeekLabels()
self.miscThreshold = self.seekGroup.sizeHint().width()+self.roomGroup.sizeHint().width()+self.miscGroup.sizeHint().width()+30
self.hideMiscLabels()
self.setMinimumWidth(self.seekGroup.sizeHint().width()+self.roomGroup.sizeHint().width()+self.miscGroup.sizeHint().width()+30)
self.seekGroup.setMinimumWidth(self.seekGroup.sizeHint().width())
self.roomGroup.setMinimumWidth(self.roomGroup.sizeHint().width())
self.miscGroup.setMinimumWidth(self.miscGroup.sizeHint().width())
self.showRoomSeekLabels()
self.showMiscLabels()
windowMaximumWidth = self.maximumWidth()
self.seekGroup.setMaximumWidth(self.seekGroup.sizeHint().width())
self.roomGroup.setMaximumWidth(self.roomGroup.sizeHint().width())
self.miscGroup.setMaximumWidth(self.miscGroup.sizeHint().width())
self.setMaximumWidth(windowMaximumWidth)
self.roomSeekThreshold = self.mainLayout.sizeHint().width()
def hideRoomSeekLabels(self):
self.roomButton.setText("")
self.seekButton.setText("")
def hideMiscLabels(self):
self.unseekButton.setText("")
if constants.MERGE_PLAYPAUSE_BUTTONS:
self.playpauseButton.setText("")
else:
self.playButton.setText("")
self.pauseButton.setText("")
def showRoomSeekLabels(self):
if not constants.SHOW_BUTTON_LABELS:
return
self.roomButton.setText(getMessage("joinroom-guibuttonlabel"))
self.seekButton.setText(getMessage("seektime-guibuttonlabel"))
def showMiscLabels(self):
self.unseekButton.setText(getMessage("undoseek-guibuttonlabel"))
if not constants.SHOW_BUTTON_LABELS:
return
if constants.MERGE_PLAYPAUSE_BUTTONS:
self.playpauseButton.setText(getMessage("togglepause-guibuttonlabel"))
else:
self.playButton.setText(getMessage("play-guibuttonlabel"))
self.pauseButton.setText(getMessage("pause-guibuttonlabel"))
def resizeEvent(self,resizeEvent):
self.updateListGeometry()
if self.roomGroup and self.miscThreshold:
currentWidth = self.mainFrame.size().width()
if currentWidth < self.miscThreshold:
if self.unseekButton.text() != "":
self.hideMiscLabels()
else:
if self.unseekButton.text() == "":
self.showMiscLabels()
if currentWidth < self.roomSeekThreshold:
if self.roomButton.text() != "":
self.hideRoomSeekLabels()
else:
if self.roomButton.text() == "":
self.showRoomSeekLabels()
def loadMediaBrowseSettings(self):
settings = QSettings("Syncplay", "MediaBrowseDialog")
settings.beginGroup("MediaBrowseDialog")
self.mediadirectory = settings.value("mediadir", "")
settings.endGroup()
def saveMediaBrowseSettings(self):
settings = QSettings("Syncplay", "MediaBrowseDialog")
settings.beginGroup("MediaBrowseDialog")
settings.setValue("mediadir", self.mediadirectory)
settings.endGroup()
def browseMediapath(self):
if self._syncplayClient._player.customOpenDialog == True:
self._syncplayClient._player.openCustomOpenDialog()
return
self.loadMediaBrowseSettings()
options = QtGui.QFileDialog.Options()
if os.path.isdir(self.mediadirectory):
@ -201,16 +355,30 @@ class MainWindow(QtGui.QMainWindow):
defaultdirectory = QtGui.QDesktopServices.storageLocation(QtGui.QDesktopServices.HomeLocation)
else:
defaultdirectory = ""
browserfilter = "All files (*)"
fileName, filtr = QtGui.QFileDialog.getOpenFileName(self,getMessage("browseformedia-label"),defaultdirectory,
browserfilter, "", options)
browserfilter = "All files (*)"
fileName, filtr = QtGui.QFileDialog.getOpenFileName(self, getMessage("browseformedia-label"), defaultdirectory,
browserfilter, "", options)
if fileName:
if sys.platform.startswith('win'):
fileName = fileName.replace("/","\\")
fileName = fileName.replace("/", "\\")
self.mediadirectory = os.path.dirname(fileName)
self.saveMediaBrowseSettings()
self._syncplayClient._player.openFile(fileName)
def createControlledRoom(self):
controlroom, ok = QtGui.QInputDialog.getText(self, getMessage("createcontrolledroom-msgbox-label"),
getMessage("controlledroominfo-msgbox-label"), QtGui.QLineEdit.Normal,
utils.stripRoomName(self._syncplayClient.getRoom()))
if ok and controlroom != '':
self._syncplayClient.createControlledRoom(controlroom)
def identifyAsController(self):
msgboxtitle = getMessage("identifyascontroller-msgbox-label")
msgboxtext = getMessage("identifyinfo-msgbox-label")
controlpassword, ok = QtGui.QInputDialog.getText(self, msgboxtitle, msgboxtext, QtGui.QLineEdit.Normal, "")
if ok and controlpassword != '':
self._syncplayClient.identifyAsController(controlpassword)
def _extractSign(self, m):
if m:
if m == "-":
@ -219,11 +387,11 @@ class MainWindow(QtGui.QMainWindow):
return 1
else:
return None
def setOffset(self):
newoffset, ok = QtGui.QInputDialog.getText(self,getMessage("setoffset-msgbox-label"),
getMessage("offsetinfo-msgbox-label"), QtGui.QLineEdit.Normal,
"")
newoffset, ok = QtGui.QInputDialog.getText(self, getMessage("setoffset-msgbox-label"),
getMessage("offsetinfo-msgbox-label"), QtGui.QLineEdit.Normal,
"")
if ok and newoffset != '':
o = re.match(constants.UI_OFFSET_REGEX, "o " + newoffset)
if o:
@ -232,13 +400,13 @@ class MainWindow(QtGui.QMainWindow):
if t is None:
return
if o.group('sign') == "/":
t = self._syncplayClient.getPlayerPosition() - t
t = self._syncplayClient.getPlayerPosition() - t
elif sign:
t = self._syncplayClient.getUserOffset() + sign * t
t = self._syncplayClient.getUserOffset() + sign * t
self._syncplayClient.setUserOffset(t)
else:
self.showErrorMessage("Invalid offset value")
def openUserGuide(self):
if sys.platform.startswith('linux'):
self.QtGui.QDesktopServices.openUrl("http://syncplay.pl/guide/linux/")
@ -249,9 +417,9 @@ class MainWindow(QtGui.QMainWindow):
def drop(self):
self.close()
def addTopLayout(self, window):
window.topSplit = QtGui.QSplitter(Qt.Horizontal)
def addTopLayout(self, window):
window.topSplit = self.topSplitter(Qt.Horizontal, self)
window.outputLayout = QtGui.QVBoxLayout()
window.outputbox = QtGui.QTextEdit()
@ -260,21 +428,22 @@ class MainWindow(QtGui.QMainWindow):
window.outputFrame = QtGui.QFrame()
window.outputFrame.setLineWidth(0)
window.outputFrame.setMidLineWidth(0)
window.outputLayout.setContentsMargins(0,0,0,0)
window.outputLayout.setContentsMargins(0, 0, 0, 0)
window.outputLayout.addWidget(window.outputlabel)
window.outputLayout.addWidget(window.outputbox)
window.outputFrame.setLayout(window.outputLayout)
window.listLayout = QtGui.QVBoxLayout()
window.listTreeModel = QtGui.QStandardItemModel()
window.listTreeView = QtGui.QTreeView()
window.listTreeView.setModel(window.listTreeModel)
window.listTreeView.setIndentation(21)
window.listTreeView.doubleClicked.connect(self.roomClicked)
window.listlabel = QtGui.QLabel(getMessage("userlist-heading-label"))
window.listFrame = QtGui.QFrame()
window.listFrame.setLineWidth(0)
window.listFrame.setMidLineWidth(0)
window.listLayout.setContentsMargins(0,0,0,0)
window.listLayout.setContentsMargins(0, 0, 0, 0)
window.listLayout.addWidget(window.listlabel)
window.listLayout.addWidget(window.listTreeView)
window.contactLabel = QtGui.QLabel()
@ -288,13 +457,13 @@ class MainWindow(QtGui.QMainWindow):
window.contactLabel.setOpenExternalLinks(True)
window.listLayout.addWidget(window.contactLabel)
window.listFrame.setLayout(window.listLayout)
window.topSplit.addWidget(window.outputFrame)
window.topSplit.addWidget(window.listFrame)
window.topSplit.setStretchFactor(0,4)
window.topSplit.setStretchFactor(1,5)
window.mainLayout.addWidget(window.topSplit)
window.topSplit.setSizePolicy(QtGui.QSizePolicy.Preferred,QtGui.QSizePolicy.Expanding)
window.topSplit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
def addBottomLayout(self, window):
window.bottomLayout = QtGui.QHBoxLayout()
@ -303,156 +472,169 @@ class MainWindow(QtGui.QMainWindow):
window.addSeekBox(MainWindow)
window.addMiscBox(MainWindow)
window.bottomLayout.addWidget(window.roomGroup, Qt.AlignLeft)
window.bottomLayout.addWidget(window.seekGroup, Qt.AlignLeft)
window.bottomLayout.addWidget(window.miscGroup, Qt.AlignLeft)
window.bottomLayout.addWidget(window.roomGroup)
window.bottomLayout.addWidget(window.seekGroup)
window.bottomLayout.addWidget(window.miscGroup)
window.mainLayout.addLayout(window.bottomLayout, Qt.AlignLeft)
def addRoomBox(self, window):
window.roomGroup = QtGui.QGroupBox(getMessage("room-heading-label"))
window.roomInput = QtGui.QLineEdit()
window.roomInput.returnPressed.connect(self.joinRoom)
window.roomButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'door_in.png'), getMessage("joinroom-guibuttonlabel"))
window.roomButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'door_in.png'),
getMessage("joinroom-guibuttonlabel"))
window.roomButton.pressed.connect(self.joinRoom)
window.roomLayout = QtGui.QHBoxLayout()
window.roomInput.setFixedWidth(150)
self.roomButton.setToolTip(getMessage("joinroom-tooltip"))
window.roomLayout.addWidget(window.roomInput)
window.roomLayout.addWidget(window.roomButton)
window.roomGroup.setLayout(window.roomLayout)
window.roomGroup.setFixedSize(window.roomGroup.sizeHint())
window.roomGroup.setFixedHeight(window.roomGroup.sizeHint().height())
def addSeekBox(self, window):
window.seekGroup = QtGui.QGroupBox(getMessage("seek-heading-label"))
window.seekInput = QtGui.QLineEdit()
window.seekInput.returnPressed.connect(self.seekPosition)
window.seekButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'clock_go.png'),getMessage("seektime-guibuttonlabel"))
window.seekButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'clock_go.png'),
getMessage("seektime-guibuttonlabel"))
window.seekButton.pressed.connect(self.seekPosition)
self.seekButton.setToolTip(getMessage("seektime-tooltip"))
window.seekLayout = QtGui.QHBoxLayout()
window.seekInput.setText("0:00")
window.seekInput.setFixedWidth(60)
window.seekLayout.addWidget(window.seekInput)
window.seekLayout.addWidget(window.seekButton)
window.seekGroup.setLayout(window.seekLayout)
window.seekGroup.setFixedSize(window.seekGroup.sizeHint())
window.seekGroup.setFixedHeight(window.seekGroup.sizeHint().height())
def addMiscBox(self, window):
window.miscGroup = QtGui.QGroupBox(getMessage("othercommands-heading-label"))
window.unseekButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'arrow_undo.png'),getMessage("undoseek-guibuttonlabel"))
window.unseekButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'arrow_undo.png'),
getMessage("undoseek-guibuttonlabel"))
window.unseekButton.pressed.connect(self.undoSeek)
self.unseekButton.setToolTip(getMessage("undoseek-tooltip"))
window.miscLayout = QtGui.QHBoxLayout()
window.miscLayout.addWidget(window.unseekButton)
if constants.MERGE_PLAYPAUSE_BUTTONS == True:
window.playpauseButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'control_pause_blue.png'),getMessage("togglepause-guibuttonlabel"))
window.playpauseButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'control_pause_blue.png'),
getMessage("togglepause-guibuttonlabel"))
window.playpauseButton.pressed.connect(self.togglePause)
window.miscLayout.addWidget(window.playpauseButton)
self.playpauseButton.setToolTip(getMessage("togglepause-tooltip"))
else:
window.playButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'control_play_blue.png'),getMessage("play-guibuttonlabel"))
window.playButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'control_play_blue.png'),
getMessage("play-guibuttonlabel"))
window.playButton.pressed.connect(self.play)
window.playButton.setMaximumWidth(60)
window.miscLayout.addWidget(window.playButton)
window.pauseButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'control_pause_blue.png'),getMessage("pause-guibuttonlabel"))
window.pauseButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'control_pause_blue.png'),
getMessage("pause-guibuttonlabel"))
window.pauseButton.pressed.connect(self.pause)
window.pauseButton.setMaximumWidth(60)
window.miscLayout.addWidget(window.pauseButton)
self.playButton.setToolTip(getMessage("play-tooltip"))
self.pauseButton.setToolTip(getMessage("pause-tooltip"))
window.miscGroup.setLayout(window.miscLayout)
window.miscGroup.setFixedSize(window.miscGroup.sizeHint())
window.miscGroup.setFixedHeight(window.miscGroup.sizeHint().height())
def addMenubar(self, window):
window.menuBar = QtGui.QMenuBar()
window.fileMenu = QtGui.QMenu(getMessage("file-menu-label"), self)
window.openAction = window.fileMenu.addAction(QtGui.QIcon(self.resourcespath + 'folder_explore.png'), getMessage("openmedia-menu-label"))
window.openAction = window.fileMenu.addAction(QtGui.QIcon(self.resourcespath + 'folder_explore.png'),
getMessage("openmedia-menu-label"))
window.openAction.triggered.connect(self.browseMediapath)
window.exitAction = window.fileMenu.addAction(QtGui.QIcon(self.resourcespath + 'cross.png'), getMessage("file-menu-label"))
window.exitAction = window.fileMenu.addAction(QtGui.QIcon(self.resourcespath + 'cross.png'),
getMessage("exit-menu-label"))
window.exitAction.triggered.connect(self.exitSyncplay)
window.menuBar.addMenu(window.fileMenu)
window.advancedMenu = QtGui.QMenu(getMessage("advanced-menu-label"), self)
window.setoffsetAction = window.advancedMenu.addAction(QtGui.QIcon(self.resourcespath + 'timeline_marker.png'),getMessage("setoffset-menu-label"))
window.setoffsetAction = window.advancedMenu.addAction(QtGui.QIcon(self.resourcespath + 'timeline_marker.png'),
getMessage("setoffset-menu-label"))
window.setoffsetAction.triggered.connect(self.setOffset)
window.createcontrolledroomAction = window.advancedMenu.addAction(
QtGui.QIcon(self.resourcespath + 'page_white_key.png'), getMessage("createcontrolledroom-menu-label"))
window.createcontrolledroomAction.triggered.connect(self.createControlledRoom)
window.identifyascontroller = window.advancedMenu.addAction(QtGui.QIcon(self.resourcespath + 'key_go.png'),
getMessage("identifyascontroller-menu-label"))
window.identifyascontroller.triggered.connect(self.identifyAsController)
window.menuBar.addMenu(window.advancedMenu)
window.helpMenu = QtGui.QMenu(getMessage("help-menu-label"), self)
window.userguideAction = window.helpMenu.addAction(QtGui.QIcon(self.resourcespath + 'help.png'), getMessage("userguide-menu-label"))
window.userguideAction = window.helpMenu.addAction(QtGui.QIcon(self.resourcespath + 'help.png'),
getMessage("userguide-menu-label"))
window.userguideAction.triggered.connect(self.openUserGuide)
window.menuBar.addMenu(window.helpMenu)
window.mainLayout.setMenuBar(window.menuBar)
def addMainFrame(self, window):
window.mainFrame = QtGui.QFrame()
window.mainFrame.setLineWidth(0)
window.mainFrame.setMidLineWidth(0)
window.mainFrame.setContentsMargins(0,0,0,0)
window.mainFrame.setContentsMargins(0, 0, 0, 0)
window.mainFrame.setLayout(window.mainLayout)
window.setCentralWidget(window.mainFrame)
def newMessage(self, message):
self.outputbox.moveCursor(QtGui.QTextCursor.End)
self.outputbox.insertHtml(message)
self.outputbox.moveCursor(QtGui.QTextCursor.End)
def resetList(self):
self.listbox.setText("")
def newListItem(self, item):
self.listbox.moveCursor(QtGui.QTextCursor.End)
self.listbox.insertHtml(item)
self.listbox.moveCursor(QtGui.QTextCursor.End)
def dragEnterEvent(self, event):
data = event.mimeData()
urls = data.urls()
if urls and urls[0].scheme() == 'file':
event.acceptProposedAction()
def dropEvent(self, event):
rewindFile = False
if QtGui.QDropEvent.proposedAction(event) == Qt.MoveAction:
QtGui.QDropEvent.setDropAction(event, Qt.CopyAction) # Avoids file being deleted
QtGui.QDropEvent.setDropAction(event, Qt.CopyAction) # Avoids file being deleted
rewindFile = True
data = event.mimeData()
urls = data.urls()
if urls and urls[0].scheme() == 'file':
if sys.platform.startswith('win'):
dropfilepath = unicode(urls[0].path().replace("/", "\\"))[1:] # Removes starting slash
else:
dropfilepath = unicode(urls[0].path())
dropfilepath = os.path.abspath(unicode(event.mimeData().urls()[0].toLocalFile()))
if rewindFile == False:
self._syncplayClient._player.openFile(dropfilepath)
else:
self._syncplayClient.setPosition(0)
self._syncplayClient._player.openFile(dropfilepath)
self._syncplayClient._player.openFile(dropfilepath, resetPosition=True)
self._syncplayClient.setPosition(0)
def saveSettings(self):
settings = QSettings("Syncplay", "MainWindow")
settings.beginGroup("MainWindow")
settings.setValue("size", self.size())
settings.setValue("pos", self.pos())
settings.endGroup()
def loadSettings(self):
settings = QSettings("Syncplay", "MainWindow")
settings.beginGroup("MainWindow")
@ -467,14 +649,16 @@ class MainWindow(QtGui.QMainWindow):
self.resourcespath = utils.findWorkingDir() + "\\resources\\"
else:
self.resourcespath = utils.findWorkingDir() + "/resources/"
self.setWindowFlags(self.windowFlags() & Qt.AA_DontUseNativeMenuBar)
self.setWindowTitle("Syncplay v" + version)
self.mainLayout = QtGui.QVBoxLayout()
self.addTopLayout(self)
self.addBottomLayout(self)
self.addMenubar(self)
self.addMainFrame(self)
self.setupSizes()
self.loadSettings()
self.setWindowIcon(QtGui.QIcon(self.resourcespath + "syncplay.png"))
self.setWindowFlags(self.windowFlags() & Qt.WindowCloseButtonHint & Qt.WindowMinimizeButtonHint & ~Qt.WindowContextHelpButtonHint)
self.setWindowFlags(self.windowFlags() & Qt.WindowCloseButtonHint & Qt.AA_DontUseNativeMenuBar & Qt.WindowMinimizeButtonHint & ~Qt.WindowContextHelpButtonHint)
self.show()
self.setAcceptDrops(True)
self.setAcceptDrops(True)

View File

@ -7,6 +7,8 @@ import sys
import os
import itertools
import hashlib
import random
import string
def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None):
"""Retry calling the decorated function using an exponential backoff.
@ -93,6 +95,19 @@ def formatTime(timeInSeconds, weeksAsTitles=True):
formattedTime = "{0:} (Title {1:.0f})".format(formattedTime, title)
return formattedTime
def formatSize (bytes, precise=False):
if bytes == 0: # E.g. when file size privacy is enabled
return "???"
try:
megabytes = int(bytes) / 1048576.0 # Technically this is a mebibyte, but whatever
if precise:
megabytes = round(megabytes, 1)
else:
megabytes = int(megabytes)
return str(megabytes) + getMessage("megabyte-suffix")
except: # E.g. when filesize is hashed
return "???"
def findWorkingDir():
frozen = getattr(sys, 'frozen', '')
if not frozen:
@ -141,7 +156,19 @@ def blackholeStdoutForFrozenWindow():
# Relate to file hashing / difference checking:
def stripfilename(filename):
return re.sub(constants.FILENAME_STRIP_REGEX, "", filename)
if filename:
return re.sub(constants.FILENAME_STRIP_REGEX, "", filename)
else:
return ""
def stripRoomName(RoomName):
if RoomName:
try:
return re.sub(constants.ROOM_NAME_STRIP_REGEX, "\g<roomnamebase>", RoomName)
except IndexError:
return RoomName
else:
return ""
def hashFilename(filename):
return hashlib.sha256(stripfilename(filename).encode('utf-8')).hexdigest()[:12]
@ -182,3 +209,69 @@ def sameFileduration (duration1, duration2):
return True
else:
return False
def meetsMinVersion(version, minVersion):
def versiontotuple(ver):
return tuple(map(int, ver.split(".")))
return versiontotuple(version) >= versiontotuple(minVersion)
class RoomPasswordProvider(object):
CONTROLLED_ROOM_REGEX = re.compile("^\+(.*):(\w{12})$")
PASSWORD_REGEX = re.compile("[A-Z]{2}-\d{3}-\d{3}")
@staticmethod
def isControlledRoom(roomName):
return bool(re.match(RoomPasswordProvider.CONTROLLED_ROOM_REGEX, roomName))
@staticmethod
def check(roomName, password, salt):
if not password or not re.match(RoomPasswordProvider.PASSWORD_REGEX, password):
raise ValueError()
if not roomName:
raise NotControlledRoom()
match = re.match(RoomPasswordProvider.CONTROLLED_ROOM_REGEX, roomName)
if not match:
raise NotControlledRoom()
roomHash = match.group(2)
computedHash = RoomPasswordProvider._computeRoomHash(match.group(1), password, salt)
return roomHash == computedHash
@staticmethod
def getControlledRoomName(roomName, password, salt):
return "+" + roomName + ":" + RoomPasswordProvider._computeRoomHash(roomName, password, salt)
@staticmethod
def _computeRoomHash(roomName, password, salt):
roomName = roomName.encode('utf8')
salt = hashlib.sha256(salt).hexdigest()
provisionalHash = hashlib.sha256(roomName + salt).hexdigest()
return hashlib.sha1(provisionalHash + salt + password).hexdigest()[:12].upper()
class RandomStringGenerator(object):
@staticmethod
def generate_room_password():
parts = (
RandomStringGenerator._get_random_letters(2),
RandomStringGenerator._get_random_numbers(3),
RandomStringGenerator._get_random_numbers(3)
)
return "{}-{}-{}".format(*parts)
@staticmethod
def generate_server_salt():
parts = (
RandomStringGenerator._get_random_letters(10),
)
return "{}".format(*parts)
@staticmethod
def _get_random_letters(quantity):
return ''.join(random.choice(string.ascii_uppercase) for _ in xrange(quantity))
@staticmethod
def _get_random_numbers(quantity):
return ''.join(random.choice(string.digits) for _ in xrange(quantity))
class NotControlledRoom(Exception):
pass

View File

@ -20,5 +20,5 @@ if __name__ == '__main__':
argsGetter = ConfigurationGetter()
args = argsGetter.getConfiguration()
reactor.listenTCP(int(args.port), SyncFactory(args.password, args.motd_file, args.isolate_rooms))
reactor.listenTCP(int(args.port), SyncFactory(args.password, args.motd_file, args.isolate_rooms, args.salt))
reactor.run()