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 VLC_SUPPORT = true
endif 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) ifeq ($(SINGLE_USER),false)
ifneq ($(BSD),true)
BASE_PATH = /usr BASE_PATH = /usr
VLC_LIB_PATH = ${PREFIX}/usr/lib VLC_LIB_PATH = ${PREFIX}/usr/lib
else
BASE_PATH = /usr/local
VLC_LIB_PATH = ${PREFIX}/usr/local/lib
endif
else else
BASE_PATH = ${HOME}/.local BASE_PATH = ${HOME}/.local
VLC_LIB_PATH = ${HOME}/.local/share VLC_LIB_PATH = ${HOME}/.local/share
@ -18,8 +36,6 @@ endif
SHARE_PATH = ${PREFIX}$(BASE_PATH)/share SHARE_PATH = ${PREFIX}$(BASE_PATH)/share
common: common:
-mkdir -p $(LIB_PATH)/syncplay/resources/
-mkdir -p $(LIB_PATH)/syncplay/resources/lua
-mkdir -p $(LIB_PATH)/syncplay/resources/lua/intf -mkdir -p $(LIB_PATH)/syncplay/resources/lua/intf
-mkdir -p $(APP_SHORTCUT_PATH) -mkdir -p $(APP_SHORTCUT_PATH)
-mkdir -p $(SHARE_PATH)/app-install/icons -mkdir -p $(SHARE_PATH)/app-install/icons

View File

@ -16,7 +16,13 @@ import syncplay
import os import os
import subprocess 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" NSIS_COMPILE = p if os.path.isfile(p) else "makensis.exe"
OUT_DIR = "syncplay v{}".format(syncplay.version) OUT_DIR = "syncplay v{}".format(syncplay.version)
SETUP_SCRIPT_PATH = "syncplay_setup.nsi" SETUP_SCRIPT_PATH = "syncplay_setup.nsi"
@ -27,6 +33,8 @@ NSIS_SCRIPT_TEMPLATE = r"""
LoadLanguageFile "$${NSISDIR}\Contrib\Language files\English.nlf" LoadLanguageFile "$${NSISDIR}\Contrib\Language files\English.nlf"
LoadLanguageFile "$${NSISDIR}\Contrib\Language files\Polish.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" Name "Syncplay $version"
OutFile "Syncplay $version Setup.exe" OutFile "Syncplay $version Setup.exe"
@ -47,6 +55,11 @@ NSIS_SCRIPT_TEMPLATE = r"""
VIAddVersionKey /LANG=$${LANG_POLISH} "LegalCopyright" "Syncplay" VIAddVersionKey /LANG=$${LANG_POLISH} "LegalCopyright" "Syncplay"
VIAddVersionKey /LANG=$${LANG_POLISH} "FileDescription" "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 ^SyncplayLanguage $${LANG_ENGLISH} "en"
LangString ^Associate $${LANG_ENGLISH} "Associate Syncplay with multimedia files." LangString ^Associate $${LANG_ENGLISH} "Associate Syncplay with multimedia files."
LangString ^VLC $${LANG_ENGLISH} "Install Syncplay interface for VLC 2 and above" LangString ^VLC $${LANG_ENGLISH} "Install Syncplay interface for VLC 2 and above"
@ -67,6 +80,29 @@ NSIS_SCRIPT_TEMPLATE = r"""
LangString ^QuickLaunchBar $${LANG_POLISH} "Pasek szybkiego uruchamiania" LangString ^QuickLaunchBar $${LANG_POLISH} "Pasek szybkiego uruchamiania"
LangString ^UninstConfig $${LANG_POLISH} "Usun plik konfiguracyjny." 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 PageEx license
LicenseData resources\license.txt LicenseData resources\license.txt
PageExEnd PageExEnd
@ -164,6 +200,10 @@ NSIS_SCRIPT_TEMPLATE = r"""
Push English Push English
Push $${LANG_POLISH} Push $${LANG_POLISH}
Push Polski Push Polski
Push $${LANG_RUSSIAN}
Push Русский
Push $${LANG_GERMAN}
Push Deutsch
Push A ; A means auto count languages Push A ; A means auto count languages
LangDLL::LangDialog "Language Selection" "Please select the language of Syncplay and the installer" LangDLL::LangDialog "Language Selection" "Please select the language of Syncplay and the installer"
Pop $$LANGUAGE Pop $$LANGUAGE
@ -216,13 +256,13 @@ NSIS_SCRIPT_TEMPLATE = r"""
$${NSD_CreateLabel} 8u 85u 187u 10u "$$(^Shortcut)" $${NSD_CreateLabel} 8u 85u 187u 10u "$$(^Shortcut)"
Pop $$Label_Shortcut Pop $$Label_Shortcut
$${NSD_CreateCheckbox} 8u 98u 50u 10u "$$(^StartMenu)" $${NSD_CreateCheckbox} 8u 98u 60u 10u "$$(^StartMenu)"
Pop $$CheckBox_StartMenuShortcut Pop $$CheckBox_StartMenuShortcut
$${NSD_CreateCheckbox} 68u 98u 50u 10u "$$(^Desktop)" $${NSD_CreateCheckbox} 78u 98u 70u 10u "$$(^Desktop)"
Pop $$CheckBox_DesktopShortcut Pop $$CheckBox_DesktopShortcut
$${NSD_CreateCheckbox} 128u 98u 150u 10u "$$(^QuickLaunchBar)" $${NSD_CreateCheckbox} 158u 98u 130u 10u "$$(^QuickLaunchBar)"
Pop $$CheckBox_QuickLaunchShortcut Pop $$CheckBox_QuickLaunchShortcut
$${If} $$CheckBox_Associate_State == $${BST_CHECKED} $${If} $$CheckBox_Associate_State == $${BST_CHECKED}
@ -582,7 +622,9 @@ guiIcons = ['resources/accept.png', 'resources/arrow_undo.png', 'resources/clock
'resources/timeline_marker.png','resources/control_play_blue.png', 'resources/timeline_marker.png','resources/control_play_blue.png',
'resources/mpc-hc.png','resources/mpc-hc64.png','resources/mplayer.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/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 = ["resources/icon.ico", "resources/syncplay.png"]
resources.extend(guiIcons) 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 Principal author: Etoh
Other contributors: DerGenaue, jb Other contributors: DerGenaue, jb
Project: http://syncplay.pl/ Project: http://syncplay.pl/
Version: 0.2.2 Version: 0.2.3
Note: Note:
* This interface module is intended to be used in conjunction with Syncplay. * 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"] local connectorversion = "0.2.3"
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 durationdelay = 500000 -- Pause for get_duration command etc for increased reliability (uses microseconds) 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 loopsleepduration = 5000 -- Pause for every event loop (uses microseconds)
local quitcheckfrequency = 20 -- Check whether VLC has closed every X loops local quitcheckfrequency = 20 -- Check whether VLC has closed every X loops
@ -119,9 +110,32 @@ local newtitle = 0
local running = true 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. -- Start hosting Syncplay interface.
port = tonumber(config["port"]) port = radixsafe_tonumber(config["port"])
if (port == nil or port < 1) then port = 4123 end if (port == nil or port < 1) then port = 4123 end
function quit_vlc() function quit_vlc()
@ -256,12 +270,12 @@ function set_time ( timetoset)
if input then if input then
local response, errormsg, realtime, titletrack local response, errormsg, realtime, titletrack
realtime = timetoset % titlemultiplier realtime = timetoset % titlemultiplier
oldtitle = tonumber(get_var("title", 0)) oldtitle = radixsafe_tonumber(get_var("title", 0))
newtitle = (timetoset - realtime) / titlemultiplier newtitle = (timetoset - realtime) / titlemultiplier
if oldtitle ~= newtitle and newtitle > -1 then if oldtitle ~= newtitle and newtitle > -1 then
set_var("title", tonumber(newtitle)) set_var("title", radixsafe_tonumber(newtitle))
end end
errormsg = set_var("time", tonumber(realtime)) errormsg = set_var("time", radixsafe_tonumber(realtime))
return errormsg return errormsg
else else
return noinput return noinput
@ -393,7 +407,7 @@ function display_osd ( argument )
if input then if input then
osdarray = get_args(argument,3) osdarray = get_args(argument,3)
--position, duration, message -> message, , position, duration (converted from seconds to microseconds) --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) vlc.osd.message(osdarray[3],channel1,osdarray[1],osdduration)
else else
errormsg = noinput 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-filepath" then response = "filepath"..msgseperator..errormerge(get_filepath())..msgterminator
elseif command == "get-filename" then response = "filename"..msgseperator..errormerge(get_filename())..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 == "get-title" then response = "title"..msgseperator..errormerge(get_var("title", 0))..msgterminator
elseif command == "set-position" then errormsg = set_time(tonumber(argument)) elseif command == "set-position" then errormsg = set_time(radixsafe_tonumber(argument))
elseif command == "seek-within-title" then errormsg = set_var("time", 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-playstate" then errormsg = set_playstate(argument)
elseif command == "set-rate" then errormsg = set_var("rate", tonumber(argument)) elseif command == "set-rate" then errormsg = set_var("rate", radixsafe_tonumber(argument))
elseif command == "set-title" then errormsg = set_var("title", 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 == "display-osd" then errormsg = display_osd(argument)
elseif command == "load-file" then response = load_file(argument) elseif command == "load-file" then response = load_file(argument)
elseif command == "close-vlc" then quit_vlc() elseif command == "close-vlc" then quit_vlc()
@ -471,7 +485,7 @@ function set_playstate(argument)
end end
if string.sub(vlc.misc.version(),1,2) == "1." then 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() quit_vlc()
else else
l = vlc.net.listen_tcp(host, port) 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' version = '1.3.0'
milestone = 'Pineapple, Pulverize and Destroy!' milestone = 'Akki'
projectURL = 'http://syncplay.pl/' projectURL = 'http://syncplay.pl/'

View File

@ -4,12 +4,12 @@ import time
import re import re
from twisted.internet.protocol import ClientFactory from twisted.internet.protocol import ClientFactory
from twisted.internet import reactor, task from twisted.internet import reactor, task
from functools import wraps
from syncplay.protocols import SyncClientProtocol from syncplay.protocols import SyncClientProtocol
from syncplay import utils, constants from syncplay import utils, constants
from syncplay.messages import getMessage from syncplay.messages import getMissingStrings, getMessage
import threading
from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \ from syncplay.constants import PRIVACY_SENDHASHED_MODE, PRIVACY_DONTSEND_MODE, \
PRIVACY_HIDDENFILENAME, FILENAME_STRIP_REGEX PRIVACY_HIDDENFILENAME
import collections import collections
class SyncClientFactory(ClientFactory): class SyncClientFactory(ClientFactory):
@ -61,14 +61,21 @@ class SyncplayClient(object):
constants.SHOW_DIFFERENT_ROOM_OSD = config['showDifferentRoomOSD'] constants.SHOW_DIFFERENT_ROOM_OSD = config['showDifferentRoomOSD']
constants.SHOW_SAME_ROOM_OSD = config['showSameRoomOSD'] constants.SHOW_SAME_ROOM_OSD = config['showSameRoomOSD']
constants.SHOW_DURATION_NOTIFICATION = config['showDurationNotification'] 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.lastLeftTime = 0
self.lastLeftUser = u"" self.lastLeftUser = u""
self.protocolFactory = SyncClientFactory(self) self.protocolFactory = SyncClientFactory(self)
self.ui = UiManager(self, ui) self.ui = UiManager(self, ui)
self.userlist = SyncplayUserlist(self.ui, self) self.userlist = SyncplayUserlist(self.ui, self)
self._protocol = None self._protocol = None
""":type : SyncClientProtocol|None"""
self._player = 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 config['room'] = config['name'] # ticket #58
self.defaultRoom = config['room'] self.defaultRoom = config['room']
self.playerPositionBeforeLastSeek = 0.0 self.playerPositionBeforeLastSeek = 0.0
@ -99,7 +106,13 @@ class SyncplayClient(object):
self._warnings = self._WarningManager(self._player, self.userlist, self.ui) 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']: 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): def initProtocol(self, protocol):
self._protocol = protocol self._protocol = protocol
@ -177,6 +190,13 @@ class SyncplayClient(object):
madeChangeOnPlayer = True madeChangeOnPlayer = True
return madeChangeOnPlayer 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): def _serverUnpaused(self, setBy):
hideFromOSD = not constants.SHOW_SAME_ROOM_OSD hideFromOSD = not constants.SHOW_SAME_ROOM_OSD
self._player.setPaused(False) self._player.setPaused(False)
@ -223,7 +243,7 @@ class SyncplayClient(object):
def _changePlayerStateAccordingToGlobalState(self, position, paused, doSeek, setBy): def _changePlayerStateAccordingToGlobalState(self, position, paused, doSeek, setBy):
madeChangeOnPlayer = False madeChangeOnPlayer = False
pauseChanged = paused != self.getGlobalPaused() pauseChanged = paused != self.getGlobalPaused() or paused != self.getPlayerPaused()
diff = self.getPlayerPosition() - position diff = self.getPlayerPosition() - position
if self._lastGlobalUpdate is None: if self._lastGlobalUpdate is None:
madeChangeOnPlayer = self._initPlayerState(position, paused) madeChangeOnPlayer = self._initPlayerState(position, paused)
@ -234,7 +254,9 @@ class SyncplayClient(object):
madeChangeOnPlayer = self._serverSeeked(position, setBy) madeChangeOnPlayer = self._serverSeeked(position, setBy)
if diff > self._config['rewindThreshold'] and not doSeek and not self._config['rewindOnDesync'] == False: if diff > self._config['rewindThreshold'] and not doSeek and not self._config['rewindOnDesync'] == False:
madeChangeOnPlayer = self._rewindPlayerDueToTimeDifference(position, setBy) 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) madeChangeOnPlayer = self._slowDownToCoverTimeDifference(diff, setBy)
if paused == False and pauseChanged: if paused == False and pauseChanged:
madeChangeOnPlayer = self._serverUnpaused(setBy) madeChangeOnPlayer = self._serverUnpaused(setBy)
@ -317,7 +339,6 @@ class SyncplayClient(object):
size = os.path.getsize(path) size = os.path.getsize(path)
except OSError: # file not accessible (stream?) except OSError: # file not accessible (stream?)
size = 0 size = 0
rawfilename = filename
filename, size = self.__executePrivacySettings(filename, size) filename, size = self.__executePrivacySettings(filename, size)
self.userlist.currentUser.setFile(filename, duration, size) self.userlist.currentUser.setFile(filename, duration, size)
self.sendFile() self.sendFile()
@ -333,6 +354,9 @@ class SyncplayClient(object):
size = 0 size = 0
return filename, size return filename, size
def setServerVersion(self, version):
self.serverVersion = version
def sendFile(self): def sendFile(self):
file_ = self.userlist.currentUser.file file_ = self.userlist.currentUser.file
if self._protocol and self._protocol.logged and 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: if self._protocol and self._protocol.logged and room:
self._protocol.sendRoomSetting(room) self._protocol.sendRoomSetting(room)
self.getUserList() 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): def getRoom(self):
return self.userlist.currentUser.room return self.userlist.currentUser.room
@ -414,6 +449,64 @@ class SyncplayClient(object):
if promptForAction: if promptForAction:
self.ui.promptFor(getMessage("enter-to-exit-prompt")) 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): class _WarningManager(object):
def __init__(self, player, userlist, ui): def __init__(self, player, userlist, ui):
self._player = player self._player = player
@ -466,6 +559,7 @@ class SyncplayUser(object):
self.username = username self.username = username
self.room = room self.room = room
self.file = file_ self.file = file_
self._controller = False
def setFile(self, filename, duration, size): def setFile(self, filename, duration, size):
file_ = { file_ = {
@ -484,7 +578,10 @@ class SyncplayUser(object):
return sameName and sameSize and sameDuration return sameName and sameSize and sameDuration
def __lt__(self, other): 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): def __repr__(self, *args, **kwargs):
if self.file: if self.file:
@ -492,6 +589,16 @@ class SyncplayUser(object):
else: else:
return "{}".format(self.username) 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): class SyncplayUserlist(object):
def __init__(self, ui, client): def __init__(self, ui, client):
self.currentUser = SyncplayUser() self.currentUser = SyncplayUser()
@ -512,40 +619,46 @@ class SyncplayUserlist(object):
showOnOSD = constants.SHOW_OSD_WARNINGS showOnOSD = constants.SHOW_OSD_WARNINGS
else: else:
showOnOSD = constants.SHOW_DIFFERENT_ROOM_OSD showOnOSD = constants.SHOW_DIFFERENT_ROOM_OSD
if constants.SHOW_NONCONTROLLER_OSD == False and self.canControl(username) == False:
showOnOSD = False
hideFromOSD = not showOnOSD hideFromOSD = not showOnOSD
if room and not file_: if not file_:
message = getMessage("room-join-notification").format(username, room) message = getMessage("room-join-notification").format(username, room)
self.ui.showMessage(message, hideFromOSD) self.ui.showMessage(message, hideFromOSD)
elif room and file_: else:
duration = utils.formatTime(file_['duration']) duration = utils.formatTime(file_['duration'])
message = getMessage("playing-notification").format(username, file_['name'], duration) message = getMessage("playing-notification").format(username, file_['name'], duration)
if self.currentUser.room <> room or self.currentUser.username == username: if self.currentUser.room <> room or self.currentUser.username == username:
message += getMessage("playing-notification/room-addendum").format(room) message += getMessage("playing-notification/room-addendum").format(room)
self.ui.showMessage(message, hideFromOSD) self.ui.showMessage(message, hideFromOSD)
if self.currentUser.file and not self.currentUser.isFileSame(file_) and self.currentUser.room == room: if self.currentUser.file and not self.currentUser.isFileSame(file_) and self.currentUser.room == room:
message = getMessage("file-different-notification").format(username) message = getMessage("file-different-notification").format(username)
self.ui.showMessage(message, not constants.SHOW_OSD_WARNINGS) self.ui.showMessage(message, hideFromOSD)
differences = [] differences = []
differentName = not utils.sameFilename(self.currentUser.file['name'], file_['name']) differentName = not utils.sameFilename(self.currentUser.file['name'], file_['name'])
differentSize = not utils.sameFilesize(self.currentUser.file['size'], file_['size']) differentSize = not utils.sameFilesize(self.currentUser.file['size'], file_['size'])
differentDuration = not utils.sameFileduration(self.currentUser.file['duration'], file_['duration']) differentDuration = not utils.sameFileduration(self.currentUser.file['duration'], file_['duration'])
if differentName: if differentName:
differences.append("filename") differences.append("filename")
if differentSize: if differentSize:
differences.append("size") differences.append("size")
if differentDuration: if differentDuration:
differences.append("duration") differences.append("duration")
message = getMessage("file-differences-notification") + ", ".join(differences) message = getMessage("file-differences-notification") + ", ".join(differences)
self.ui.showMessage(message, not constants.SHOW_OSD_WARNINGS) 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 username == self.currentUser.username:
if isController is not None:
self.currentUser.setControllerStatus(isController)
return return
user = SyncplayUser(username, room, file_) user = SyncplayUser(username, room, file_)
if isController is not None:
user.setControllerStatus(isController)
self._users[username] = user self._users[username] = user
if not noMessage: if not noMessage:
self.__showUserChangeMessage(username, room, file_) self.__showUserChangeMessage(username, room, file_)
self.userListChange() self.userListChange(room)
def removeUser(self, username): def removeUser(self, username):
hideFromOSD = not constants.SHOW_DIFFERENT_ROOM_OSD hideFromOSD = not constants.SHOW_DIFFERENT_ROOM_OSD
@ -572,6 +685,8 @@ class SyncplayUserlist(object):
if self._users.has_key(username): if self._users.has_key(username):
user = self._users[username] user = self._users[username]
oldRoom = user.room if user.room else None oldRoom = user.room if user.room else None
if user.room != room:
user.setControllerStatus(isController=False)
self.__displayModUserMessage(username, room, file_, user, oldRoom) self.__displayModUserMessage(username, room, file_, user, oldRoom)
user.room = room user.room = room
if file_: if file_:
@ -580,12 +695,20 @@ class SyncplayUserlist(object):
self.__showUserChangeMessage(username, room, file_) self.__showUserChangeMessage(username, room, file_)
else: else:
self.addUser(username, room, file_) 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): def areAllFilesInRoomSame(self):
for user in self._users.itervalues(): for user in self._users.itervalues():
if user.room == self.currentUser.room and user.file and not self.currentUser.isFileSame(user.file): 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 return True
def areYouAloneInRoom(self): def areYouAloneInRoom(self):
@ -600,8 +723,18 @@ class SyncplayUserlist(object):
return True return True
return False return False
def userListChange(self): def canControl(self, username):
self._roomUsersChanged = True 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() self.ui.userListChange()
def roomStateConfirmed(self): def roomStateConfirmed(self):
@ -637,6 +770,10 @@ class UiManager(object):
self.__ui = ui self.__ui = ui
self.lastError = "" 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): def showMessage(self, message, noPlayer=False, noTimestamp=False):
if not noPlayer: self.showOSDMessage(message) if not noPlayer: self.showOSDMessage(message)
self.__ui.showMessage(message, noTimestamp) self.__ui.showMessage(message, noTimestamp)
@ -648,6 +785,9 @@ class UiManager(object):
if constants.SHOW_OSD and self._client._player: if constants.SHOW_OSD and self._client._player:
self._client._player.displayMessage(message, duration * 1000) self._client._player.displayMessage(message, duration * 1000)
def setControllerStatus(self, username, isController):
self.__ui.setControllerStatus(username, isController)
def showErrorMessage(self, message, criticalerror=False): def showErrorMessage(self, message, criticalerror=False):
if message <> self.lastError: # Avoid double call bug if message <> self.lastError: # Avoid double call bug
self.lastError = message self.lastError = message
@ -662,5 +802,10 @@ class UiManager(object):
def markEndOfUserlist(self): def markEndOfUserlist(self):
self.__ui.markEndOfUserlist() self.__ui.markEndOfUserlist()
def updateRoomName(self, room=""):
self.__ui.updateRoomName(room)
def drop(self): def drop(self):
self.__ui.drop() 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_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_BUTTON_LABELS = True # If disabled, only shows icons for main GUI buttons
SHOW_TOOLTIPS = True SHOW_TOOLTIPS = True
WARN_ABOUT_MISSING_STRINGS = False # (If debug mode is enabled)
#Overriden by config #Overriden by config
SHOW_OSD = True # Sends Syncplay messages to media player OSD SHOW_OSD = True # Sends Syncplay messages to media player OSD
SHOW_OSD_WARNINGS = True # Show warnings if playing different file, alone in room 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_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_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_DIFFERENT_ROOM_OSD = False # Show OSD notifications for events relating to room user is not in
SHOW_DURATION_NOTIFICATION = True SHOW_DURATION_NOTIFICATION = True
DEBUG_MODE = False
#Changing these might be ok #Changing these might be ok
DEFAULT_REWIND_THRESHOLD = 4 DEFAULT_REWIND_THRESHOLD = 4
MINIMUM_REWIND_THRESHOLD = 3 MINIMUM_REWIND_THRESHOLD = 3
DEFAULT_FASTFORWARD_THRESHOLD = 5
MINIMUM_FASTFORWARD_THRESHOLD = 2
FASTFORWARD_EXTRA_TIME = 0.25
SEEK_THRESHOLD = 1 SEEK_THRESHOLD = 1
SLOWDOWN_RATE = 0.95 SLOWDOWN_RATE = 0.95
DEFAULT_SLOWDOWN_KICKIN_THRESHOLD = 1.5 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 SYNC_ON_PAUSE = True # Client seek to global position - subtitles may disappear on some media players
#Usually there's no need to adjust these #Usually there's no need to adjust these
FILENAME_STRIP_REGEX = u"[-~_\.\[\](): ]" 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_UNDO = ["u", "undo", "revert"]
COMMANDS_LIST = ["l", "list", "users"] COMMANDS_LIST = ["l", "list", "users"]
COMMANDS_PAUSE = ["p", "play", "pause"] COMMANDS_PAUSE = ["p", "play", "pause"]
COMMANDS_ROOM = ["r", "room"] COMMANDS_ROOM = ["r", "room"]
COMMANDS_HELP = ['help', 'h', '?', '/?', r'\?'] COMMANDS_HELP = ['help', 'h', '?', '/?', r'\?']
COMMANDS_CREATE = ['c','create']
COMMANDS_AUTH = ['a','auth']
MPC_MIN_VER = "1.6.4" MPC_MIN_VER = "1.6.4"
VLC_MIN_VERSION = "2.0.0" VLC_MIN_VERSION = "2.0.0"
VLC_INTERFACE_MIN_VERSION = "0.2.1" VLC_INTERFACE_MIN_VERSION = "0.2.1"
CONTROLLED_ROOMS_MIN_VERSION = "1.3.0"
MPC_PATHS = [ MPC_PATHS = [
r"C:\Program Files (x86)\MPC-HC\mpc-hc.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-hc.exe",
r"C:\Program Files\MPC-HC\mpc-hc64.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-hc.exe",
r"C:\Program Files\Media Player Classic - Home Cinema\mpc-hc64.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)\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 (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\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 (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\combined community codec pack\mpc\mpc-hc.exe",
r"C:\Program Files\MPC HomeCinema (x64)\mpc-hc64.exe", r"c:\program files\mpc homecinema (x64)\mpc-hc64.exe",
] ]
MPLAYER_PATHS = ["mplayer2", "mplayer"] 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", 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", r"c:\program Files (x86)\mpv\mpv.exe", r"c:\program Files (x86)\mpv-player\mpv.exe",
"/Applications/mpv.app/Contents/MacOS/mpv"] "/Applications/mpv.app/Contents/MacOS/mpv"]
VLC_PATHS = [ VLC_PATHS = [
r"C:\Program Files (x86)\VideoLAN\VLC\vlc.exe", r"c:\program files (x86)\videolan\vlc\vlc.exe",
r"C:\Program Files\VideoLAN\VLC\vlc.exe", r"c:\program files\videolan\vlc\vlc.exe",
"/usr/bin/vlc", "/usr/bin/vlc",
"/usr/bin/vlc-wrapper", "/usr/bin/vlc-wrapper",
"/Applications/VLC.app/Contents/MacOS/VLC", "/Applications/VLC.app/Contents/MacOS/VLC",
@ -89,14 +100,16 @@ MPC_LOCK_WAIT_TIME = 0.2
MPC_RETRY_WAIT_TIME = 0.01 MPC_RETRY_WAIT_TIME = 0.01
MPC_MAX_RETRIES = 30 MPC_MAX_RETRIES = 30
MPC_PAUSE_TOGGLE_DELAY = 0.05 MPC_PAUSE_TOGGLE_DELAY = 0.05
MPV_NEWFILE_IGNORE_TIME = 1
MPV_LOCK_WAIT_TIME = 0.2
VLC_OPEN_MAX_WAIT_TIME = 15 VLC_OPEN_MAX_WAIT_TIME = 15
VLC_MIN_PORT = 10000 VLC_MIN_PORT = 10000
VLC_MAX_PORT = 55000 VLC_MAX_PORT = 55000
#These are not changes you're looking for #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_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_SUBCHECKBOX = "QCheckBox, QLabel {{ margin-left: 6px; padding-left: 21px; 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_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_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_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; }" STYLE_NOTIFICATIONBOX = "Username { color: #367AA9; font-weight:bold; }"
@ -104,12 +117,19 @@ STYLE_USERNAME = "color: #367AA9; font-weight:bold;"
STYLE_ERRORNOTIFICATION = "color: red;" STYLE_ERRORNOTIFICATION = "color: red;"
STYLE_DIFFERENTITEM_COLOR = 'red' STYLE_DIFFERENTITEM_COLOR = 'red'
STYLE_NOFILEITEM_COLOR = 'blue' 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 # --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', 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'] VLC_SLAVE_NONOSX_ARGS = ['--no-one-instance', '--no-one-instance-when-started-from-file']
MPLAYER_ANSWER_REGEX = "^ANS_([a-zA-Z_-]+)=(.+)$|^(Exiting)\.\.\. \((.+)\)$" MPLAYER_ANSWER_REGEX = "^ANS_([a-zA-Z_-]+)=(.+)$|^(Exiting)\.\.\. \((.+)\)$"
VLC_ANSWER_REGEX = r"(?:^(?P<command>[a-zA-Z_]+)(?:\: )?(?P<argument>.*))" VLC_ANSWER_REGEX = r"(?:^(?P<command>[a-zA-Z_]+)(?:\: )?(?P<argument>.*))"
@ -127,3 +147,5 @@ ERROR_MESSAGE_MARKER = "*"
LOAD_SAVE_MANUALLY_MARKER = "!" LOAD_SAVE_MANUALLY_MARKER = "!"
CONFIG_NAME_MARKER = ":" CONFIG_NAME_MARKER = ":"
CONFIG_VALUE_MARKER = "=" CONFIG_VALUE_MARKER = "="
USERITEM_CONTROLLER_ROLE = 0
USERITEM_READY_ROLE = 1

View File

@ -2,6 +2,7 @@
from syncplay import constants from syncplay import constants
en = { en = {
"LANGUAGE" : "English",
# Client notifications # Client notifications
"config-cleared-notification" : "Settings cleared. Changes will be saved when you store a valid configuration.", "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 "retrying-notification" : "%s, Retrying in %d seconds...", # Seconds
"rewind-notification" : "Rewinded due to time difference with <{}>", # User "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 "slowdown-notification" : "Slowing down due to time difference with <{}>", # User
"revert-notification" : "Reverting speed back to normal", "revert-notification" : "Reverting speed back to normal",
@ -31,8 +33,12 @@ en = {
"playing-notification" : "<{}> is playing '{}' ({})", # User, file, duration "playing-notification" : "<{}> is playing '{}' ({})", # User, file, duration
"playing-notification/room-addendum" : " in room: '{}'", # Room "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-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", "room-files-not-same" : "Not all files played in the room are the same",
"alone-in-the-room": "You're alone in the room", "alone-in-the-room": "You're alone in the room",
@ -53,10 +59,13 @@ en = {
"commandlist-notification/pause" : "\tp - toggle pause", "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/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/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 "syncplay-version-notification" : "Syncplay version: {}", # syncplay.version
"more-info-notification" : "More info available at: {}", # projectURL "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.", "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-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 "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", "server-timeout-error" : "Connection with server timed out",
"mpc-slave-error" : "Unable to start MPC in slave mode!", "mpc-slave-error" : "Unable to start MPC in slave mode!",
"mpc-version-insufficient-error" : "MPC version not sufficient, please use `mpc-hc` >= `{}`", "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-file-open-error" : "Player failed opening file",
"player-path-error" : "Player path is not set properly", "player-path-error" : "Player path is not set properly",
"hostname-empty-error" : "Hostname can't be empty", "hostname-empty-error" : "Hostname can't be empty",
@ -82,6 +92,12 @@ en = {
"unable-to-start-client-error" : "Unable to start client", "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", "not-json-error" : "Not a json encoded string\n",
"hello-arguments-error" : "Not enough Hello arguments\n", "hello-arguments-error" : "Not enough Hello arguments\n",
"version-mismatch-error" : "Mismatch between versions of client and server\n", "version-mismatch-error" : "Mismatch between versions of client and server\n",
@ -105,7 +121,7 @@ en = {
"file-argument" : 'file to play', "file-argument" : 'file to play',
"args-argument" : 'player options, if you need to pass options starting with - prepend them with single \'--\' argument', "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', "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-argument" : 'prints your version',
"version-message" : "You're using Syncplay version {} ({})", "version-message" : "You're using Syncplay version {} ({})",
@ -127,6 +143,7 @@ en = {
"more-title" : "Show more settings", "more-title" : "Show more settings",
"slowdown-threshold-label" : "Slow down threshold:", "slowdown-threshold-label" : "Slow down threshold:",
"rewind-threshold-label" : "Rewind threshold:", "rewind-threshold-label" : "Rewind threshold:",
"fastforward-threshold-label" : "Fast-forward threshold:",
"never-rewind-value" : "Never", "never-rewind-value" : "Never",
"seconds-suffix" : " secs", "seconds-suffix" : " secs",
"privacy-sendraw-option" : "Send raw", "privacy-sendraw-option" : "Send raw",
@ -135,7 +152,7 @@ en = {
"filename-privacy-label" : "Filename information:", "filename-privacy-label" : "Filename information:",
"filesize-privacy-label" : "File size information:", "filesize-privacy-label" : "File size information:",
"slowondesync-label" : "Slow down on minor desync (not supported on MPC-HC)", "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)", "pauseonleave-label" : "Pause when user leaves (e.g. if they are disconnected)",
"forceguiprompt-label" : "Don't always show this dialog", # (Inverted) "forceguiprompt-label" : "Don't always show this dialog", # (Inverted)
"nostore-label" : "Don't store this configuration", # (Inverted) "nostore-label" : "Don't store this configuration", # (Inverted)
@ -143,16 +160,21 @@ en = {
"showosdwarnings-label" : "Include warnings (e.g. when files are different)", "showosdwarnings-label" : "Include warnings (e.g. when files are different)",
"showsameroomosd-label" : "Include events in your room", "showsameroomosd-label" : "Include events in your room",
"shownoncontrollerosd-label" : "Include events from non-controllers in controlled rooms",
"showdifferentroomosd-label" : "Include events in other rooms", "showdifferentroomosd-label" : "Include events in other rooms",
"showslowdownosd-label" :"Include slowing down / reverting notification", "showslowdownosd-label" :"Include slowing down / reverting notification",
"showcontactinfo-label" : "Show contact info box", "showcontactinfo-label" : "Show contact info box",
"showbuttonlabels-label" : "Show labels on buttons", "language-label" : "Language",
"showtooltips-label" : "Show tooltips",
"showdurationnotification-label" : "Warn about media duration mismatches", "showdurationnotification-label" : "Warn about media duration mismatches",
"basics-label" : "Basics", "basics-label" : "Basics",
"sync-label" : "Sync", "sync-label" : "Sync",
"sync-lagging-title" : "If others are lagging behind...",
"sync-other-title" : "Other sync options",
"messages-label" : "Messages", "messages-label" : "Messages",
"messages-osd-title" : "On-screen Display settings",
"messages-other-title" : "Other display settings",
"privacy-label" : "Privacy", "privacy-label" : "Privacy",
"privacy-title" : "Privacy settings",
"help-label" : "Help", "help-label" : "Help",
"reset-label" : "Restore defaults", "reset-label" : "Restore defaults",
@ -166,10 +188,12 @@ en = {
"undoseek-guibuttonlabel" : "Undo seek", "undoseek-guibuttonlabel" : "Undo seek",
"togglepause-guibuttonlabel" : "Toggle pause", "togglepause-guibuttonlabel" : "Toggle pause",
"play-guibuttonlabel" : "Play", "play-guibuttonlabel" : "Play",
"pause-guibuttonlabel" : "Play", "pause-guibuttonlabel" : "Pause",
"roomuser-heading-label" : "Room / User", "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", "notifications-heading-label" : "Notifications",
"userlist-heading-label" : "List of who is playing what", "userlist-heading-label" : "List of who is playing what",
"othercommands-heading-label" : "Other commands", "othercommands-heading-label" : "Other commands",
@ -183,12 +207,24 @@ en = {
"exit-menu-label" : "E&xit", "exit-menu-label" : "E&xit",
"advanced-menu-label" : "&Advanced", "advanced-menu-label" : "&Advanced",
"setoffset-menu-label" : "Set &offset", "setoffset-menu-label" : "Set &offset",
"createcontrolledroom-menu-label" : "&Create controlled room",
"identifyascontroller-menu-label" : "&Identify as room controller",
"help-menu-label" : "&Help", "help-menu-label" : "&Help",
"userguide-menu-label" : "Open user &guide", "userguide-menu-label" : "Open user &guide",
"setoffset-msgbox-label" : "Set offset", "setoffset-msgbox-label" : "Set offset",
"offsetinfo-msgbox-label" : "Offset (see http://syncplay.pl/guide/ for usage instructions):", "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 # 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.", "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.", "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), "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), "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.", "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.", "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-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-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.", "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.", "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.", "pauseonleave-tooltip" : "Pause playback if you get disconnected or someone leaves from your room.",
"rewindondesync-label" : "Rewind on major desync (highly recommended)", "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) "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) "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!", "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.", "showosd-tooltip" : "Sends Syncplay messages to media player OSD.",
"showosdwarnings-tooltip" : "Show warnings if playing different file, alone in room.", "showosdwarnings-tooltip" : "Show warnings if playing different file, alone in room.",
"showsameroomosd-tooltip" : "Show OSD notifications for events relating to room user is in.", "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.", "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.", "showslowdownosd-tooltip" :"Show notifications of slowing down / reverting on time difference.",
"showcontactinfo-tooltip" : "Show information box about contacting Syncplay developers in main Syncplay window.", "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.", "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.", "help-tooltip" : "Opens the Syncplay.pl user guide.",
"reset-tooltip" : "Reset all settings to the default configuration.", "reset-tooltip" : "Reset all settings to the default configuration.",
@ -247,6 +286,7 @@ en = {
"welcome-server-notification" : "Welcome to Syncplay server, ver. {0}", # version "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-connected-room-server-notification" : "{0}({2}) connected to room '{1}'", # username, host, room
"client-left-server-notification" : "{0} left server", # name "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 # Server arguments
@ -255,11 +295,10 @@ en = {
"server-port-argument" : 'server TCP port', "server-port-argument" : 'server TCP port',
"server-password-argument" : 'server password', "server-password-argument" : 'server password',
"server-isolate-room-argument" : 'should rooms be isolated?', "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-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-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-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 # Server errors
"unknown-command-server-error" : "Unknown command {}", # message "unknown-command-server-error" : "Unknown command {}", # message
@ -269,13 +308,12 @@ en = {
"password-required-server-error" : "Password required", "password-required-server-error" : "Password required",
"wrong-password-server-error" : "Wrong password supplied", "wrong-password-server-error" : "Wrong password supplied",
"hello-server-error" : "Not enough Hello arguments", "hello-server-error" : "Not enough Hello arguments",
"version-mismatch-server-error" : "Mismatch between versions of client and server"
} }
pl = { pl = {
"LANGUAGE" : "Polski", # (Polish)
# Client notifications # Client notifications
"connection-attempt-notification" : u"Próba połączenia z {}:{}", # Port, IP "connection-attempt-notification" : u"Próba połączenia z {}:{}", # Port, IP
"reconnection-attempt-notification" : u"Połączenie z serwerem zostało przerwane, ponowne łączenie", "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" "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 = { messages = {
"en": en, "en": en,
"pl": pl, "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): 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): def getMessage(type_, locale=None):
if constants.SHOW_BUTTON_LABELS == False: if constants.SHOW_BUTTON_LABELS == False:
@ -327,7 +955,7 @@ def getMessage(type_, locale=None):
if constants.SHOW_TOOLTIPS == False: if constants.SHOW_TOOLTIPS == False:
if "-tooltip" in type_: if "-tooltip" in type_:
return "" return ""
lang = messages["current"] lang = messages["CURRENT"]
if locale and messages.has_key(locale): if locale and messages.has_key(locale):
if messages[locale].has_key(type_): if messages[locale].has_key(type_):
return unicode(messages[locale][type_]) return unicode(messages[locale][type_])

View File

@ -49,7 +49,7 @@ class BasePlayer(object):
''' '''
@type filePath: string @type filePath: string
''' '''
def openFile(self, filePath): def openFile(self, filePath, resetPosition=False):
raise NotImplementedError() raise NotImplementedError()
@ -83,6 +83,23 @@ class BasePlayer(object):
def getExpandedPath(path): def getExpandedPath(path):
raise NotImplementedError() 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): class DummyPlayer(BasePlayer):
@ -101,3 +118,7 @@ class DummyPlayer(BasePlayer):
@staticmethod @staticmethod
def getExpandedPath(path): def getExpandedPath(path):
return path return path
@staticmethod
def getPlayerPathErrors(playerPath, filePath):
return None

View File

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

View File

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

View File

@ -1,23 +1,35 @@
import re
import subprocess
from syncplay.players.mplayer import MplayerPlayer from syncplay.players.mplayer import MplayerPlayer
from syncplay.messages import getMessage
from syncplay import constants from syncplay import constants
import os import os, sys, time
class MpvPlayer(MplayerPlayer): class MpvPlayer(MplayerPlayer):
SLAVE_ARGS = constants.MPV_SLAVE_ARGS RE_VERSION = re.compile('.*mpv (\d)\.(\d)\.\d.*')
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')
@staticmethod @staticmethod
def run(client, playerPath, filePath, args): 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 @staticmethod
def getDefaultPlayerPathsList(): def getDefaultPlayerPathsList():
@ -53,3 +65,112 @@ class MpvPlayer(MplayerPlayer):
@staticmethod @staticmethod
def getIconPath(path): def getIconPath(path):
return constants.MPV_ICONPATH 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 asynchat, asyncore
import urllib import urllib
from syncplay.messages import getMessage from syncplay.messages import getMessage
import time
class VlcPlayer(BasePlayer): class VlcPlayer(BasePlayer):
speedSupported = True speedSupported = True
customOpenDialog = False
RE_ANSWER = re.compile(constants.VLC_ANSWER_REGEX) RE_ANSWER = re.compile(constants.VLC_ANSWER_REGEX)
SLAVE_ARGS = constants.VLC_SLAVE_ARGS SLAVE_ARGS = constants.VLC_SLAVE_ARGS
if not sys.platform.startswith('darwin'): if not sys.platform.startswith('darwin'):
@ -30,6 +30,13 @@ class VlcPlayer(BasePlayer):
self._filename = None self._filename = None
self._filepath = None self._filepath = None
self._filechanged = False 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._durationAsk = threading.Event()
self._filenameAsk = threading.Event() self._filenameAsk = threading.Event()
@ -91,8 +98,7 @@ class VlcPlayer(BasePlayer):
self._listener.sendLine("set-rate: {:.2n}".format(value)) self._listener.sendLine("set-rate: {:.2n}".format(value))
def setPosition(self, value): def setPosition(self, value):
self._position = value self._listener.sendLine("set-position: {}".format(value).replace(".",self.radixChar))
self._listener.sendLine("set-position: {}".format(value))
def setPaused(self, value): def setPaused(self, value):
self._paused = value self._paused = value
@ -112,7 +118,7 @@ class VlcPlayer(BasePlayer):
def _isASCII (self, s): def _isASCII (self, s):
return all(ord(c) < 128 for c in s) return all(ord(c) < 128 for c in s)
def openFile(self, filePath): def openFile(self, filePath, resetPosition=False):
if self._isASCII(filePath): if self._isASCII(filePath):
self._listener.sendLine('load-file: {}'.format(filePath.encode('ascii', 'ignore'))) self._listener.sendLine('load-file: {}'.format(filePath.encode('ascii', 'ignore')))
else: else:
@ -125,6 +131,7 @@ class VlcPlayer(BasePlayer):
self._listener.sendLine("get-filename") self._listener.sendLine("get-filename")
def lineReceived(self, line): def lineReceived(self, line):
self._client.ui.showDebugMessage("player >> {}".format(line))
match, name, value = self.RE_ANSWER.match(line), "", "" match, name, value = self.RE_ANSWER.match(line), "", ""
if match: if match:
name, value = match.group('command'), match.group('argument') name, value = match.group('command'), match.group('argument')
@ -193,6 +200,10 @@ class VlcPlayer(BasePlayer):
return True return True
return False return False
@staticmethod
def getPlayerPathErrors(playerPath, filePath):
return None
@staticmethod @staticmethod
def getIconPath(path): def getIconPath(path):
return constants.VLC_ICONPATH return constants.VLC_ICONPATH
@ -322,15 +333,14 @@ class VlcPlayer(BasePlayer):
self.__playerController.drop() self.__playerController.drop()
def found_terminator(self): def found_terminator(self):
# print "received: {}".format("".join(self._ibuffer))
self.__playerController.lineReceived("".join(self._ibuffer)) self.__playerController.lineReceived("".join(self._ibuffer))
self._ibuffer = [] self._ibuffer = []
def sendLine(self, line): def sendLine(self, line):
if self.connected: if self.connected:
# print "send: {}".format(line)
try: try:
self.push(line + "\n") self.push(line + "\n")
self._client.ui.showDebugMessage("player >> {}".format(line))
except: except:
pass pass
if line == "close-vlc": if line == "close-vlc":

View File

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

View File

@ -1,4 +1,5 @@
import hashlib import hashlib
import random
from twisted.internet import task, reactor from twisted.internet import task, reactor
from twisted.internet.protocol import Factory from twisted.internet.protocol import Factory
import syncplay import syncplay
@ -10,13 +11,18 @@ import codecs
import os import os
from string import Template from string import Template
import argparse import argparse
from syncplay.utils import RoomPasswordProvider, NotControlledRoom, RandomStringGenerator, meetsMinVersion
class SyncFactory(Factory): 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) print getMessage("welcome-server-notification").format(syncplay.version)
if password: if password:
password = hashlib.md5(password).hexdigest() password = hashlib.md5(password).hexdigest()
self.password = password 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 self._motdFilePath = motdFilePath
if not isolateRooms: if not isolateRooms:
self._roomManager = RoomManager() self._roomManager = RoomManager()
@ -36,7 +42,7 @@ class SyncFactory(Factory):
def getMotd(self, userIp, username, room, clientVersion): def getMotd(self, userIp, username, room, clientVersion):
oldClient = False oldClient = False
if constants.WARN_OLD_CLIENTS: 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 oldClient = True
if self._motdFilePath and os.path.isfile(self._motdFilePath): if self._motdFilePath and os.path.isfile(self._motdFilePath):
tmpl = codecs.open(self._motdFilePath, "r", "utf-8-sig").read() tmpl = codecs.open(self._motdFilePath, "r", "utf-8-sig").read()
@ -54,7 +60,7 @@ class SyncFactory(Factory):
else: else:
return "" return ""
def addWatcher(self, watcherProtocol, username, roomName, roomPassword): def addWatcher(self, watcherProtocol, username, roomName):
username = self._roomManager.findFreeUsername(username) username = self._roomManager.findFreeUsername(username)
watcher = Watcher(self, watcherProtocol, username) watcher = Watcher(self, watcherProtocol, username)
self.setWatcherRoom(watcher, roomName, asJoin=True) self.setWatcherRoom(watcher, roomName, asJoin=True)
@ -65,13 +71,16 @@ class SyncFactory(Factory):
self.sendJoinMessage(watcher) self.sendJoinMessage(watcher)
else: else:
self.sendRoomSwitchMessage(watcher) self.sendRoomSwitchMessage(watcher)
if RoomPasswordProvider.isControlledRoom(roomName):
for controller in watcher.getRoom().getControllers():
watcher.sendControlledRoomAuthStatus(True, controller, roomName)
def sendRoomSwitchMessage(self, watcher): def sendRoomSwitchMessage(self, watcher):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None) l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, None)
self._roomManager.broadcast(watcher, l) self._roomManager.broadcast(watcher, l)
def removeWatcher(self, watcher): def removeWatcher(self, watcher):
if watcher.getRoom(): if watcher and watcher.getRoom():
self.sendLeftMessage(watcher) self.sendLeftMessage(watcher)
self._roomManager.removeWatcher(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 l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"joined": True}) if w != watcher else None
self._roomManager.broadcast(watcher, l) 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) l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), watcher.getFile(), None)
self._roomManager.broadcast(watcher, l) self._roomManager.broadcast(watcher, l)
def forcePositionUpdate(self, room, watcher, doSeek): def forcePositionUpdate(self, watcher, doSeek, watcherPauseState):
room = watcher.getRoom() room = watcher.getRoom()
paused, position = room.isPaused(), watcher.getPosition() if room.canControl(watcher):
setBy = watcher paused, position = room.isPaused(), watcher.getPosition()
room.setPosition(watcher.getPosition(), setBy) setBy = watcher
l = lambda w: w.sendState(position, paused, doSeek, setBy, True) l = lambda w: w.sendState(position, paused, doSeek, setBy, True)
self._roomManager.broadcastRoom(watcher, l) 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): def getAllWatchersForUser(self, forUser):
return self._roomManager.getAllWatchersForUser(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): class RoomManager(object):
def __init__(self): def __init__(self):
self._rooms = {} self._rooms = {}
@ -135,7 +163,10 @@ class RoomManager(object):
if roomName in self._rooms: if roomName in self._rooms:
return self._rooms[roomName] return self._rooms[roomName]
else: else:
room = Room(roomName) if RoomPasswordProvider.isControlledRoom(roomName):
room = ControlledRoom(roomName)
else:
room = Room(roomName)
self._rooms[roomName] = room self._rooms[roomName] = room
return room return room
@ -228,6 +259,44 @@ class Room(object):
def getSetBy(self): def getSetBy(self):
return self._setBy 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): class Watcher(object):
def __init__(self, server, connector, name): def __init__(self, server, connector, name):
self._server = server self._server = server
@ -243,7 +312,7 @@ class Watcher(object):
def setFile(self, file_): def setFile(self, file_):
self._file = file_ self._file = file_
self._server.sendFileUpdate(self, file_) self._server.sendFileUpdate(self)
def setRoom(self, room): def setRoom(self, room):
self._room = room self._room = room
@ -277,16 +346,22 @@ class Watcher(object):
def sendSetting(self, user, room, file_, event): def sendSetting(self, user, room, file_, event):
self._connector.sendUserSetting(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): def __lt__(self, b):
if self.getPosition() is None or self._file is None: if self.getPosition() is None or self._file is None:
return False 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 True
return self.getPosition() < b.getPosition() return self.getPosition() < b.getPosition()
def _scheduleSendState(self): def _scheduleSendState(self):
self._sendStateTimer = task.LoopingCall(self._askForStateUpdate) 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): def _askForStateUpdate(self, doSeek=False, forcedUpdate=False):
self._server.sendState(self, doSeek, forcedUpdate) self._server.sendState(self, doSeek, forcedUpdate)
@ -313,18 +388,25 @@ class Watcher(object):
return False return False
return self._room.isPaused() and not paused or not self._room.isPaused() and paused 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): def updateState(self, position, paused, doSeek, messageAge):
pauseChanged = self.__hasPauseChanged(paused) pauseChanged = self.__hasPauseChanged(paused)
self._lastUpdatedOn = time.time() self._lastUpdatedOn = time.time()
if pauseChanged: if pauseChanged:
self.getRoom().setPaused(Room.STATE_PAUSED if paused else Room.STATE_PLAYING, self) self.getRoom().setPaused(Room.STATE_PAUSED if paused else Room.STATE_PLAYING, self)
if position is not None: if position is not None:
if not paused: position = self._updatePositionByAge(messageAge, paused, position)
position += messageAge
self.setPosition(position) self.setPosition(position)
if doSeek or pauseChanged: 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): class ConfigurationGetter(object):
def getConfiguration(self): 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('--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('--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('--isolate-rooms', action='store_true', help=getMessage("server-isolate-room-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")) 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, "playerClass": None,
"slowdownThreshold": constants.DEFAULT_SLOWDOWN_KICKIN_THRESHOLD, "slowdownThreshold": constants.DEFAULT_SLOWDOWN_KICKIN_THRESHOLD,
"rewindThreshold": constants.DEFAULT_REWIND_THRESHOLD, "rewindThreshold": constants.DEFAULT_REWIND_THRESHOLD,
"fastforwardThreshold": constants.DEFAULT_FASTFORWARD_THRESHOLD,
"rewindOnDesync": True, "rewindOnDesync": True,
"slowOnDesync": True, "slowOnDesync": True,
"fastforwardOnDesync": True,
"dontSlowDownWithMe": False, "dontSlowDownWithMe": False,
"filenamePrivacyMode": constants.PRIVACY_SENDRAW_MODE, "filenamePrivacyMode": constants.PRIVACY_SENDRAW_MODE,
"filesizePrivacyMode": constants.PRIVACY_SENDRAW_MODE, "filesizePrivacyMode": constants.PRIVACY_SENDRAW_MODE,
@ -50,9 +52,8 @@ class ConfigurationGetter(object):
"showSlowdownOSD" : True, "showSlowdownOSD" : True,
"showDifferentRoomOSD" : False, "showDifferentRoomOSD" : False,
"showSameRoomOSD" : True, "showSameRoomOSD" : True,
"showNonControllerOSD" : False,
"showContactInfo" : True, "showContactInfo" : True,
"showButtonLabels" : True,
"showTooltips" : True,
"showDurationNotification" : True "showDurationNotification" : True
} }
@ -87,21 +88,21 @@ class ConfigurationGetter(object):
"showSlowdownOSD", "showSlowdownOSD",
"showDifferentRoomOSD", "showDifferentRoomOSD",
"showSameRoomOSD", "showSameRoomOSD",
"showNonControllerOSD",
"showContactInfo" , "showContactInfo" ,
"showButtonLabels",
"showTooltips",
"showDurationNotification" "showDurationNotification"
] ]
self._numeric = [ self._numeric = [
"slowdownThreshold", "slowdownThreshold",
"rewindThreshold" "rewindThreshold",
"fastforwardThreshold",
] ]
self._iniStructure = { self._iniStructure = {
"server_data": ["host", "port", "password"], "server_data": ["host", "port", "password"],
"client_settings": ["name", "room", "playerPath", "slowdownThreshold", "rewindThreshold", "slowOnDesync", "rewindOnDesync", "dontSlowDownWithMe", "forceGuiPrompt", "filenamePrivacyMode", "filesizePrivacyMode", "pauseOnLeave"], "client_settings": ["name", "room", "playerPath", "slowdownThreshold", "rewindThreshold", "fastforwardThreshold", "slowOnDesync", "rewindOnDesync", "fastforwardOnDesync", "dontSlowDownWithMe", "forceGuiPrompt", "filenamePrivacyMode", "filesizePrivacyMode", "pauseOnLeave"],
"gui": ["showOSD", "showOSDWarnings", "showSlowdownOSD", "showDifferentRoomOSD", "showSameRoomOSD", "showContactInfo" , "showButtonLabels", "showTooltips", "showDurationNotification"], "gui": ["showOSD", "showOSDWarnings", "showSlowdownOSD", "showDifferentRoomOSD", "showSameRoomOSD", "showNonControllerOSD", "showContactInfo" , "showDurationNotification"],
"general": ["language"] "general": ["language"]
} }
@ -143,20 +144,20 @@ class ConfigurationGetter(object):
if player: if player:
self._config["playerClass"] = player self._config["playerClass"] = player
else: else:
raise InvalidConfigValue("Player path is not set properly") raise InvalidConfigValue(getMessage("player-path-config-error"))
if player.__name__ in ['MpvPlayer', 'MplayerPlayer']: playerPathErrors = player.getPlayerPathErrors(self._config["playerPath"], self._config['file'] if self._config['file'] else None)
if not self._config['file']: if playerPathErrors:
raise InvalidConfigValue("File must be selected before starting your player") raise InvalidConfigValue(playerPathErrors)
elif key == "host": elif key == "host":
self._config["host"], self._config["port"] = self._splitPortAndHost(self._config["host"]) self._config["host"], self._config["port"] = self._splitPortAndHost(self._config["host"])
hostNotValid = (self._config["host"] == "" or self._config["host"] is None) hostNotValid = (self._config["host"] == "" or self._config["host"] is None)
portNotValid = (_isPortValid(self._config["port"]) == False) portNotValid = (_isPortValid(self._config["port"]) == False)
if hostNotValid: if hostNotValid:
raise InvalidConfigValue("Hostname can't be empty") raise InvalidConfigValue(getMessage("no-hostname-config-error"))
elif portNotValid: 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: 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): def _overrideConfigWithArgs(self, args):
for key, val in vars(args).items(): for key, val in vars(args).items():

View File

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

View File

@ -36,6 +36,9 @@ class ConsoleUI(threading.Thread):
except EOFError: except EOFError:
pass pass
def updateRoomName(self, room=""):
pass
def promptFor(self, prompt=">", message=""): def promptFor(self, prompt=">", message=""):
if message <> "": if message <> "":
print(message) print(message)
@ -49,14 +52,17 @@ class ConsoleUI(threading.Thread):
message = u"In room '{}':".format(room) message = u"In room '{}':".format(room)
self.showMessage(message, True) self.showMessage(message, True)
for user in rooms[room]: 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: if user.file:
message = u"{} is playing:".format(username) message = u"{} is playing:".format(username)
self.showMessage(message, True) self.showMessage(message, True)
message = u" File: '{}' ({})".format(user.file['name'], formatTime(user.file['duration'])) message = u" File: '{}' ({})".format(user.file['name'], formatTime(user.file['duration']))
if currentUser.file: if currentUser.file:
if user.file['name'] == currentUser.file['name'] and user.file['size'] != currentUser.file['size']: 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) self.showMessage(message, True)
else: else:
message = u"{} is not playing a file".format(username) message = u"{} is not playing a file".format(username)
@ -134,6 +140,15 @@ class ConsoleUI(threading.Thread):
self._syncplayClient.setRoom(room) self._syncplayClient.setRoom(room)
self._syncplayClient.sendRoom() 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: else:
if self._tryAdvancedCommands(data): if self._tryAdvancedCommands(data):
return return
@ -146,6 +161,8 @@ class ConsoleUI(threading.Thread):
self.showMessage(getMessage("commandlist-notification/pause"), True) self.showMessage(getMessage("commandlist-notification/pause"), True)
self.showMessage(getMessage("commandlist-notification/seek"), True) self.showMessage(getMessage("commandlist-notification/seek"), True)
self.showMessage(getMessage("commandlist-notification/help"), 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("syncplay-version-notification").format(syncplay.version), True)
self.showMessage(getMessage("more-info-notification").format(syncplay.projectURL), True) self.showMessage(getMessage("more-info-notification").format(syncplay.projectURL), True)

View File

@ -1,15 +1,74 @@
from PySide import QtGui #@UnresolvedImport from PySide import QtGui
from PySide.QtCore import Qt, QSettings, QSize, QPoint #@UnresolvedImport from PySide.QtCore import Qt, QSettings, QSize, QPoint
from syncplay import utils, constants, version from syncplay import utils, constants, version
from syncplay.messages import getMessage from syncplay.messages import getMessage
import sys import sys
import time import time
import re import re
import os import os
import threading from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration, RoomPasswordProvider, formatSize
from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration
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 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): def addClient(self, client):
self._syncplayClient = client self._syncplayClient = client
self.roomInput.setText(self._syncplayClient.getRoom()) self.roomInput.setText(self._syncplayClient.getRoom())
@ -17,29 +76,14 @@ class MainWindow(QtGui.QMainWindow):
try: try:
if self.contactLabel and not self.config['showContactInfo']: if self.contactLabel and not self.config['showContactInfo']:
self.contactLabel.hide() self.contactLabel.hide()
if not self.config['showButtonLabels']: if not constants.SHOW_BUTTON_LABELS:
if constants.MERGE_PLAYPAUSE_BUTTONS: self.hideRoomSeekLabels()
self.playpauseButton.setText("") self.hideMiscLabels()
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())
except (): except ():
pass pass
def promptFor(self, prompt=">", message=""): def promptFor(self, prompt=">", message=""):
#TODO: Prompt user # TODO: Prompt user
return None return None
def showMessage(self, message, noTimestamp=False): def showMessage(self, message, noTimestamp=False):
@ -55,60 +99,100 @@ class MainWindow(QtGui.QMainWindow):
def showUserList(self, currentUser, rooms): def showUserList(self, currentUser, rooms):
self._usertreebuffer = QtGui.QStandardItemModel() self._usertreebuffer = QtGui.QStandardItemModel()
self._usertreebuffer.setColumnCount(2) self._usertreebuffer.setHorizontalHeaderLabels(
self._usertreebuffer.setHorizontalHeaderLabels((getMessage("roomuser-heading-label"),getMessage("fileplayed-heading-label"))) (getMessage("roomuser-heading-label"), getMessage("size-heading-label"), getMessage("duration-heading-label"), getMessage("filename-heading-label") ))
usertreeRoot = self._usertreebuffer.invisibleRootItem() usertreeRoot = self._usertreebuffer.invisibleRootItem()
for room in rooms: for room in rooms:
roomitem = QtGui.QStandardItem(room) roomitem = QtGui.QStandardItem(room)
font = QtGui.QFont()
font.setItalic(True)
if room == currentUser.room: if room == currentUser.room:
font = QtGui.QFont()
font.setWeight(QtGui.QFont.Bold) font.setWeight(QtGui.QFont.Bold)
roomitem.setFont(font) roomitem.setFont(font)
blankitem = QtGui.QStandardItem("") roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable)
roomitem.setFlags(roomitem.flags() & ~Qt.ItemIsEditable) usertreeRoot.appendRow(roomitem)
blankitem.setFlags(blankitem.flags() & ~Qt.ItemIsEditable) isControlledRoom = RoomPasswordProvider.isControlledRoom(room)
usertreeRoot.appendRow((roomitem, blankitem))
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]: for user in rooms[room]:
useritem = QtGui.QStandardItem(user.username) useritem = QtGui.QStandardItem(user.username)
fileitem = QtGui.QStandardItem("") isController = user.isController()
useritem.setData(isController, Qt.UserRole + constants.USERITEM_CONTROLLER_ROLE)
if user.file: 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: if currentUser.file:
sameName = sameFilename(user.file['name'], currentUser.file['name']) sameName = sameFilename(user.file['name'], currentUser.file['name'])
sameSize = sameFilesize(user.file['size'], currentUser.file['size']) sameSize = sameFilesize(user.file['size'], currentUser.file['size'])
sameDuration = sameFileduration(user.file['duration'], currentUser.file['duration']) sameDuration = sameFileduration(user.file['duration'], currentUser.file['duration'])
sameRoom = room == currentUser.room sameRoom = room == currentUser.room
differentName = not sameName underlinefont = QtGui.QFont()
differentSize = not sameSize underlinefont.setUnderline(True)
differentDuration = not sameDuration if sameRoom:
if sameName or sameRoom: if not sameName:
if differentSize and sameDuration: filenameitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentsize-note"))) filenameitem.setFont(underlinefont)
elif differentSize and differentDuration: if not sameSize:
fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentsizeandduration-note"))) if currentUser.file is not None and formatSize(user.file['size']) == formatSize(currentUser.file['size']):
elif differentDuration: filesizeitem = QtGui.QStandardItem(formatSize(user.file['size'],precise=True))
fileitem = QtGui.QStandardItem(u"{} ({}) ({})".format(user.file['name'], formatTime(user.file['duration']), getMessage("differentduration-note"))) filesizeitem.setFont(underlinefont)
if sameRoom and (differentName or differentSize or differentDuration): filesizeitem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_DIFFERENTITEM_COLOR)))
fileitem.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: else:
fileitem = QtGui.QStandardItem(getMessage("nofile-note")) filenameitem = QtGui.QStandardItem(getMessage("nofile-note"))
filedurationitem = QtGui.QStandardItem("")
filesizeitem = QtGui.QStandardItem("")
if room == currentUser.room: 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: if currentUser.username == user.username:
font = QtGui.QFont()
font.setWeight(QtGui.QFont.Bold) font.setWeight(QtGui.QFont.Bold)
useritem.setFont(font) if isControlledRoom and not isController:
useritem.setFlags(useritem.flags() & ~Qt.ItemIsEditable) useritem.setForeground(QtGui.QBrush(QtGui.QColor(constants.STYLE_NOTCONTROLLER_COLOR)))
fileitem.setFlags(fileitem.flags() & ~Qt.ItemIsEditable) useritem.setFont(font)
roomitem.appendRow((useritem, fileitem)) 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.listTreeModel = self._usertreebuffer
self.listTreeView.setModel(self.listTreeModel) self.listTreeView.setModel(self.listTreeModel)
self.listTreeView.setItemDelegate(UserlistItemDelegate())
self.listTreeView.setItemsExpandable(False) self.listTreeView.setItemsExpandable(False)
self.listTreeView.setRootIsDecorated(False)
self.listTreeView.expandAll() self.listTreeView.expandAll()
self.listTreeView.resizeColumnToContents(0) self.updateListGeometry()
self.listTreeView.resizeColumnToContents(1)
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): def roomClicked(self, item):
while item.parent().row() != -1: while item.parent().row() != -1:
@ -118,23 +202,26 @@ class MainWindow(QtGui.QMainWindow):
def userListChange(self): def userListChange(self):
self._syncplayClient.showUserList() self._syncplayClient.showUserList()
def updateRoomName(self, room=""):
self.roomInput.setText(room)
def showDebugMessage(self, message): def showDebugMessage(self, message):
print(message) print(message)
def showErrorMessage(self, message, criticalerror = False): def showErrorMessage(self, message, criticalerror=False):
message = unicode(message) message = unicode(message)
if criticalerror: 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("&", "&amp;").replace('"', "&quot;").replace("<", "&lt;").replace(">", "&gt;")
message = message.replace("\n", "<br />") message = message.replace("\n", "<br />")
message = "<span style=\"{}\">".format(constants.STYLE_ERRORNOTIFICATION) + message + "</span>" message = "<span style=\"{}\">".format(constants.STYLE_ERRORNOTIFICATION) + message + "</span>"
self.newMessage(time.strftime(constants.UI_TIME_FORMAT, time.localtime()) + message + "<br />") 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: if room == None:
room = self.roomInput.text() room = self.roomInput.text()
if room == "": if room == "":
if self._syncplayClient.userlist.currentUser.file: if self._syncplayClient.userlist.currentUser.file:
room = self._syncplayClient.userlist.currentUser.file["name"] room = self._syncplayClient.userlist.currentUser.file["name"]
else: else:
room = self._syncplayClient.defaultRoom room = self._syncplayClient.defaultRoom
@ -178,6 +265,69 @@ class MainWindow(QtGui.QMainWindow):
self.exitSyncplay() self.exitSyncplay()
self.saveSettings() 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): def loadMediaBrowseSettings(self):
settings = QSettings("Syncplay", "MediaBrowseDialog") settings = QSettings("Syncplay", "MediaBrowseDialog")
settings.beginGroup("MediaBrowseDialog") settings.beginGroup("MediaBrowseDialog")
@ -191,6 +341,10 @@ class MainWindow(QtGui.QMainWindow):
settings.endGroup() settings.endGroup()
def browseMediapath(self): def browseMediapath(self):
if self._syncplayClient._player.customOpenDialog == True:
self._syncplayClient._player.openCustomOpenDialog()
return
self.loadMediaBrowseSettings() self.loadMediaBrowseSettings()
options = QtGui.QFileDialog.Options() options = QtGui.QFileDialog.Options()
if os.path.isdir(self.mediadirectory): if os.path.isdir(self.mediadirectory):
@ -202,15 +356,29 @@ class MainWindow(QtGui.QMainWindow):
else: else:
defaultdirectory = "" defaultdirectory = ""
browserfilter = "All files (*)" browserfilter = "All files (*)"
fileName, filtr = QtGui.QFileDialog.getOpenFileName(self,getMessage("browseformedia-label"),defaultdirectory, fileName, filtr = QtGui.QFileDialog.getOpenFileName(self, getMessage("browseformedia-label"), defaultdirectory,
browserfilter, "", options) browserfilter, "", options)
if fileName: if fileName:
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
fileName = fileName.replace("/","\\") fileName = fileName.replace("/", "\\")
self.mediadirectory = os.path.dirname(fileName) self.mediadirectory = os.path.dirname(fileName)
self.saveMediaBrowseSettings() self.saveMediaBrowseSettings()
self._syncplayClient._player.openFile(fileName) 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): def _extractSign(self, m):
if m: if m:
if m == "-": if m == "-":
@ -221,9 +389,9 @@ class MainWindow(QtGui.QMainWindow):
return None return None
def setOffset(self): def setOffset(self):
newoffset, ok = QtGui.QInputDialog.getText(self,getMessage("setoffset-msgbox-label"), newoffset, ok = QtGui.QInputDialog.getText(self, getMessage("setoffset-msgbox-label"),
getMessage("offsetinfo-msgbox-label"), QtGui.QLineEdit.Normal, getMessage("offsetinfo-msgbox-label"), QtGui.QLineEdit.Normal,
"") "")
if ok and newoffset != '': if ok and newoffset != '':
o = re.match(constants.UI_OFFSET_REGEX, "o " + newoffset) o = re.match(constants.UI_OFFSET_REGEX, "o " + newoffset)
if o: if o:
@ -232,9 +400,9 @@ class MainWindow(QtGui.QMainWindow):
if t is None: if t is None:
return return
if o.group('sign') == "/": if o.group('sign') == "/":
t = self._syncplayClient.getPlayerPosition() - t t = self._syncplayClient.getPlayerPosition() - t
elif sign: elif sign:
t = self._syncplayClient.getUserOffset() + sign * t t = self._syncplayClient.getUserOffset() + sign * t
self._syncplayClient.setUserOffset(t) self._syncplayClient.setUserOffset(t)
else: else:
self.showErrorMessage("Invalid offset value") self.showErrorMessage("Invalid offset value")
@ -251,7 +419,7 @@ class MainWindow(QtGui.QMainWindow):
self.close() self.close()
def addTopLayout(self, window): def addTopLayout(self, window):
window.topSplit = QtGui.QSplitter(Qt.Horizontal) window.topSplit = self.topSplitter(Qt.Horizontal, self)
window.outputLayout = QtGui.QVBoxLayout() window.outputLayout = QtGui.QVBoxLayout()
window.outputbox = QtGui.QTextEdit() window.outputbox = QtGui.QTextEdit()
@ -260,7 +428,7 @@ class MainWindow(QtGui.QMainWindow):
window.outputFrame = QtGui.QFrame() window.outputFrame = QtGui.QFrame()
window.outputFrame.setLineWidth(0) window.outputFrame.setLineWidth(0)
window.outputFrame.setMidLineWidth(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.outputlabel)
window.outputLayout.addWidget(window.outputbox) window.outputLayout.addWidget(window.outputbox)
window.outputFrame.setLayout(window.outputLayout) window.outputFrame.setLayout(window.outputLayout)
@ -269,12 +437,13 @@ class MainWindow(QtGui.QMainWindow):
window.listTreeModel = QtGui.QStandardItemModel() window.listTreeModel = QtGui.QStandardItemModel()
window.listTreeView = QtGui.QTreeView() window.listTreeView = QtGui.QTreeView()
window.listTreeView.setModel(window.listTreeModel) window.listTreeView.setModel(window.listTreeModel)
window.listTreeView.setIndentation(21)
window.listTreeView.doubleClicked.connect(self.roomClicked) window.listTreeView.doubleClicked.connect(self.roomClicked)
window.listlabel = QtGui.QLabel(getMessage("userlist-heading-label")) window.listlabel = QtGui.QLabel(getMessage("userlist-heading-label"))
window.listFrame = QtGui.QFrame() window.listFrame = QtGui.QFrame()
window.listFrame.setLineWidth(0) window.listFrame.setLineWidth(0)
window.listFrame.setMidLineWidth(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.listlabel)
window.listLayout.addWidget(window.listTreeView) window.listLayout.addWidget(window.listTreeView)
window.contactLabel = QtGui.QLabel() window.contactLabel = QtGui.QLabel()
@ -294,7 +463,7 @@ class MainWindow(QtGui.QMainWindow):
window.topSplit.setStretchFactor(0,4) window.topSplit.setStretchFactor(0,4)
window.topSplit.setStretchFactor(1,5) window.topSplit.setStretchFactor(1,5)
window.mainLayout.addWidget(window.topSplit) 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): def addBottomLayout(self, window):
window.bottomLayout = QtGui.QHBoxLayout() window.bottomLayout = QtGui.QHBoxLayout()
@ -303,9 +472,9 @@ class MainWindow(QtGui.QMainWindow):
window.addSeekBox(MainWindow) window.addSeekBox(MainWindow)
window.addMiscBox(MainWindow) window.addMiscBox(MainWindow)
window.bottomLayout.addWidget(window.roomGroup, Qt.AlignLeft) window.bottomLayout.addWidget(window.roomGroup)
window.bottomLayout.addWidget(window.seekGroup, Qt.AlignLeft) window.bottomLayout.addWidget(window.seekGroup)
window.bottomLayout.addWidget(window.miscGroup, Qt.AlignLeft) window.bottomLayout.addWidget(window.miscGroup)
window.mainLayout.addLayout(window.bottomLayout, Qt.AlignLeft) window.mainLayout.addLayout(window.bottomLayout, Qt.AlignLeft)
@ -314,7 +483,8 @@ class MainWindow(QtGui.QMainWindow):
window.roomInput = QtGui.QLineEdit() window.roomInput = QtGui.QLineEdit()
window.roomInput.returnPressed.connect(self.joinRoom) 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.roomButton.pressed.connect(self.joinRoom)
window.roomLayout = QtGui.QHBoxLayout() window.roomLayout = QtGui.QHBoxLayout()
window.roomInput.setFixedWidth(150) window.roomInput.setFixedWidth(150)
@ -325,14 +495,15 @@ class MainWindow(QtGui.QMainWindow):
window.roomLayout.addWidget(window.roomButton) window.roomLayout.addWidget(window.roomButton)
window.roomGroup.setLayout(window.roomLayout) window.roomGroup.setLayout(window.roomLayout)
window.roomGroup.setFixedSize(window.roomGroup.sizeHint()) window.roomGroup.setFixedHeight(window.roomGroup.sizeHint().height())
def addSeekBox(self, window): def addSeekBox(self, window):
window.seekGroup = QtGui.QGroupBox(getMessage("seek-heading-label")) window.seekGroup = QtGui.QGroupBox(getMessage("seek-heading-label"))
window.seekInput = QtGui.QLineEdit() window.seekInput = QtGui.QLineEdit()
window.seekInput.returnPressed.connect(self.seekPosition) 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) window.seekButton.pressed.connect(self.seekPosition)
self.seekButton.setToolTip(getMessage("seektime-tooltip")) self.seekButton.setToolTip(getMessage("seektime-tooltip"))
@ -345,55 +516,69 @@ class MainWindow(QtGui.QMainWindow):
window.seekLayout.addWidget(window.seekButton) window.seekLayout.addWidget(window.seekButton)
window.seekGroup.setLayout(window.seekLayout) window.seekGroup.setLayout(window.seekLayout)
window.seekGroup.setFixedSize(window.seekGroup.sizeHint()) window.seekGroup.setFixedHeight(window.seekGroup.sizeHint().height())
def addMiscBox(self, window): def addMiscBox(self, window):
window.miscGroup = QtGui.QGroupBox(getMessage("othercommands-heading-label")) 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) window.unseekButton.pressed.connect(self.undoSeek)
self.unseekButton.setToolTip(getMessage("undoseek-tooltip")) self.unseekButton.setToolTip(getMessage("undoseek-tooltip"))
window.miscLayout = QtGui.QHBoxLayout() window.miscLayout = QtGui.QHBoxLayout()
window.miscLayout.addWidget(window.unseekButton) window.miscLayout.addWidget(window.unseekButton)
if constants.MERGE_PLAYPAUSE_BUTTONS == True: 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.playpauseButton.pressed.connect(self.togglePause)
window.miscLayout.addWidget(window.playpauseButton) window.miscLayout.addWidget(window.playpauseButton)
self.playpauseButton.setToolTip(getMessage("togglepause-tooltip")) self.playpauseButton.setToolTip(getMessage("togglepause-tooltip"))
else: 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.pressed.connect(self.play)
window.playButton.setMaximumWidth(60)
window.miscLayout.addWidget(window.playButton) 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.pressed.connect(self.pause)
window.pauseButton.setMaximumWidth(60)
window.miscLayout.addWidget(window.pauseButton) window.miscLayout.addWidget(window.pauseButton)
self.playButton.setToolTip(getMessage("play-tooltip")) self.playButton.setToolTip(getMessage("play-tooltip"))
self.pauseButton.setToolTip(getMessage("pause-tooltip")) self.pauseButton.setToolTip(getMessage("pause-tooltip"))
window.miscGroup.setLayout(window.miscLayout) window.miscGroup.setLayout(window.miscLayout)
window.miscGroup.setFixedSize(window.miscGroup.sizeHint()) window.miscGroup.setFixedHeight(window.miscGroup.sizeHint().height())
def addMenubar(self, window): def addMenubar(self, window):
window.menuBar = QtGui.QMenuBar() window.menuBar = QtGui.QMenuBar()
window.fileMenu = QtGui.QMenu(getMessage("file-menu-label"), self) 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.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.exitAction.triggered.connect(self.exitSyncplay)
window.menuBar.addMenu(window.fileMenu) window.menuBar.addMenu(window.fileMenu)
window.advancedMenu = QtGui.QMenu(getMessage("advanced-menu-label"), self) 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.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.menuBar.addMenu(window.advancedMenu)
window.helpMenu = QtGui.QMenu(getMessage("help-menu-label"), self) 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.userguideAction.triggered.connect(self.openUserGuide)
window.menuBar.addMenu(window.helpMenu) window.menuBar.addMenu(window.helpMenu)
@ -403,7 +588,7 @@ class MainWindow(QtGui.QMainWindow):
window.mainFrame = QtGui.QFrame() window.mainFrame = QtGui.QFrame()
window.mainFrame.setLineWidth(0) window.mainFrame.setLineWidth(0)
window.mainFrame.setMidLineWidth(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.mainFrame.setLayout(window.mainLayout)
window.setCentralWidget(window.mainFrame) window.setCentralWidget(window.mainFrame)
@ -430,20 +615,17 @@ class MainWindow(QtGui.QMainWindow):
def dropEvent(self, event): def dropEvent(self, event):
rewindFile = False rewindFile = False
if QtGui.QDropEvent.proposedAction(event) == Qt.MoveAction: 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 rewindFile = True
data = event.mimeData() data = event.mimeData()
urls = data.urls() urls = data.urls()
if urls and urls[0].scheme() == 'file': if urls and urls[0].scheme() == 'file':
if sys.platform.startswith('win'): dropfilepath = os.path.abspath(unicode(event.mimeData().urls()[0].toLocalFile()))
dropfilepath = unicode(urls[0].path().replace("/", "\\"))[1:] # Removes starting slash
else:
dropfilepath = unicode(urls[0].path())
if rewindFile == False: if rewindFile == False:
self._syncplayClient._player.openFile(dropfilepath) self._syncplayClient._player.openFile(dropfilepath)
else: else:
self._syncplayClient.setPosition(0) self._syncplayClient.setPosition(0)
self._syncplayClient._player.openFile(dropfilepath) self._syncplayClient._player.openFile(dropfilepath, resetPosition=True)
self._syncplayClient.setPosition(0) self._syncplayClient.setPosition(0)
def saveSettings(self): def saveSettings(self):
@ -467,14 +649,16 @@ class MainWindow(QtGui.QMainWindow):
self.resourcespath = utils.findWorkingDir() + "\\resources\\" self.resourcespath = utils.findWorkingDir() + "\\resources\\"
else: else:
self.resourcespath = utils.findWorkingDir() + "/resources/" self.resourcespath = utils.findWorkingDir() + "/resources/"
self.setWindowFlags(self.windowFlags() & Qt.AA_DontUseNativeMenuBar)
self.setWindowTitle("Syncplay v" + version) self.setWindowTitle("Syncplay v" + version)
self.mainLayout = QtGui.QVBoxLayout() self.mainLayout = QtGui.QVBoxLayout()
self.addTopLayout(self) self.addTopLayout(self)
self.addBottomLayout(self) self.addBottomLayout(self)
self.addMenubar(self) self.addMenubar(self)
self.addMainFrame(self) self.addMainFrame(self)
self.setupSizes()
self.loadSettings() self.loadSettings()
self.setWindowIcon(QtGui.QIcon(self.resourcespath + "syncplay.png")) 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.show()
self.setAcceptDrops(True) self.setAcceptDrops(True)

View File

@ -7,6 +7,8 @@ import sys
import os import os
import itertools import itertools
import hashlib import hashlib
import random
import string
def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None): def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None):
"""Retry calling the decorated function using an exponential backoff. """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) formattedTime = "{0:} (Title {1:.0f})".format(formattedTime, title)
return formattedTime 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(): def findWorkingDir():
frozen = getattr(sys, 'frozen', '') frozen = getattr(sys, 'frozen', '')
if not frozen: if not frozen:
@ -141,7 +156,19 @@ def blackholeStdoutForFrozenWindow():
# Relate to file hashing / difference checking: # Relate to file hashing / difference checking:
def stripfilename(filename): 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): def hashFilename(filename):
return hashlib.sha256(stripfilename(filename).encode('utf-8')).hexdigest()[:12] return hashlib.sha256(stripfilename(filename).encode('utf-8')).hexdigest()[:12]
@ -182,3 +209,69 @@ def sameFileduration (duration1, duration2):
return True return True
else: else:
return False 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() argsGetter = ConfigurationGetter()
args = argsGetter.getConfiguration() 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() reactor.run()