Merge pull request #93 from Syncplay/sharedplaylists

Shared playlists
This commit is contained in:
Etoh 2016-07-02 23:54:34 +01:00 committed by GitHub
commit e4b033ad99
26 changed files with 2679 additions and 1290 deletions

View File

@ -127,13 +127,13 @@ NSIS_SCRIPT_TEMPLATE = r"""
Var Dialog
Var Icon_Syncplay
Var Icon_Syncplay_Handle
Var CheckBox_Associate
;Var CheckBox_Associate
Var CheckBox_VLC
Var CheckBox_AutomaticUpdates
Var CheckBox_StartMenuShortcut
Var CheckBox_DesktopShortcut
Var CheckBox_QuickLaunchShortcut
Var CheckBox_Associate_State
;Var CheckBox_Associate_State
Var CheckBox_VLC_State
Var CheckBox_AutomaticUpdates_State
Var CheckBox_StartMenuShortcut_State
@ -165,13 +165,13 @@ NSIS_SCRIPT_TEMPLATE = r"""
Var Drive
Var VLC_Directory
!macro APP_ASSOCIATE EXT FileCLASS DESCRIPTION COMMANDTEXT COMMAND
WriteRegStr HKCR ".$${EXT}" "" "$${FileCLASS}"
WriteRegStr HKCR "$${FileCLASS}" "" `$${DESCRIPTION}`
WriteRegStr HKCR "$${FileCLASS}\shell" "" "open"
WriteRegStr HKCR "$${FileCLASS}\shell\open" "" `$${COMMANDTEXT}`
WriteRegStr HKCR "$${FileCLASS}\shell\open\command" "" `$${COMMAND}`
!macroend
;!macro APP_ASSOCIATE EXT FileCLASS DESCRIPTION COMMANDTEXT COMMAND
; WriteRegStr HKCR ".$${EXT}" "" "$${FileCLASS}"
; WriteRegStr HKCR "$${FileCLASS}" "" `$${DESCRIPTION}`
; WriteRegStr HKCR "$${FileCLASS}\shell" "" "open"
; WriteRegStr HKCR "$${FileCLASS}\shell\open" "" `$${COMMANDTEXT}`
; WriteRegStr HKCR "$${FileCLASS}\shell\open\command" "" `$${COMMAND}`
;!macroend
!macro APP_UNASSOCIATE EXT FileCLASS
; Backup the previously associated File class
@ -180,10 +180,10 @@ NSIS_SCRIPT_TEMPLATE = r"""
DeleteRegKey HKCR `$${FileCLASS}`
!macroend
!macro ASSOCIATE EXT
!insertmacro APP_ASSOCIATE "$${EXT}" "Syncplay.$${EXT}" "$$INSTDIR\Syncplay.exe,%1%" \
"Open with Syncplay" "$$INSTDIR\Syncplay.exe $$\"%1$$\""
!macroend
;!macro ASSOCIATE EXT
; !insertmacro APP_ASSOCIATE "$${EXT}" "Syncplay.$${EXT}" "$$INSTDIR\Syncplay.exe,%1%" \
; "Open with Syncplay" "$$INSTDIR\Syncplay.exe $$\"%1$$\""
;!macroend
!macro UNASSOCIATE EXT
!insertmacro APP_UNASSOCIATE "$${EXT}" "Syncplay.$${EXT}"
@ -197,7 +197,7 @@ NSIS_SCRIPT_TEMPLATE = r"""
MessageBox MB_OK|MB_ICONEXCLAMATION "The installer is already running."
Abort
StrCpy $$CheckBox_Associate_State $${BST_CHECKED}
;StrCpy $$CheckBox_Associate_State $${BST_CHECKED}
StrCpy $$CheckBox_StartMenuShortcut_State $${BST_CHECKED}
Call GetVLCDir
Call UpdateVLCCheckbox
@ -257,8 +257,8 @@ NSIS_SCRIPT_TEMPLATE = r"""
$${NSD_CreateLabel} 321u 122u 132 8u "$$(^SpaceAvailable)$$AvailibleSpaceGiB.$$AvailibleSpaceGB"
Pop $$Label_Space
$${NSD_CreateCheckBox} 8u 59u 187u 10u "$$(^Associate)"
Pop $$CheckBox_Associate
;$${NSD_CreateCheckBox} 8u 59u 187u 10u "$$(^Associate)"
;Pop $$CheckBox_Associate
$${NSD_CreateBrowseButton} 185u 70u 70u 14u "$$(^BrowseVLCBtn)"
Pop $$Button_Browse_VLC
@ -283,9 +283,9 @@ NSIS_SCRIPT_TEMPLATE = r"""
$${NSD_CreateCheckbox} 158u 111u 130u 10u "$$(^QuickLaunchBar)"
Pop $$CheckBox_QuickLaunchShortcut
$${If} $$CheckBox_Associate_State == $${BST_CHECKED}
$${NSD_Check} $$CheckBox_Associate
$${EndIf}
;$${If} $$CheckBox_Associate_State == $${BST_CHECKED}
; $${NSD_Check} $$CheckBox_Associate
;$${EndIf}
$${If} $$CheckBox_VLC_State == $${BST_CHECKED}
$${NSD_Check} $$CheckBox_VLC
@ -317,7 +317,7 @@ NSIS_SCRIPT_TEMPLATE = r"""
Function DirectoryCustomLeave
$${NSD_GetText} $$Text_Directory $$INSTDIR
$${NSD_GetState} $$CheckBox_Associate $$CheckBox_Associate_State
;$${NSD_GetState} $$CheckBox_Associate $$CheckBox_Associate_State
$${NSD_GetState} $$CheckBox_VLC $$CheckBox_VLC_State
$${NSD_GetState} $$CheckBox_AutomaticUpdates $$CheckBox_AutomaticUpdates_State
$${NSD_GetState} $$CheckBox_StartMenuShortcut $$CheckBox_StartMenuShortcut_State
@ -395,10 +395,10 @@ NSIS_SCRIPT_TEMPLATE = r"""
FunctionEnd
Function InstallOptions
$${If} $$CheckBox_Associate_State == $${BST_CHECKED}
Call Associate
DetailPrint "Associated Syncplay with multimedia files"
$${EndIf}
;$${If} $$CheckBox_Associate_State == $${BST_CHECKED}
; Call Associate
; DetailPrint "Associated Syncplay with multimedia files"
;$${EndIf}
$${If} $$CheckBox_StartMenuShortcut_State == $${BST_CHECKED}
CreateDirectory $$SMPROGRAMS\Syncplay
@ -425,32 +425,32 @@ NSIS_SCRIPT_TEMPLATE = r"""
FunctionEnd
;Associates extensions with Syncplay
Function Associate
!insertmacro ASSOCIATE avi
!insertmacro ASSOCIATE mpg
!insertmacro ASSOCIATE mpeg
!insertmacro ASSOCIATE mpe
!insertmacro ASSOCIATE m1v
!insertmacro ASSOCIATE m2v
!insertmacro ASSOCIATE mpv2
!insertmacro ASSOCIATE mp2v
!insertmacro ASSOCIATE mkv
!insertmacro ASSOCIATE mp4
!insertmacro ASSOCIATE m4v
!insertmacro ASSOCIATE mp4v
!insertmacro ASSOCIATE 3gp
!insertmacro ASSOCIATE 3gpp
!insertmacro ASSOCIATE 3g2
!insertmacro ASSOCIATE 3pg2
!insertmacro ASSOCIATE flv
!insertmacro ASSOCIATE f4v
!insertmacro ASSOCIATE rm
!insertmacro ASSOCIATE wmv
!insertmacro ASSOCIATE swf
!insertmacro ASSOCIATE rmvb
!insertmacro ASSOCIATE divx
!insertmacro ASSOCIATE amv
FunctionEnd
;Function Associate
; !insertmacro ASSOCIATE avi
; !insertmacro ASSOCIATE mpg
; !insertmacro ASSOCIATE mpeg
; !insertmacro ASSOCIATE mpe
; !insertmacro ASSOCIATE m1v
; !insertmacro ASSOCIATE m2v
; !insertmacro ASSOCIATE mpv2
; !insertmacro ASSOCIATE mp2v
; !insertmacro ASSOCIATE mkv
; !insertmacro ASSOCIATE mp4
; !insertmacro ASSOCIATE m4v
; !insertmacro ASSOCIATE mp4v
; !insertmacro ASSOCIATE 3gp
; !insertmacro ASSOCIATE 3gpp
; !insertmacro ASSOCIATE 3g2
; !insertmacro ASSOCIATE 3pg2
; !insertmacro ASSOCIATE flv
; !insertmacro ASSOCIATE f4v
; !insertmacro ASSOCIATE rm
; !insertmacro ASSOCIATE wmv
; !insertmacro ASSOCIATE swf
; !insertmacro ASSOCIATE rmvb
; !insertmacro ASSOCIATE divx
; !insertmacro ASSOCIATE amv
;FunctionEnd
Function WriteRegistry
Call GetSize
@ -653,9 +653,11 @@ guiIcons = ['resources/accept.png', 'resources/arrow_undo.png', 'resources/clock
'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/empty_checkbox.png', 'resources/tick_checkbox.png',
'resources/world_explore.png', 'resources/application_get.png', 'resources/cog.png',
'resources/film_go.png', 'resources/world_go.png',
'resources/arrow_refresh.png', 'resources/spinner.mng'
'resources/world_explore.png', 'resources/application_get.png', 'resources/cog.png', 'resources/arrow_switch.png',
'resources/film_go.png', 'resources/world_go.png', 'resources/arrow_refresh.png', 'resources/bullet_right_grey.png',
'resources/film_folder_edit.png',
'resources/shield_edit.png',
'resources/world_add.png', 'resources/film_add.png', 'resources/delete.png', 'resources/spinner.mng'
]
resources = ["resources/icon.ico", "resources/syncplay.png"]
resources.extend(guiIcons)

BIN
resources/arrow_switch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

BIN
resources/delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 715 B

BIN
resources/film_add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 B

View File

@ -5,7 +5,7 @@
Principal author: Etoh
Other contributors: DerGenaue, jb
Project: http://syncplay.pl/
Version: 0.2.8
Version: 0.2.9
Note:
* This interface module is intended to be used in conjunction with Syncplay.
@ -84,7 +84,7 @@ You may also need to re-copy the syncplay.lua file when you update VLC.
--]==========================================================================]
local connectorversion = "0.2.8"
local connectorversion = "0.2.9"
local vlcversion = vlc.misc.version()
local durationdelay = 500000 -- Pause for get_duration command etc for increased reliability (uses microseconds)
local loopsleepduration = 2500 -- Pause for every event loop (uses microseconds)
@ -463,7 +463,9 @@ function load_file (filepath)
-- [Used by load-file command]
local uri = vlc.strings.make_uri(filepath)
vlc.playlist.add({{path=uri}})
vlc.playlist.clear()
vlc.playlist.enqueue({{path=uri}})
vlc.playlist.next()
return "load-file-attempted\n"
end

BIN
resources/shield_edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 B

BIN
resources/world_add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

View File

@ -1,4 +1,4 @@
version = '1.3.4'
milestone = 'Chami'
release_number = '21'
version = '1.4.0'
milestone = 'Yoitsu'
release_number = '27'
projectURL = 'http://syncplay.pl/'

View File

@ -4,8 +4,10 @@ import time
import re
import sys
import ast
import random
import threading
from twisted.internet.protocol import ClientFactory
from twisted.internet import reactor, task
from twisted.internet import reactor, task, defer, threads
from functools import wraps
from copy import deepcopy
from syncplay.protocols import SyncClientProtocol
@ -69,6 +71,7 @@ class SyncplayClient(object):
self.lastControlPasswordAttempt = None
self.serverVersion = "0.0.0"
self.lastRewindTime = None
self.lastLeftTime = 0
self.lastPausedOnLeaveTime = None
self.lastLeftUser = u""
@ -113,7 +116,12 @@ class SyncplayClient(object):
self.autoplayTimer = task.LoopingCall(self.autoplayCountdown)
self.autoplayTimeLeft = constants.AUTOPLAY_DELAY
self.__playerReady = defer.Deferred()
self._warnings = self._WarningManager(self._player, self.userlist, self.ui, self)
self.fileSwitch = FileSwitchManager(self)
self.playlist = SyncplayPlaylist(self)
if constants.LIST_RELATIVE_CONFIGS and self._config.has_key('loadedRelativePaths') and self._config['loadedRelativePaths']:
paths = "; ".join(self._config['loadedRelativePaths'])
self.ui.showMessage(getMessage("relative-config-notification").format(paths), noPlayer=True, noTimestamp=True)
@ -121,7 +129,7 @@ class SyncplayClient(object):
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))
self.ui.showDebugMessage(u"MISSING/UNUSED STRINGS DETECTED:\n{}".format(missingStrings))
def initProtocol(self, protocol):
self._protocol = protocol
@ -136,6 +144,13 @@ class SyncplayClient(object):
if not self._player.secondaryOSDSupported:
constants.OSD_WARNING_MESSAGE_DURATION = constants.NO_SECONDARY_OSD_WARNING_DURATION
self.scheduleAskPlayer()
self.__playerReady.callback(player)
def addPlayerReadyCallback(self, lambdaToCall):
self.__playerReady.addCallback(lambdaToCall)
def playerIsNotReady(self):
return self._player is None
def scheduleAskPlayer(self, when=constants.PLAYER_ASK_DELAY):
self._askPlayerTimer = task.LoopingCall(self.askPlayer)
@ -163,12 +178,42 @@ class SyncplayClient(object):
seeked = _playerDiff > constants.SEEK_THRESHOLD and _globalDiff > constants.SEEK_THRESHOLD
return pauseChange, seeked
def rewindFile(self):
self.setPosition(-1)
self.establishRewindDoubleCheck()
def establishRewindDoubleCheck(self):
reactor.callLater(0.5, self.doubleCheckRewindFile,)
reactor.callLater(1, self.doubleCheckRewindFile,)
reactor.callLater(1.5, self.doubleCheckRewindFile,)
def doubleCheckRewindFile(self):
if self.getStoredPlayerPosition() > 5:
self.setPosition(-1)
self.ui.showDebugMessage("Rewinded after double-check")
def updatePlayerStatus(self, paused, position):
position -= self.getUserOffset()
pauseChange, seeked = self._determinePlayerStateChange(paused, position)
self._playerPosition = position
self._playerPaused = paused
if pauseChange and utils.meetsMinVersion(self.serverVersion, constants.USER_READY_MIN_VERSION):
currentLength = self.userlist.currentUser.file["duration"] if self.userlist.currentUser.file else 0
if pauseChange and paused and currentLength > constants.PLAYLIST_LOAD_NEXT_FILE_MINIMUM_LENGTH\
and abs(position - currentLength ) < constants.PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD:
self.playlist.advancePlaylistCheck()
elif pauseChange and utils.meetsMinVersion(self.serverVersion, constants.USER_READY_MIN_VERSION):
if currentLength == 0 or currentLength == -1 or\
not (not self.playlist.notJustChangedPlaylist() and abs(position - currentLength ) < constants.PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD):
pauseChange = self._toggleReady(pauseChange, paused)
if self._lastGlobalUpdate:
self._lastPlayerUpdate = time.time()
if (pauseChange or seeked) and self._protocol:
if seeked:
self.playerPositionBeforeLastSeek = self.getGlobalPosition()
self._protocol.sendState(self.getPlayerPosition(), self.getPlayerPaused(), seeked, None, True)
def _toggleReady(self, pauseChange, paused):
if not self.userlist.currentUser.canControl():
self._player.setPaused(self._globalPaused)
self.toggleReady(manuallyInitiated=True)
@ -191,12 +236,7 @@ class SyncplayClient(object):
self.lastPausedOnLeaveTime = None
else:
self.changeReadyState(not self.getPlayerPaused(), manuallyInitiated=False)
if self._lastGlobalUpdate:
self._lastPlayerUpdate = time.time()
if (pauseChange or seeked) and self._protocol:
if seeked:
self.playerPositionBeforeLastSeek = self.getGlobalPosition()
self._protocol.sendState(self.getPlayerPosition(), self.getPlayerPaused(), seeked, None, True)
return pauseChange
def getLocalState(self):
paused = self.getPlayerPaused()
@ -307,7 +347,8 @@ class SyncplayClient(object):
self.behindFirstDetected = time.time()
else:
durationBehind = time.time() - self.behindFirstDetected
if (durationBehind > (self._config['fastforwardThreshold']-constants.FASTFORWARD_BEHIND_THRESHOLD)) and (diff < (self._config['fastforwardThreshold'] * -1)):
if (durationBehind > (self._config['fastforwardThreshold']-constants.FASTFORWARD_BEHIND_THRESHOLD))\
and (diff < (self._config['fastforwardThreshold'] * -1)):
madeChangeOnPlayer = self._fastforwardPlayerDueToTimeDifference(position, setBy)
self.behindFirstDetected = time.time() + constants.FASTFORWARD_RESET_THRESHOLD
else:
@ -368,6 +409,9 @@ class SyncplayClient(object):
position += diff
return position
def getStoredPlayerPosition(self):
return self._playerPosition if self._playerPosition is not None else None
def getPlayerPaused(self):
if not self._lastPlayerUpdate:
if self._lastGlobalUpdate:
@ -390,6 +434,7 @@ class SyncplayClient(object):
return self._globalPaused
def updateFile(self, filename, duration, path):
newPath = u""
if utils.isURL(path):
filename = path
@ -403,9 +448,54 @@ class SyncplayClient(object):
size = os.path.getsize(path)
except:
size = 0
if not utils.isURL(path) and os.path.exists(path):
self.fileSwitch.notifyUserIfFileNotInMediaDirectory(filename, path)
filename, size = self.__executePrivacySettings(filename, size)
self.userlist.currentUser.setFile(filename, duration, size, path)
self.sendFile()
self.playlist.changeToPlaylistIndexFromFilename(filename)
def setTrustedDomains(self, newTrustedDomains):
from syncplay.ui.ConfigurationGetter import ConfigurationGetter
ConfigurationGetter().setConfigOption("trustedDomains", newTrustedDomains)
self._config['trustedDomains'] = newTrustedDomains
self.fileSwitchFoundFiles()
def isURITrusted(self, URIToTest):
for trustedProtocol in constants.TRUSTABLE_WEB_PROTOCOLS:
if URIToTest.startswith(trustedProtocol):
if self._config['onlySwitchToTrustedDomains']:
if self._config['trustedDomains']:
for trustedDomain in self._config['trustedDomains']:
trustableURI = ''.join([trustedProtocol,trustedDomain,u"/"])
if URIToTest.startswith(trustableURI):
return True
return False
else:
return True
return False
def openFile(self, filePath, resetPosition=False):
self._player.openFile(filePath, resetPosition)
if resetPosition:
self.establishRewindDoubleCheck()
self.lastRewindTime = time.time()
def fileSwitchFoundFiles(self):
self.ui.fileSwitchFoundFiles()
self.playlist.loadCurrentPlaylistIndex()
def setPlaylistIndex(self, index):
self._protocol.setPlaylistIndex(index)
def changeToPlaylistIndex(self, *args, **kwargs):
self.playlist.changeToPlaylistIndex(*args, **kwargs)
def loopSingleFiles(self):
return self._config["loopSingleFiles"]
def isPlaylistLoopingEnabled(self):
return self._config["loopAtEndOfPlaylist"]
def __executePrivacySettings(self, filename, size):
if self._config['filenamePrivacyMode'] == PRIVACY_SENDHASHED_MODE:
@ -442,8 +532,6 @@ class SyncplayClient(object):
if username and username <> "":
self.userlist.currentUser.username = username
else:
import random
random.seed()
random_number = random.randrange(1000, 9999)
self.userlist.currentUser.username = "Anonymous" + str(random_number) # Not localised as this would give away locale
@ -469,6 +557,12 @@ class SyncplayClient(object):
if storedRoomPassword:
self.identifyAsController(storedRoomPassword)
def isConnectedAndInARoom(self):
return self._protocol and self._protocol.logged and self.userlist.currentUser.room
def sharedPlaylistIsEnabled(self):
return self._config['sharedPlaylistEnabled']
def connected(self):
readyState = self._config['readyAtStart'] if self.userlist.currentUser.isReady() is None else self.userlist.currentUser.isReady()
self._protocol.setReady(readyState, manuallyInitiated=False)
@ -493,6 +587,9 @@ class SyncplayClient(object):
def setPosition(self, position):
if self._lastPlayerUpdate:
self._lastPlayerUpdate = time.time()
if self.lastRewindTime is not None and abs(time.time() - self.lastRewindTime) < 1.0 and position > 5:
self.ui.showDebugMessage("Ignored seek to {} after rewind".format(position))
return
position += self.getUserOffset()
if self._player and self.userlist.currentUser.file:
if position < 0:
@ -550,6 +647,14 @@ class SyncplayClient(object):
return wrapper
return requireMinVersionDecorator
def changePlaylistEnabledState(self, newState):
oldState = self.sharedPlaylistIsEnabled()
from syncplay.ui.ConfigurationGetter import ConfigurationGetter
ConfigurationGetter().setConfigOption("sharedPlaylistEnabled", newState)
self._config["sharedPlaylistEnabled"] = newState
if oldState == False and newState == True:
self.playlist.loadCurrentPlaylistIndex()
def changeAutoplayState(self, newState):
self.autoPlay = newState
self.autoplayCheck()
@ -576,13 +681,16 @@ class SyncplayClient(object):
return True
elif unpauseAction == constants.UNPAUSE_IFOTHERSREADY_MODE and self.userlist.areAllOtherUsersInRoomReady():
return True
elif unpauseAction == constants.UNPAUSE_IFMINUSERSREADY_MODE and self.userlist.areAllOtherUsersInRoomReady() and self.autoPlayThreshold and self.userlist.usersInRoomCount() >= self.autoPlayThreshold:
elif unpauseAction == constants.UNPAUSE_IFMINUSERSREADY_MODE and self.userlist.areAllOtherUsersInRoomReady()\
and self.autoPlayThreshold and self.userlist.usersInRoomCount() >= self.autoPlayThreshold:
return True
else:
return False
def autoplayConditionsMet(self):
return self._playerPaused and self.autoPlay and self.userlist.currentUser.canControl() and self.userlist.isReadinessSupported() and self.userlist.areAllUsersInRoomReady() and self.autoPlayThreshold and self.userlist.usersInRoomCount() >= self.autoPlayThreshold
return self._playerPaused and self.autoPlay and self.userlist.currentUser.canControl() and self.userlist.isReadinessSupported()\
and self.userlist.areAllUsersInRoomReady(requireSameFilenames=self._config["autoplayRequireSameFilenames"])\
and self.autoPlayThreshold and self.userlist.usersInRoomCount() >= self.autoPlayThreshold
def autoplayTimerIsRunning(self):
return self.autoplayTimer.running
@ -601,7 +709,9 @@ class SyncplayClient(object):
if not self.autoplayConditionsMet():
self.stopAutoplayCountdown()
return
countdownMessage = u"{}{}{}".format(getMessage("all-users-ready").format(self.userlist.readyUserCount()),self._player.osdMessageSeparator, getMessage("autoplaying-notification").format(int(self.autoplayTimeLeft)))
allReadyMessage = getMessage("all-users-ready").format(self.userlist.readyUserCount())
autoplayingMessage = getMessage("autoplaying-notification").format(int(self.autoplayTimeLeft))
countdownMessage = u"{}{}{}".format(allReadyMessage,self._player.osdMessageSeparator, autoplayingMessage)
self.ui.showOSDMessage(countdownMessage, 1, secondaryOSD=True)
if self.autoplayTimeLeft <= 0:
self.setPaused(False)
@ -692,9 +802,11 @@ class SyncplayClient(object):
response = json.loads(response)
publicServers = None
if response["public-servers"]:
publicServers = response["public-servers"].replace("&#8221;","'").replace(":&#8217;","'").replace("&#8217;","'").replace("&#8242;","'").replace("\n","").replace("\r","")
publicServers = response["public-servers"].\
replace("&#8221;","'").replace(":&#8217;","'").replace("&#8217;","'").replace("&#8242;","'").replace("\n","").replace("\r","")
publicServers = ast.literal_eval(publicServers)
return response["version-status"], response["version-message"] if response.has_key("version-message") else None, response["version-url"] if response.has_key("version-url") else None, publicServers
return response["version-status"], response["version-message"] if response.has_key("version-message")\
else None, response["version-url"] if response.has_key("version-url") else None, publicServers
except:
return "failed", getMessage("update-check-failed-notification").format(syncplay.version), constants.SYNCPLAY_DOWNLOAD_URL, None
@ -779,9 +891,11 @@ class SyncplayClient(object):
fileDifferencesMessage = getMessage("room-file-differences").format(fileDifferencesForRoom)
if self._userlist.currentUser.canControl() and self._userlist.isReadinessSupported():
if self._userlist.areAllUsersInRoomReady():
osdMessage = u"{}{}{}".format(fileDifferencesMessage, self._client._player.osdMessageSeparator, getMessage("all-users-ready").format(self._userlist.readyUserCount()))
allReadyMessage = getMessage("all-users-ready").format(self._userlist.readyUserCount())
osdMessage = u"{}{}{}".format(fileDifferencesMessage, self._client._player.osdMessageSeparator, allReadyMessage)
else:
osdMessage = u"{}{}{}".format(fileDifferencesMessage, self._client._player.osdMessageSeparator, getMessage("not-all-ready").format(self._userlist.usersInRoomNotReady()))
notAllReadyMessage = getMessage("not-all-ready").format(self._userlist.usersInRoomNotReady())
osdMessage = u"{}{}{}".format(fileDifferencesMessage, self._client._player.osdMessageSeparator, notAllReadyMessage)
else:
osdMessage = fileDifferencesMessage
elif self._userlist.isReadinessSupported():
@ -845,9 +959,9 @@ class SyncplayUser(object):
def __repr__(self, *args, **kwargs):
if self.file:
return "{}: {} ({}, {})".format(self.username, self.file['name'], self.file['duration'], self.file['size'])
return u"{}: {} ({}, {})".format(self.username, self.file['name'], self.file['duration'], self.file['size'])
else:
return "{}".format(self.username)
return u"{}".format(self.username)
def setControllerStatus(self, isController):
self._controller = isController
@ -1011,13 +1125,19 @@ class SyncplayUserlist(object):
user = self._users[username]
user.setControllerStatus(True)
def areAllUsersInRoomReady(self):
def areAllUsersInRoomReady(self, requireSameFilenames=False):
if not self.currentUser.canControl():
return True
if not self.currentUser.isReady():
return False
for user in self._users.itervalues():
if user.room == self.currentUser.room and user.isReadyWithFile() == False:
if user.room == self.currentUser.room:
if user.isReadyWithFile() == False:
return False
elif requireSameFilenames and\
(self.currentUser.file is None
or user.file is None
or not utils.sameFilename(self.currentUser.file['name'], user.file['name'])):
return False
return True
@ -1155,6 +1275,15 @@ class UiManager(object):
self.lastSecondaryOSDEndTime = None
self.lastError = ""
def setPlaylist(self, newPlaylist, newIndexFilename=None):
self.__ui.setPlaylist(newPlaylist, newIndexFilename)
def setPlaylistIndexFilename(self, filename):
self.__ui.setPlaylistIndexFilename(filename)
def fileSwitchFoundFiles(self):
self.__ui.fileSwitchFoundFiles()
def showDebugMessage(self, message):
if constants.DEBUG_MODE and message.rstrip():
sys.stderr.write("{}{}\n".format(time.strftime(constants.UI_TIME_FORMAT, time.localtime()),message.rstrip()))
@ -1215,4 +1344,422 @@ class UiManager(object):
def drop(self):
self.__ui.drop()
class SyncplayPlaylist():
def __init__(self, client):
self._client = client
self._ui = self._client.ui
self._previousPlaylist = None
self._previousPlaylistRoom = None
self._playlist = []
self._playlistIndex = None
self.addedChangeListCallback = False
self._lastPlaylistIndexChange = time.time()
def needsSharedPlaylistsEnabled(f): # @NoSelf
@wraps(f)
def wrapper(self, *args, **kwds):
if not self._client.sharedPlaylistIsEnabled():
self._ui.showDebugMessage(u"Tried to use shared playlists when it was disabled!")
return
return f(self, *args, **kwds)
return wrapper
def changeToPlaylistIndexFromFilename(self, filename):
try:
index = self._playlist.index(filename)
if index <> self._playlistIndex:
self.changeToPlaylistIndex(index)
except ValueError:
pass
def changeToPlaylistIndex(self, index, username = None):
if self._playlist is None or len(self._playlist) == 0:
return
if index is None:
return
if username is None and not self._client.sharedPlaylistIsEnabled():
return
self._lastPlaylistIndexChange = time.time()
if self._client.playerIsNotReady():
if not self.addedChangeListCallback:
self.addedChangeListCallback = True
self._client.addPlayerReadyCallback(lambda x: self.changeToPlaylistIndex(index, username))
return
try:
filename = self._playlist[index]
self._ui.setPlaylistIndexFilename(filename)
if not self._client.sharedPlaylistIsEnabled():
self._playlistIndex = index
if username is not None and self._client.userlist.currentUser.file and filename == self._client.userlist.currentUser.file['name']:
return
except IndexError:
pass
self._playlistIndex = index
if username is None:
if self._client.isConnectedAndInARoom() and self._client.sharedPlaylistIsEnabled():
self._client.setPlaylistIndex(index)
else:
self._ui.showMessage(getMessage("playlist-selection-changed-notification").format(username))
self.switchToNewPlaylistIndex(index)
@needsSharedPlaylistsEnabled
def switchToNewPlaylistIndex(self, index, resetPosition=False):
self._lastPlaylistIndexChange = time.time()
if self._client.playerIsNotReady():
self._client.addPlayerReadyCallback(lambda x: self.switchToNewPlaylistIndex(index, resetPosition))
return
try:
if index is None:
self._ui.showDebugMessage(u"Cannot switch to None index in playlist")
return
filename = self._playlist[index]
# TODO: Handle isse with index being None
if utils.isURL(filename):
if self._client.isURITrusted(filename):
self._client.openFile(filename, resetPosition=resetPosition)
else:
self._ui.showErrorMessage(getMessage("cannot-add-unsafe-path-error").format(filename))
return
else:
path = self._client.fileSwitch.findFilepath(filename, highPriority=True)
if path:
self._client.openFile(path, resetPosition)
else:
self._ui.showErrorMessage(getMessage("cannot-find-file-for-playlist-switch-error").format(filename))
return
except IndexError:
self._ui.showDebugMessage("Could not change playlist index due to IndexError")
def _getValidIndexFromNewPlaylist(self, newPlaylist=None):
if self._playlistIndex is None or not newPlaylist or len(newPlaylist) <= 1:
return 0
i = self._playlistIndex
while i <= len(self._playlist):
try:
filename = self._playlist[i]
validIndex = newPlaylist.index(filename)
return validIndex
except:
i += 1
i = self._playlistIndex
while i > 0:
try:
filename = self._playlist[i]
validIndex = newPlaylist.index(filename)
return validIndex+1 if validIndex < len(newPlaylist)-1 else validIndex
except:
i -= 1
return 0
def _getFilenameFromIndexInGivenPlaylist(self, _playlist, _index):
if not _index or not _playlist:
return None
filename = _playlist[_index] if len(_playlist) > _index else None
return filename
def changePlaylist(self, files, username = None, resetIndex=False):
if self._playlist == files:
if self._playlistIndex <> 0 and resetIndex:
self.changeToPlaylistIndex(0)
return
if resetIndex:
newIndex = 0
filename = files[0] if files and len(files) > 0 else None
else:
newIndex = self._getValidIndexFromNewPlaylist(files)
filename = self._getFilenameFromIndexInGivenPlaylist(files, newIndex)
self._updateUndoPlaylistBuffer(newPlaylist=files, newRoom=self._client.userlist.currentUser.room)
self._playlist = files
if username is None:
if self._client.isConnectedAndInARoom() and self._client.sharedPlaylistIsEnabled():
self._client._protocol.setPlaylist(files)
self.changeToPlaylistIndex(newIndex)
self._ui.setPlaylist(self._playlist, filename)
self._ui.showMessage(getMessage("playlist-contents-changed-notification").format(self._client.getUsername()))
else:
self._ui.setPlaylist(self._playlist)
self._ui.showMessage(getMessage("playlist-contents-changed-notification").format(username))
@needsSharedPlaylistsEnabled
def undoPlaylistChange(self):
if self.canUndoPlaylist(self._playlist):
newPlaylist = self._getPreviousPlaylist()
self.changePlaylist(newPlaylist, username=None)
@needsSharedPlaylistsEnabled
def shufflePlaylist(self):
if self._playlist and len(self._playlist) > 0:
shuffledPlaylist = deepcopy(self._playlist)
random.shuffle(shuffledPlaylist)
self.changePlaylist(shuffledPlaylist, username=None, resetIndex=True)
def canUndoPlaylist(self, currentPlaylist):
return self._previousPlaylist is not None and currentPlaylist <> self._previousPlaylist
def loadCurrentPlaylistIndex(self):
if self._notPlayingCurrentIndex():
self.switchToNewPlaylistIndex(self._playlistIndex)
@needsSharedPlaylistsEnabled
def advancePlaylistCheck(self):
position = self._client.getStoredPlayerPosition()
currentLength = self._client.userlist.currentUser.file["duration"] if self._client.userlist.currentUser.file else 0
if currentLength > constants.PLAYLIST_LOAD_NEXT_FILE_MINIMUM_LENGTH\
and abs(position - currentLength ) < constants.PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD\
and self.notJustChangedPlaylist():
self.loadNextFileInPlaylist()
def notJustChangedPlaylist(self):
secondsSinceLastChange = time.time() - self._lastPlaylistIndexChange
return secondsSinceLastChange > constants.PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD
@needsSharedPlaylistsEnabled
def loadNextFileInPlaylist(self):
if self._notPlayingCurrentIndex():
return
if len(self._playlist) == 1 and self._client.loopSingleFiles():
self._client.rewindFile()
self._client.setPaused(False)
reactor.callLater(0.5, self._client.setPaused, False,)
elif self._thereIsNextPlaylistIndex():
self.switchToNewPlaylistIndex(self._nextPlaylistIndex(), resetPosition=True)
def _updateUndoPlaylistBuffer(self, newPlaylist, newRoom):
if self._playlistBufferIsFromOldRoom(newRoom):
self._movePlaylistBufferToNewRoom(newRoom)
elif self._playlistBufferNeedsUpdating(newPlaylist):
self._previousPlaylist = self._playlist
def _getPreviousPlaylist(self):
return self._previousPlaylist
def _notPlayingCurrentIndex(self):
if self._playlistIndex is None or self._playlist is None or len(self._playlist) <= self._playlistIndex:
self._ui.showDebugMessage(u"Not playing current index - Index none or length issue")
return True
currentPlaylistFilename = self._playlist[self._playlistIndex]
if self._client.userlist.currentUser.file and currentPlaylistFilename == self._client.userlist.currentUser.file['name']:
return False
else:
self._ui.showDebugMessage(u"Not playing current index - Filename mismatch or no file")
return True
def _thereIsNextPlaylistIndex(self):
if self._playlistIndex is None:
return False
elif len(self._playlist) == 1 and not self._client.loopSingleFiles():
return False
elif self._playlistIsAtEnd():
return self._client.isPlaylistLoopingEnabled()
else:
return True
def _nextPlaylistIndex(self):
if self._playlistIsAtEnd():
return 0
else:
return self._playlistIndex+1
def _playlistIsAtEnd(self):
return len(self._playlist) <= self._playlistIndex+1
def _playlistBufferIsFromOldRoom(self, newRoom):
return self._previousPlaylistRoom <> newRoom
def _movePlaylistBufferToNewRoom(self, currentRoom):
self._previousPlaylist = None
self._previousPlaylistRoom = currentRoom
def _playlistBufferNeedsUpdating(self, newPlaylist):
return self._previousPlaylist <> self._playlist and self._playlist <> newPlaylist
class FileSwitchManager(object):
def __init__(self, client):
self._client = client
self.mediaFilesCache = {}
self.filenameWatchlist = []
self.currentDirectory = None
self.mediaDirectories = None
self.lock = threading.Lock()
self.folderSearchEnabled = True
self.directorySearchError = None
self.newInfo = False
self.currentlyUpdating = False
self.newWatchlist = []
self.fileSwitchTimer = task.LoopingCall(self.updateInfo)
self.fileSwitchTimer.start(constants.FOLDER_SEARCH_DOUBLE_CHECK_INTERVAL, True)
self.mediaDirectoriesNotFound = []
def setClient(self, newClient):
self.client = newClient
def setCurrentDirectory(self, curDir):
self.currentDirectory = curDir
def changeMediaDirectories(self, mediaDirs):
from syncplay.ui.ConfigurationGetter import ConfigurationGetter
ConfigurationGetter().setConfigOption("mediaSearchDirectories", mediaDirs)
self._client._config["mediaSearchDirectories"] = mediaDirs
self._client.ui.showMessage(getMessage("media-directory-list-updated-notification"))
self.mediaDirectoriesNotFound = []
self.folderSearchEnabled = True
self.setMediaDirectories(mediaDirs)
if mediaDirs == "":
self._client.ui.showErrorMessage(getMessage("no-media-directories-error"))
self.mediaFilesCache = {}
self.newInfo = True
self.checkForFileSwitchUpdate()
def setMediaDirectories(self, mediaDirs):
self.mediaDirectories = mediaDirs
self.updateInfo()
def checkForFileSwitchUpdate(self):
if self.newInfo:
self.newInfo = False
self.infoUpdated()
if self.directorySearchError:
self._client.ui.showErrorMessage(self.directorySearchError)
self.directorySearchError = None
def updateInfo(self):
if not self.currentlyUpdating and self.mediaDirectories:
threads.deferToThread(self._updateInfoThread).addCallback(lambda x: self.checkForFileSwitchUpdate())
def setFilenameWatchlist(self, unfoundFilenames):
self.filenameWatchlist = unfoundFilenames
def _updateInfoThread(self):
with self.lock:
try:
self.currentlyUpdating = True
dirsToSearch = self.mediaDirectories
if dirsToSearch:
# Spin up hard drives to prevent premature timeout
randomFilename = u"RandomFile"+unicode(random.randrange(10000, 99999))+u".txt"
for directory in dirsToSearch:
if not os.path.isdir(directory):
self.directorySearchError = getMessage("cannot-find-directory-error").format(directory)
startTime = time.time()
if os.path.isfile(os.path.join(directory, randomFilename)):
randomFilename = u"RandomFile"+unicode(random.randrange(10000, 99999))+u".txt"
if time.time() - startTime > constants.FOLDER_SEARCH_FIRST_FILE_TIMEOUT:
self.folderSearchEnabled = False
self.directorySearchError = getMessage("folder-search-first-file-timeout-error").format(directory)
return
# Actual directory search
newMediaFilesCache = {}
startTime = time.time()
for directory in dirsToSearch:
for root, dirs, files in os.walk(directory):
newMediaFilesCache[root] = files
if time.time() - startTime > constants.FOLDER_SEARCH_TIMEOUT:
self.directorySearchError = getMessage("folder-search-timeout-error").format(directory)
self.folderSearchEnabled = False
return
if self.mediaFilesCache <> newMediaFilesCache:
self.mediaFilesCache = newMediaFilesCache
self.newInfo = True
finally:
self.currentlyUpdating = False
def infoUpdated(self):
self._client.fileSwitchFoundFiles()
def findFilepath(self, filename, highPriority=False):
if filename is None:
return
if self.mediaFilesCache is not None:
for directory in self.mediaFilesCache:
files = self.mediaFilesCache[directory]
if len(files) > 0 and filename in files:
filepath = os.path.join(directory, filename)
if os.path.isfile(filepath):
return filepath
if highPriority and self.folderSearchEnabled:
directoryList = self.mediaDirectories
# Spin up hard drives to prevent premature timeout
randomFilename = u"RandomFile"+unicode(random.randrange(10000, 99999))+u".txt"
for directory in directoryList:
startTime = time.time()
if os.path.isfile(os.path.join(directory, randomFilename)):
randomFilename = u"RandomFile"+unicode(random.randrange(10000, 99999))+u".txt"
if not self.folderSearchEnabled:
return
if time.time() - startTime > constants.FOLDER_SEARCH_FIRST_FILE_TIMEOUT:
return
startTime = time.time()
if filename and directoryList:
for directory in directoryList:
for root, dirs, files in os.walk(directory):
if filename in files:
return os.path.join(root,filename)
if time.time() - startTime > constants.FOLDER_SEARCH_TIMEOUT:
self.folderSearchEnabled = False
self.directorySearchError = getMessage("folder-search-timeout-error").format(directory)
return None
return None
def areWatchedFilenamesInCache(self):
if self.filenameWatchlist is not None:
for filename in self.filenameWatchlist:
if self.isFilenameInCache(filename):
return True
def isFilenameInCache(self, filename):
if filename is not None and self.mediaFilesCache is not None:
for directory in self.mediaFilesCache:
files = self.mediaFilesCache[directory]
if filename in files:
return True
def getDirectoryOfFilenameInCache(self, filename):
if filename is not None and self.mediaFilesCache is not None:
for directory in self.mediaFilesCache:
files = self.mediaFilesCache[directory]
if filename in files:
return directory
return None
def isDirectoryInList(self, directoryToFind, folderList):
if directoryToFind and folderList:
normedDirectoryToFind = os.path.normcase(os.path.normpath(directoryToFind))
for listedFolder in folderList:
normedListedFolder = os.path.normcase(os.path.normpath(listedFolder))
if normedDirectoryToFind.startswith(normedListedFolder):
return True
return False
def notifyUserIfFileNotInMediaDirectory(self, filenameToFind, path):
directoryToFind = os.path.dirname(path)
if directoryToFind in self.mediaDirectoriesNotFound:
return
if self.mediaDirectories is not None and self.mediaFilesCache is not None:
if directoryToFind in self.mediaFilesCache:
return
for directory in self.mediaFilesCache:
files = self.mediaFilesCache[directory]
if filenameToFind in files:
return
if directoryToFind in self.mediaFilesCache:
return
if self.isDirectoryInList(directoryToFind, self.mediaDirectories):
return
self._client.ui.showErrorMessage(getMessage("added-file-not-in-media-directory-error").format(directoryToFind))
self.mediaDirectoriesNotFound.append(directoryToFind)

View File

@ -17,6 +17,8 @@ SHOW_TOOLTIPS = True
WARN_ABOUT_MISSING_STRINGS = False # (If debug mode is enabled)
FALLBACK_INITIAL_LANGUAGE = "en"
FALLBACK_PUBLIC_SYNCPLAY_SERVERS = [[u'syncplay.pl:8995 (France)', u'syncplay.pl:8995'],[u'syncplay.pl:8996 (France)', u'syncplay.pl:8996'],[u'syncplay.pl:8997 (France)', u'syncplay.pl:8997'],[u'syncplay.pl:8998 (France)', u'syncplay.pl:8998'],[u'syncplay.pl:8999 (France)', u'syncplay.pl:8999']]
PLAYLIST_LOAD_NEXT_FILE_MINIMUM_LENGTH = 10 # Seconds
PLAYLIST_LOAD_NEXT_FILE_TIME_FROM_END_THRESHOLD = 5 # Seconds (only triggered if file is paused, e.g. due to EOF)
#Overriden by config
SHOW_OSD = True # Sends Syncplay messages to media player OSD
@ -51,8 +53,9 @@ AUTOPLAY_DELAY = 3.0
SYNC_ON_PAUSE = True # Client seek to global position - subtitles may disappear on some media players
# Options for the File Switch feature:
FOLDER_SEARCH_TIMEOUT = 60.0 # Secs - How long to wait until searches in folder to update cache are aborted (may be longer than this if hard drive needs to spin up)
FOLDER_SEARCH_DOUBLE_CHECK_INTERVAL = 120.0 # Secs - Frequency of updating cache when someone is playing a file not in current cache
FOLDER_SEARCH_FIRST_FILE_TIMEOUT = 10.0 # Secs - How long to wait to find the first file in folder search (to take account of HDD spin up)
FOLDER_SEARCH_TIMEOUT = 3.0 # Secs - How long to wait until searches in folder to update cache are aborted (after first file is found)
FOLDER_SEARCH_DOUBLE_CHECK_INTERVAL = 30.0 # Secs - Frequency of updating cache when someone is playing a file not in current cache
#Usually there's no need to adjust these
LAST_PAUSED_DIFF_THRESHOLD = 2
@ -69,7 +72,7 @@ COMMANDS_AUTH = ['a','auth']
COMMANDS_TOGGLE = ['t','toggle']
MPC_MIN_VER = "1.6.4"
VLC_MIN_VERSION = "2.2.1"
VLC_INTERFACE_MIN_VERSION = "0.2.8"
VLC_INTERFACE_MIN_VERSION = "0.2.9"
VLC_LATENCY_ERROR_THRESHOLD = 2.0
MPV_UNRESPONSIVE_THRESHOLD = 60.0
CONTROLLED_ROOMS_MIN_VERSION = "1.3.0"
@ -107,6 +110,8 @@ MPV_ICONPATH = "mpv.png"
MPC_ICONPATH = "mpc-hc.png"
MPC64_ICONPATH = "mpc-hc64.png"
MPV_ERROR_MESSAGES_TO_REPEAT = ['[ytdl_hook] Your version of youtube-dl is too old', '[ytdl_hook] youtube-dl failed', 'Failed to recognize file format.']
#Changing these is usually not something you're looking for
PLAYER_ASK_DELAY = 0.1
PING_MOVING_AVERAGE_WEIGHT = 0.85
@ -116,7 +121,10 @@ MPC_RETRY_WAIT_TIME = 0.01
MPC_MAX_RETRIES = 30
MPC_PAUSE_TOGGLE_DELAY = 0.05
MPV_NEWFILE_IGNORE_TIME = 1
MPV_LOCK_WAIT_TIME = 0.2
MPV_SENDMESSAGE_COOLDOWN_TIME = 0.05
MPV_MAX_NEWFILE_COOLDOWN_TIME = 3
STREAM_ADDITIONAL_IGNORE_TIME = 10
MPV_LOCK_WAIT_TIME = 0.05
VLC_OPEN_MAX_WAIT_TIME = 15
VLC_MIN_PORT = 10000
VLC_MAX_PORT = 55000
@ -136,6 +144,7 @@ STYLE_ERRORNOTIFICATION = "color: red;"
STYLE_DIFFERENTITEM_COLOR = 'red'
STYLE_NOFILEITEM_COLOR = 'blue'
STYLE_NOTCONTROLLER_COLOR = 'grey'
STYLE_UNTRUSTEDITEM_COLOR = 'purple'
USERLIST_GUI_USERNAME_OFFSET = 21 # Pixels
USERLIST_GUI_USERNAME_COLUMN = 0
@ -149,6 +158,8 @@ MPV_NEW_VERSION = False
VLC_SLAVE_ARGS = ['--extraintf=luaintf', '--lua-intf=syncplay', '--no-quiet', '--no-input-fast-seek',
'--play-and-pause', '--start-time=0']
VLC_SLAVE_NONOSX_ARGS = ['--no-one-instance', '--no-one-instance-when-started-from-file']
MPV_SUPERSEDE_IF_DUPLICATE_COMMANDS = ["no-osd set time-pos ", "loadfile "]
MPV_REMOVE_BOTH_IF_DUPLICATE_COMMANDS = ["cycle pause"]
MPLAYER_ANSWER_REGEX = "^ANS_([a-zA-Z_-]+)=(.+)$|^(Exiting)\.\.\. \((.+)\)$"
VLC_ANSWER_REGEX = r"(?:^(?P<command>[a-zA-Z_]+)(?:\: )?(?P<argument>.*))"
UI_COMMAND_REGEX = r"^(?P<command>[^\ ]+)(?:\ (?P<parameter>.+))?"
@ -176,9 +187,13 @@ FILEITEM_SWITCH_ROLE = 1
FILEITEM_SWITCH_NO_SWITCH = 0
FILEITEM_SWITCH_FILE_SWITCH = 1
FILEITEM_SWITCH_STREAM_SWITCH = 2
PLAYLISTITEM_CURRENTLYPLAYING_ROLE = 3
SYNCPLAY_UPDATE_URL = u"http://syncplay.pl/checkforupdate?{}" # Params
SYNCPLAY_DOWNLOAD_URL = "http://syncplay.pl/download/"
SYNCPLAY_PUBLIC_SERVER_LIST_URL = u"http://syncplay.pl/listpublicservers?{}" # Params
DEFAULT_TRUSTED_DOMAINS = [u"youtube.com",u"youtu.be"]
TRUSTABLE_WEB_PROTOCOLS = [u"http://www.",u"https://www.",u"http://",u"https://"]
PRIVATE_FILE_FIELDS = ["path"]

View File

@ -1,5 +1,6 @@
# coding:utf8
from syncplay import constants
import messages_en
import messages_ru
import messages_de

View File

@ -28,6 +28,8 @@ de = {
"current-offset-notification" : u"Aktueller Offset: {} Sekunden", # Offset
"media-directory-list-updated-notification" : u"Syncplay media directories have been updated.", # TODO: Translate
"room-join-notification" : u"<{}> hat den Raum '{}' betreten", # User
"left-notification" : u"<{}> ist gegangen", # User
"left-paused-notification" : u"<{}> ist gegangen, <{}> pausierte", # User who left, User who paused
@ -136,8 +138,12 @@ de = {
"invalid-seek-value" : u"Ungültige Zeitangabe",
"invalid-offset-value" : u"Ungültiger Offset-Wert",
"switch-file-not-found-error" : u"Konnte nicht zur Datei '{0}' wechseln. Syncplay sucht im Ordner der aktuellen Datei und angegebenen Medien-Verzeichnissen.", # File not found, folder it was not found in
"folder-search-timeout-error" : u"Die Suche nach Mediendateien in '{}' wurde abgebrochen weil sie zu lange gedauert hat. Dies tritt auf, wenn ein zu durchsuchender Medienordner zu viele Unterordner hat. Syncplay wird bis zum Neustart nur noch das Verzeichnis der aktuellen Datei durchsuchen.", #Folder
"switch-file-not-found-error" : u"Konnte nicht zur Datei '{0}' wechseln. Syncplay looks in the specified media directories.", # File not found, folder it was not found in # TODO: Re-translate "Syncplay sucht im Ordner der aktuellen Datei und angegebenen Medien-Verzeichnissen." to reference to checking in "current media directory"
"folder-search-timeout-error" : u"The search for media in media directories was aborted as it took too long to search through '{}'. This will occur if you select a folder with too many sub-folders in your list of media folders to search through. For automatic file switching to work again please select File->Set Media Directories in the menu bar and remove this directory or replace it with an appropriate sub-folder.", #Folder # TODO: Translate
"folder-search-first-file-timeout-error" : u"The search for media in '{}' was aborted as it took too long to access the directory. This could happen if it is a network drive or if you configure your drive to spin down after a period of inactivity. For automatic file switching to work again please go to File->Set Media Directories and either remove the directory or resolve the issue (e.g. by changing power saving settings).", #Folder # TODO: Translate
"added-file-not-in-media-directory-error" : u"You loaded a file in '{}' which is not a known media directory. You can add this as a media directory by selecting File->Set Media Directories in the menu bar.", #Folder # TODO: Translate
"no-media-directories-error" : u"No media directories have been set. For shared playlist and file switching features to work properly please select File->Set Media Directories and specify where Syncplay should look to find media files.", # TODO: Translate
"cannot-find-directory-error" : u"Could not find media directory '{}'. To update your list of media directories please select File->Set Media Directories from the menu bar and specify where Syncplay should look to find media files.", # TODO: Translate
"failed-to-load-server-list-error" : u"Konnte die Liste der öffentlichen Server nicht laden. Bitte besuche http://www.syncplay.pl/ [Englisch] mit deinem Browser.",
@ -191,7 +197,6 @@ de = {
"pauseonleave-label" : u"Pausieren wenn ein Benutzer austritt",
"readyatstart-label" : u"Standardmäßig auf \'Bereit\' stellen",
"forceguiprompt-label" : u"Diesen Dialog nicht mehr anzeigen",
"nostore-label" : u"Diese Konfiguration nicht speichern",
"showosd-label" : u"OSD-Nachrichten anzeigen",
"showosdwarnings-label" : u"Zeige Warnungen (z.B. wenn Dateien verschieden)",
@ -221,6 +226,7 @@ de = {
"unpause-ifothersready-option" : u"Wiedergeben wenn bereits als Bereit gesetzt oder alle anderen bereit sind (Standard)",
"unpause-ifminusersready-option" : u"Wiedergeben wenn bereits als Bereit gesetzt oder die minimale Anzahl anderer Nutzer bereit ist",
"unpause-always" : u"Immer wiedergeben",
"syncplay-trusteddomains-title": u"Trusted domains (for streaming services and hosted content)", # TODO: Translate into German
"help-label" : u"Hilfe",
"reset-label" : u"Standardwerte zurücksetzen",
@ -229,7 +235,8 @@ de = {
"contact-label" : u"Du hast eine Idee, einen Bug gefunden oder möchtest Feedback geben? Sende eine E-Mail an <a href=\"mailto:dev@syncplay.pl\">dev@syncplay.pl</a>, chatte auf dem <a href=\"https://webchat.freenode.net/?channels=#syncplay\">#Syncplay IRC-Kanal</a> auf irc.freenode.net oder <a href=\"https://github.com/Uriziel/syncplay/issues\">öffne eine Fehlermeldung auf GitHub</a>. Außerdem findest du auf <a href=\"http://syncplay.pl/\">http://syncplay.pl/</a> weitere Informationen, Hilfestellungen und Updates.",
"joinroom-menu-label" : u"Raum beitreten",
"joinroom-label" : u"Raum beitreten",
"joinroom-menu-label" : u"Raum beitreten {}", #TODO: Might want to fix this
"seektime-menu-label" : u"Spule zu Zeit",
"undoseek-menu-label" : u"Rückgängig",
"play-menu-label" : u"Wiedergabe",
@ -253,12 +260,14 @@ de = {
"file-menu-label" : u"&Datei", # & precedes shortcut key
"openmedia-menu-label" : u"&Mediendatei öffnen...",
"openstreamurl-menu-label" : u"&Stream URL öffnen",
"setmediadirectories-menu-label" : u"Set media &directories", # TODO: Translate
"exit-menu-label" : u"&Beenden",
"advanced-menu-label" : u"&Erweitert",
"window-menu-label" : u"&Fenster",
"setoffset-menu-label" : u"&Offset einstellen",
"createcontrolledroom-menu-label" : u"&Zentral gesteuerten Raum erstellen",
"identifyascontroller-menu-label" : u"Als Raumleiter &identifizieren",
"settrusteddomains-menu-label" : u"Set &trusted domains", # TODO: Translate
"playback-menu-label" : u"&Wiedergabe",
@ -272,6 +281,11 @@ de = {
"promptforstreamurl-msgbox-label" : u"Stream URL öffnen",
"promptforstreamurlinfo-msgbox-label" : u"Stream URL",
"addfolder-label" : u"Add folder", # TODO: Translate
"adduris-msgbox-label" : u"Add URLs to playlist (one per line)", # TODO: Translate
"trusteddomains-msgbox-label" : u"Domains it is okay to automatically switch to (one per line)", # TODO: Translate
"createcontrolledroom-msgbox-label" : u"Zentral gesteuerten Raum erstellen",
"controlledroominfo-msgbox-label" : u"Namen des zentral gesteuerten Raums eingeben\r\n(siehe http://syncplay.pl/guide/ für eine Anleitung [Englisch]):",
@ -323,6 +337,7 @@ de = {
"unpause-ifalreadyready-tooltip" : u"Wenn du nicht bereit bist und Play drückst wirst du als bereit gesetzt - zum Starten der Wiedergabe nochmal drücken.",
"unpause-ifothersready-tooltip" : u"Wenn du Play drückst und nicht bereit bist, wird nur gestartet, wenn alle anderen bereit sind.",
"unpause-ifminusersready-tooltip" : u"Wenn du Play drückst und nicht bereit bist, wird nur gestartet, wenn die minimale Anzahl anderer Benutzer bereit ist.",
"trusteddomains-arguments-tooltip" : u"Domains that it is okay for Syncplay to automatically switch to when shared playlists is enabled.", # TODO: Translate into German
"help-tooltip" : u"Öffnet Hilfe auf syncplay.pl [Englisch]",
"reset-tooltip" : u"Alle Einstellungen auf Standardwerte zurücksetzen.",
@ -368,5 +383,28 @@ de = {
"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"
}
"hello-server-error" : u"Zu wenige Hello-Argumente",
# Playlists TODO: Translate all this to German
"playlist-selection-changed-notification" : u"{} changed the playlist selection", # Username
"playlist-contents-changed-notification" : u"{} updated the playlist", # Username
"cannot-find-file-for-playlist-switch-error" : u"Could not find file {} in media directories for playlist switch!", # Filename
"cannot-add-duplicate-error" : u"Could not add second entry for '{}' to the playlist as no duplicates are allowed.", #Filename
"cannot-add-unsafe-path-error" : u"Could not automatically load {} because it is not on a trusted domain. You can switch to the URL manually by double clicking it in the playlist, and add trusted domains via File->Advanced->Set Trusted Domains.", # Filename
"sharedplaylistenabled-label" : u"Enable shared playlists",
"removefromplaylist-menu-label" : u"Remove from playlist",
"shuffleplaylist-menuu-label" : u"Shuffle playlist",
"undoplaylist-menu-label" : u"Undo last change to playlist",
"addfilestoplaylist-menu-label" : u"Add file(s) to bottom of playlist",
"addurlstoplaylist-menu-label" : u"Add URL(s) to bottom of playlist",
"addusersfiletoplaylist-menu-label" : u"Add {} file to playlist", # item owner indicator
"addusersstreamstoplaylist-menu-label" : u"Add {} stream to playlist", # item owner indicator
"openusersstream-menu-label" : u"Open {} stream", # [username]'s
"openusersfile-menu-label" : u"Open {} file", # [username]'s
"item-is-yours-indicator" : u"your", # Goes with addusersfiletoplaylist/addusersstreamstoplaylist
"item-is-others-indicator" : u"{}'s", # username - goes with addusersfiletoplaylist/addusersstreamstoplaylist
"playlist-instruction-item-message" : u"Drag file here to add it to the shared playlist.",
"sharedplaylistenabled-tooltip" : u"Room operators can add files to a synced playlist to make it easy for everyone to watching the same thing. Configure media directories under 'Misc'.",
}

View File

@ -28,6 +28,8 @@ en = {
"current-offset-notification" : "Current offset: {} seconds", # Offset
"media-directory-list-updated-notification" : u"Syncplay media directories have been updated.",
"room-join-notification" : u"<{}> has joined the room: '{}'", # User
"left-notification" : u"<{}> has left", # User
"left-paused-notification" : u"<{}> left, <{}> paused", # User who left, User who paused
@ -136,8 +138,12 @@ en = {
"invalid-seek-value" : u"Invalid seek value",
"invalid-offset-value" : u"Invalid offset value",
"switch-file-not-found-error" : u"Could not switch to file '{0}'. Syncplay looks in the folder of the currently playing file and specified media directories.", # File not found
"folder-search-timeout-error" : u"The search for media in '{}' was aborted as it took too long. This will occur if you select a folder with too many sub-folders in your list of media folders to search through. Until Syncplay is restarted only the directory of the currently open file will be checked.", #Folder
"switch-file-not-found-error" : u"Could not switch to file '{0}'. Syncplay looks in specified media directories.", # File not found
"folder-search-timeout-error" : u"The search for media in media directories was aborted as it took too long to search through '{}'. This will occur if you select a folder with too many sub-folders in your list of media folders to search through. For automatic file switching to work again please select File->Set Media Directories in the menu bar and remove this directory or replace it with an appropriate sub-folder.", #Folder
"folder-search-first-file-timeout-error" : u"The search for media in '{}' was aborted as it took too long to access the directory. This could happen if it is a network drive or if you configure your drive to spin down after a period of inactivity. For automatic file switching to work again please go to File->Set Media Directories and either remove the directory or resolve the issue (e.g. by changing power saving settings).", #Folder
"added-file-not-in-media-directory-error" : u"You loaded a file in '{}' which is not a known media directory. You can add this as a media directory by selecting File->Set Media Directories in the menu bar.", #Folder
"no-media-directories-error" : u"No media directories have been set. For shared playlist and file switching features to work properly please select File->Set Media Directories and specify where Syncplay should look to find media files.",
"cannot-find-directory-error" : u"Could not find media directory '{}'. To update your list of media directories please select File->Set Media Directories from the menu bar and specify where Syncplay should look to find media files.",
"failed-to-load-server-list-error" : u"Failed to load public server list. Please visit http://www.syncplay.pl/ in your browser.",
@ -193,7 +199,6 @@ en = {
"pauseonleave-label" : "Pause when user leaves (e.g. if they are disconnected)",
"readyatstart-label" : "Set me as 'ready to watch' by default",
"forceguiprompt-label" : "Don't always show the Syncplay configuration window", # (Inverted)
"nostore-label" : "Don't store this configuration", # (Inverted)
"showosd-label" : "Enable OSD Messages",
"showosdwarnings-label" : "Include warnings (e.g. when files are different, users not ready)",
@ -202,7 +207,7 @@ en = {
"showdifferentroomosd-label" : "Include events in other rooms",
"showslowdownosd-label" :"Include slowing down / reverting notifications",
"language-label" : "Language:",
"automatic-language" : "Default ({})", # Default language
"automatic-language" : u"Default ({})", # Default language
"showdurationnotification-label" : "Warn about media duration mismatches",
"basics-label" : "Basics",
"readiness-label" : u"Play/Pause",
@ -223,6 +228,7 @@ en = {
"unpause-ifothersready-option" : u"Unpause if already ready or others in room are ready (default)",
"unpause-ifminusersready-option" : u"Unpause if already ready or if all others ready and min users ready",
"unpause-always" : u"Always unpause",
"syncplay-trusteddomains-title": u"Trusted domains (for streaming services and hosted content)",
"help-label" : "Help",
"reset-label" : "Restore defaults",
@ -231,7 +237,8 @@ en = {
"contact-label" : "Feel free to e-mail <a href=\"mailto:dev@syncplay.pl\"><nobr>dev@syncplay.pl</nobr></a>, chat via the <a href=\"https://webchat.freenode.net/?channels=#syncplay\"><nobr>#Syncplay IRC channel</nobr></a> on irc.freenode.net, <a href=\"https://github.com/Uriziel/syncplay/issues\"><nobr>raise an issue</nobr></a> via GitHub, <a href=\"https://www.facebook.com/SyncplaySoftware\"><nobr>like us on Facebook</nobr></a>, <a href=\"https://twitter.com/Syncplay/\"><nobr>follow us on Twitter</nobr></a>, or visit <a href=\"http://syncplay.pl/\"><nobr>http://syncplay.pl/</nobr></a>",
"joinroom-menu-label" : "Join room",
"joinroom-label" : "Join room",
"joinroom-menu-label" : u"Join room {}",
"seektime-menu-label" : "Seek to time",
"undoseek-menu-label" : "Undo seek",
"play-menu-label" : "Play",
@ -255,12 +262,14 @@ en = {
"file-menu-label" : "&File", # & precedes shortcut key
"openmedia-menu-label" : "&Open media file",
"openstreamurl-menu-label" : "Open &media stream URL",
"setmediadirectories-menu-label" : u"Set media &directories",
"exit-menu-label" : "E&xit",
"advanced-menu-label" : "&Advanced",
"window-menu-label" : "&Window",
"setoffset-menu-label" : "Set &offset",
"createcontrolledroom-menu-label" : "&Create managed room",
"identifyascontroller-menu-label" : "&Identify as room operator",
"settrusteddomains-menu-label" : u"Set &trusted domains",
"playback-menu-label" : u"&Playback",
@ -274,6 +283,11 @@ en = {
"promptforstreamurl-msgbox-label" : "Open media stream URL",
"promptforstreamurlinfo-msgbox-label" : "Stream URL",
"addfolder-label" : u"Add folder",
"adduris-msgbox-label" : u"Add URLs to playlist (one per line)",
"trusteddomains-msgbox-label" : u"Domains it is okay to automatically switch to (one per line)",
"createcontrolledroom-msgbox-label" : "Create managed room",
"controlledroominfo-msgbox-label" : "Enter name of managed room\r\n(see http://syncplay.pl/guide/ for usage instructions):",
@ -323,6 +337,7 @@ en = {
"unpause-ifalreadyready-tooltip" : u"If you press unpause when not ready it will set you as ready - press unpause again to unpause.",
"unpause-ifothersready-tooltip" : u"If you press unpause when not ready, it will only upause if others are ready.",
"unpause-ifminusersready-tooltip" : u"If you press unpause when not ready, it will only upause if others are ready and minimum users threshold is met.",
"trusteddomains-arguments-tooltip" : u"Domains that it is okay for Syncplay to automatically switch to when shared playlists is enabled.",
"help-tooltip" : "Opens the Syncplay.pl user guide.",
"reset-tooltip" : "Reset all settings to the default configuration.",
@ -332,7 +347,7 @@ en = {
"seektime-msgbox-label" : "Jump to specified time (in seconds / min:sec). Use +/- for relative seek.",
"ready-tooltip" : "Indicates whether you are ready to watch.",
"autoplay-tooltip" : "Auto-play when all users who have readiness indicator are ready and minimum user threshold met.",
"switch-to-file-tooltip" : "Double click to switch to {}", # Filename
"switch-to-file-tooltip" : u"Double click to switch to {}", # Filename
# In-userlist notes (GUI)
"differentsize-note" : "Different size!",
@ -345,8 +360,8 @@ en = {
# Server notifications
"welcome-server-notification" : "Welcome to Syncplay server, ver. {0}", # version
"client-connected-room-server-notification" : "{0}({2}) connected to room '{1}'", # username, host, room
"client-left-server-notification" : "{0} left server", # name
"client-connected-room-server-notification" : u"{0}({2}) connected to room '{1}'", # username, host, room
"client-left-server-notification" : u"{0} left server", # name
"no-salt-notification" : "PLEASE NOTE: To allow room operator 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
@ -360,14 +375,37 @@ en = {
"server-disable-ready-argument" : u"disable readiness feature",
"server-motd-argument": "path to file from which motd will be fetched",
"server-messed-up-motd-unescaped-placeholders": "Message of the Day has unescaped placeholders. All $ signs should be doubled ($$).",
"server-messed-up-motd-too-long": "Message of the Day is too long - maximum of {} chars, {} given.",
"server-messed-up-motd-too-long": u"Message of the Day is too long - maximum of {} chars, {} given.",
# Server errors
"unknown-command-server-error" : "Unknown command {}", # message
"unknown-command-server-error" : u"Unknown command {}", # message
"not-json-server-error" : "Not a json encoded string {}", # message
"not-known-server-error" : "You must be known to server before sending this command",
"client-drop-server-error" : "Client drop: {} -- {}", # host, error
"client-drop-server-error" : u"Client drop: {} -- {}", # host, error
"password-required-server-error" : "Password required",
"wrong-password-server-error" : "Wrong password supplied",
"hello-server-error" : "Not enough Hello arguments",
}
# Playlists
"playlist-selection-changed-notification" : u"{} changed the playlist selection", # Username
"playlist-contents-changed-notification" : u"{} updated the playlist", # Username
"cannot-find-file-for-playlist-switch-error" : u"Could not find file {} in media directories for playlist switch!", # Filename
"cannot-add-duplicate-error" : u"Could not add second entry for '{}' to the playlist as no duplicates are allowed.", #Filename
"cannot-add-unsafe-path-error" : u"Could not automatically load {} because it is not on a trusted domain. You can switch to the URL manually by double clicking it in the playlist, and add trusted domains via File->Advanced->Set Trusted Domains.", # Filename
"sharedplaylistenabled-label" : u"Enable shared playlists",
"removefromplaylist-menu-label" : u"Remove from playlist",
"shuffleplaylist-menuu-label" : u"Shuffle playlist",
"undoplaylist-menu-label" : u"Undo last change to playlist",
"addfilestoplaylist-menu-label" : u"Add file(s) to bottom of playlist",
"addurlstoplaylist-menu-label" : u"Add URL(s) to bottom of playlist",
"addusersfiletoplaylist-menu-label" : u"Add {} file to playlist", # item owner indicator
"addusersstreamstoplaylist-menu-label" : u"Add {} stream to playlist", # item owner indicator
"openusersstream-menu-label" : u"Open {} stream", # [username]'s
"openusersfile-menu-label" : u"Open {} file", # [username]'s
"item-is-yours-indicator" : u"your", # Goes with addusersfiletoplaylist/addusersstreamstoplaylist
"item-is-others-indicator" : u"{}'s", # username - goes with addusersfiletoplaylist/addusersstreamstoplaylist
"playlist-instruction-item-message" : u"Drag file here to add it to the shared playlist.",
"sharedplaylistenabled-tooltip" : u"Room operators can add files to a synced playlist to make it easy for everyone to watching the same thing. Configure media directories under 'Misc'.",
}

View File

@ -28,6 +28,8 @@ ru = {
"current-offset-notification" : u"Текущее смещение: {} секунд(ы)", # Offset
"media-directory-list-updated-notification" : u"Syncplay media directories have been updated.", # TODO: Translate
"room-join-notification" : u"<{}> зашел(зашла) в комнату: '{}'", # User
"left-notification" : u"<{}> покинул(а) комнату", # User
"left-paused-notification" : u"<{}> покинул(а) комнату, <{}> приостановил(а) воспроизведение", # User who left, User who paused
@ -136,8 +138,12 @@ ru = {
"invalid-seek-value" : u"Некорректное значение для перемотки",
"invalid-offset-value" : u"Некорректное смещение",
"switch-file-not-found-error" : u"Невозможно переключиться на файл '{0}'. Syncplay looks in the folder of the currently playing file and specified media directories.", # File not found # TODO: Translate last part into Russian
"folder-search-timeout-error" : u"The search for media in '{}' was aborted as it took too long. This will occur if you select a folder with too many sub-folders in your list of media folders to search through. Until Syncplay is restarted only the directory of the currently open file will be checked.", #Folder # TODO: Translate into Russian
"switch-file-not-found-error" : u"Невозможно переключиться на файл '{0}'. Syncplay looks in the folder specified media directories.", # File not found # TODO: Translate last part into Russian
"folder-search-timeout-error" : u"The search for media in media directories was aborted as it took too long to search through '{}'. This will occur if you select a folder with too many sub-folders in your list of media folders to search through. For automatic file switching to work again please select File->Set Media Directories in the menu bar and remove this directory or replace it with an appropriate sub-folder.", #Folder # TODO: Translate
"folder-search-first-file-timeout-error" : u"The search for media in '{}' was aborted as it took too long to access the directory. This could happen if it is a network drive or if you configure your drive to spin down after a period of inactivity. For automatic file switching to work again please go to File->Set Media Directories and either remove the directory or resolve the issue (e.g. by changing power saving settings).", #Folder # TODO: Translate
"added-file-not-in-media-directory-error" : u"You loaded a file in '{}' which is not a known media directory. You can add this as a media directory by selecting File->Set Media Directories in the menu bar.", #Folder #TODO: Translate
"no-media-directories-error" : u"No media directories have been set. For shared playlist and file switching features to work properly please select File->Set Media Directories and specify where Syncplay should look to find media files.", # TODO: Translate
"cannot-find-directory-error" : u"Could not find media directory '{}'. To update your list of media directories please select File->Set Media Directories from the menu bar and specify where Syncplay should look to find media files.", # TODO: Translate
"failed-to-load-server-list-error" : u"Failed to load public server list. Please visit http://www.syncplay.pl/ in your browser.", # TODO: Translate into Russian
@ -193,7 +199,6 @@ ru = {
"readyatstart-label" : u"Выставить статус 'готово к просмотру' по умолчанию",
"fastforwardondesync-label" : u"Ускорять видео при отставании (рекомендуется)",
"forceguiprompt-label" : u"Не показывать больше этот диалог", # (Inverted)
"nostore-label" : u"Не сохранять текущую конфигурацию", # (Inverted)
"showosd-label" : u"Включить экранные сообщения (поверх видео)",
"showosdwarnings-label" : u"Показывать предупреждения (напр., когда файлы не совпадают)",
@ -223,6 +228,7 @@ ru = {
"unpause-ifothersready-option" : u"Unpause if already ready or others in room are ready (default)", # TODO: Translate into Russian
"unpause-ifminusersready-option" : u"Unpause if already ready or if all others ready and min users ready", # TODO: Translate into Russian
"unpause-always" : u"Always unpause", # TODO: Translate into Russian
"syncplay-trusteddomains-title": u"Trusted domains (for streaming services and hosted content)", # TODO: Translate into Russian
"help-label" : u"Помощь",
"reset-label" : u"Сброс настроек",
@ -231,7 +237,8 @@ ru = {
"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\">задавайте вопросы через GitHub</a>. Кроме того, заходите на <a href=\"http://syncplay.pl/\">http://syncplay.pl/</a> за инорфмацией, помощью и обновлениями!",
"joinroom-menu-label" : u"Зайти в комнату",
"joinroom-label" : u"Зайти в комнату",
"joinroom-menu-label" : u"Зайти в комнату {}", #TODO: Might want to fix this
"seektime-menu-label" : u"Перемотать",
"undoseek-menu-label" : u"Отменить перемотку",
"play-menu-label" : u"Play",
@ -255,12 +262,14 @@ ru = {
"file-menu-label" : u"&Файл", # & precedes shortcut key
"openmedia-menu-label" : u"&Открыть видеофайл",
"openstreamurl-menu-label" : u"Открыть URL &потокового вещания",
"setmediadirectories-menu-label" : u"Set media &directories", # TODO: Translate
"exit-menu-label" : u"&Выход",
"advanced-menu-label" : u"&Дополнительно",
"window-menu-label" : u"&Окна",
"setoffset-menu-label" : u"Установить &смещение",
"createcontrolledroom-menu-label" : u"&Создать управляемую комнату",
"identifyascontroller-menu-label" : u"&Войти как оператор комнаты",
"settrusteddomains-menu-label" : u"Set &trusted domains", # TODO: Translate
"playback-menu-label" : u"&Воспроизведение",
@ -274,6 +283,11 @@ ru = {
"promptforstreamurl-msgbox-label" : u"Открыть URL потокового вещания",
"promptforstreamurlinfo-msgbox-label" : u"URL потока",
"addfolder-label" : u"Add folder", # TODO: Translate
"adduris-msgbox-label" : u"Add URLs to playlist (one per line)", # TODO: Translate
"trusteddomains-msgbox-label" : u"Domains it is okay to automatically switch to (one per line)", # TODO: Translate
"createcontrolledroom-msgbox-label" : u"Создать управляемую комнату",
"controlledroominfo-msgbox-label" : u"Введите имя управляемой комнаты\r\n(см. инструкцию на странице http://syncplay.pl/guide/):",
@ -308,7 +322,7 @@ ru = {
"pauseonleave-tooltip" : u"Приостановить воспроизведение, если Вы покинули комнату или кто-то из зрителей отключился от сервера.",
"readyatstart-tooltip" : u"Отметить Вас готовым к просмотру сразу же (по умолчанию Вы отмечены не готовым)",
"forceguiprompt-tooltip" : u"Окно настройки не будет отображаться при открытии файла в Syncplay.", # (Inverted)
"nostore-tooltip" : u"Запустить Syncplay с данной конфигурацией, но не сохранять изменения навсегда.", # (Inverted)
"nostore-tooltip" : u"Запустить Syncplay с данной конфигурацией, но не сохранять изменения навсегда.",
"rewindondesync-tooltip" : u"Перематывать назад, когда это необходимо для синхронизации. Отключение этой опции может привести к большим рассинхронизациям!",
"fastforwardondesync-tooltip" : u"Перематывать вперед при рассинхронизации с оператором комнаты (или если включена опция 'Никогда не замедлять и не перематывать видео другим').",
"showosd-tooltip" : u"Отправлять сообщения Syncplay в видеопроигрыватель и отображать их поверх видео (OSD - On Screen Display).",
@ -323,6 +337,7 @@ ru = {
"unpause-ifalreadyready-tooltip" : u"If you press unpause when not ready it will set you as ready - press unpause again to unpause.", # TODO: Translate into Russian
"unpause-ifothersready-tooltip" : u"If you press unpause when not ready, it will only upause if others are ready.", # TODO: Translate into Russian
"unpause-ifminusersready-tooltip" : u"If you press unpause when not ready, it will only upause if others are ready and minimum users threshold is met.", # TODO: Translate into Russian
"trusteddomains-arguments-tooltip" : u"Domains that it is okay for Syncplay to automatically switch to when shared playlists is enabled.", # TODO: Translate into Russian
"help-tooltip" : u"Открыть Руководство Пользователя на Syncplay.pl.",
"reset-tooltip" : u"Сбрасывает все настройки Syncplay в начальное состояние.",
@ -370,4 +385,26 @@ ru = {
"wrong-password-server-error" : u"Указан неверный пароль.",
"hello-server-error" : u"Не хватает аргументов Hello.",
}
# Playlists TODO: Translate all this to Russian
"playlist-selection-changed-notification" : u"{} changed the playlist selection", # Username
"playlist-contents-changed-notification" : u"{} updated the playlist", # Username
"cannot-find-file-for-playlist-switch-error" : u"Could not find file {} in media directories for playlist switch!", # Filename
"cannot-add-duplicate-error" : u"Could not add second entry for '{}' to the playlist as no duplicates are allowed.", #Filename
"cannot-add-unsafe-path-error" : u"Could not automatically load {} because it is not on a trusted domain. You can switch to the URL manually by double clicking it in the playlist, and add trusted domains via File->Advanced->Set Trusted Domains.", # Filename # TODO: Translate
"sharedplaylistenabled-label" : u"Enable shared playlists",
"removefromplaylist-menu-label" : u"Remove from playlist",
"shuffleplaylist-menuu-label" : u"Shuffle playlist",
"undoplaylist-menu-label" : u"Undo last change to playlist",
"addfilestoplaylist-menu-label" : u"Add file(s) to bottom of playlist",
"addurlstoplaylist-menu-label" : u"Add URL(s) to bottom of playlist",
"addusersfiletoplaylist-menu-label" : u"Add {} file to playlist", # item owner indicator
"addusersstreamstoplaylist-menu-label" : u"Add {} stream to playlist", # item owner indicator
"openusersstream-menu-label" : u"Open {} stream", # [username]'s
"openusersfile-menu-label" : u"Open {} file", # [username]'s
"item-is-yours-indicator" : u"your", # Goes with addusersfiletoplaylist/addusersstreamstoplaylist
"item-is-others-indicator" : u"{}'s", # username - goes with addusersfiletoplaylist/addusersstreamstoplaylist
"playlist-instruction-item-message" : u"Drag file here to add it to the shared playlist.",
"sharedplaylistenabled-tooltip" : u"Room operators can add files to a synced playlist to make it easy for everyone to watching the same thing. Configure media directories under 'Misc'.",
}

View File

@ -103,7 +103,7 @@ class MpcHcApi:
elif cmd == self.CMD_STATE:
self.loadState = int(value)
fileNotReady = self.loadState == self.__MPC_LOADSTATE.MLS_CLOSING or self.loadState == self.__MPC_LOADSTATE.MLS_LOADING
fileNotReady = self.loadState == self.__MPC_LOADSTATE.MLS_CLOSING or self.loadState == self.__MPC_LOADSTATE.MLS_LOADING or self.loadState == self.__MPC_LOADSTATE.MLS_CLOSED
if fileNotReady:
self.playState = None
self.__locks.fileReady.clear()
@ -419,8 +419,8 @@ class MPCHCAPIPlayer(BasePlayer):
self.__positionUpdate.wait(constants.MPC_LOCK_WAIT_TIME)
return self._mpcApi.lastFilePosition
@retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1)
def askForStatus(self):
try:
if self._mpcApi.filePlaying and self.__preventAsking.wait(0) and self.__fileUpdate.acquire(0):
self.__fileUpdate.release()
position = self.__getPosition()
@ -429,7 +429,9 @@ class MPCHCAPIPlayer(BasePlayer):
if self.__preventAsking.wait(0) and self.__fileUpdate.acquire(0):
self.__client.updatePlayerStatus(paused, position)
self.__fileUpdate.release()
return
else:
self.__echoGlobalStatus()
except MpcHcApi.PlayerNotReadyException:
self.__echoGlobalStatus()
def __echoGlobalStatus(self):

View File

@ -88,7 +88,7 @@ class MplayerPlayer(BasePlayer):
self._listener.sendLine("get_property {}".format(property_))
def displayMessage(self, message, duration=(constants.OSD_DURATION * 1000), secondaryOSD=False):
self._listener.sendLine(u'{} "{!s}" {} {}'.format(self.OSD_QUERY, message, duration, constants.MPLAYER_OSD_LEVEL).encode('utf-8'))
self._listener.sendLine(u'{} "{!s}" {} {}'.format(self.OSD_QUERY, self._stripNewlines(message), duration, constants.MPLAYER_OSD_LEVEL).encode('utf-8'))
def setSpeed(self, value):
self._setProperty('speed', "{:.2f}".format(value))
@ -105,7 +105,7 @@ class MplayerPlayer(BasePlayer):
self.setPosition(self._client.getGlobalPosition())
def setPosition(self, value):
self._position = value
self._position = max(value,0)
self._setProperty(self.POSITION_QUERY, "{}".format(value))
time.sleep(0.03)
@ -129,10 +129,17 @@ class MplayerPlayer(BasePlayer):
def _getPosition(self):
self._getProperty(self.POSITION_QUERY)
def _stripNewlines(self, text):
text = text.replace("\r", "")
text = text.replace("\n", "")
return text
def _quoteArg(self, arg):
arg = arg.replace('\\', '\\\\')
arg = arg.replace("'", "\\'")
arg = arg.replace('"', '\\"')
arg = arg.replace("\r", "")
arg = arg.replace("\n", "")
return u'"{}"'.format(arg)
def _fileIsLoaded(self):
@ -142,7 +149,7 @@ class MplayerPlayer(BasePlayer):
pass
def _storePosition(self, value):
self._position = value
self._position = max(value,0)
def _storePauseState(self, value):
self._paused = value
@ -216,7 +223,7 @@ class MplayerPlayer(BasePlayer):
@staticmethod
def isValidPlayerPath(path):
if "mplayer" in path and MplayerPlayer.getExpandedPath(path):
if "mplayer" in path and MplayerPlayer.getExpandedPath(path) and not "mplayerc.exe" in path: # "mplayerc.exe" is Media Player Classic (not Home Cinema):
return True
return False
@ -259,6 +266,10 @@ class MplayerPlayer(BasePlayer):
class __Listener(threading.Thread):
def __init__(self, playerController, playerPath, filePath, args):
self.sendQueue = []
self.readyToSend = True
self.lastSendTime = None
self.lastNotReadyTime = None
self.__playerController = playerController
if self.__playerController.getPlayerPathErrors(playerPath,filePath):
raise ValueError()
@ -315,7 +326,70 @@ class MplayerPlayer(BasePlayer):
self.__playerController.lineReceived(line)
self.__playerController.drop()
def sendLine(self, line):
def isReadyForSend(self):
self.checkForReadinessOverride()
return self.readyToSend
def setReadyToSend(self, newReadyState):
oldState = self.readyToSend
self.readyToSend = newReadyState
self.lastNotReadyTime = time.time() if newReadyState == False else None
if self.readyToSend == True:
self.__playerController._client.ui.showDebugMessage("<mpv> Ready to send: True")
else:
self.__playerController._client.ui.showDebugMessage("<mpv> Ready to send: False")
if self.readyToSend == True and oldState == False:
self.processSendQueue()
def checkForReadinessOverride(self):
if self.lastNotReadyTime and time.time() - self.lastNotReadyTime > constants.MPV_MAX_NEWFILE_COOLDOWN_TIME:
self.setReadyToSend(True)
def sendLine(self, line, notReadyAfterThis=None):
self.checkForReadinessOverride()
if self.readyToSend == False and "print_text ANS_pause" in line:
self.__playerController._client.ui.showDebugMessage("<mpv> Not ready to get status update, so skipping")
return
try:
if self.sendQueue:
if constants.MPV_SUPERSEDE_IF_DUPLICATE_COMMANDS:
for command in constants.MPV_SUPERSEDE_IF_DUPLICATE_COMMANDS:
if line.startswith(command):
for itemID, deletionCandidate in enumerate(self.sendQueue):
if deletionCandidate.startswith(command):
self.__playerController._client.ui.showDebugMessage(u"<mpv> Remove duplicate (supersede): {}".format(self.sendQueue[itemID]))
self.sendQueue.remove(self.sendQueue[itemID])
break
break
if constants.MPV_REMOVE_BOTH_IF_DUPLICATE_COMMANDS:
for command in constants.MPV_REMOVE_BOTH_IF_DUPLICATE_COMMANDS:
if line == command:
for itemID, deletionCandidate in enumerate(self.sendQueue):
if deletionCandidate == command:
self.__playerController._client.ui.showDebugMessage(u"<mpv> Remove duplicate (delete both): {}".format(self.sendQueue[itemID]))
self.__playerController._client.ui.showDebugMessage(self.sendQueue[itemID])
return
except:
self.__playerController._client.ui.showDebugMessage("<mpv> Problem removing duplicates, etc")
self.sendQueue.append(line)
self.processSendQueue()
if notReadyAfterThis:
self.setReadyToSend(False)
def processSendQueue(self):
while self.sendQueue and self.readyToSend:
if self.lastSendTime and time.time() - self.lastSendTime < constants.MPV_SENDMESSAGE_COOLDOWN_TIME:
self.__playerController._client.ui.showDebugMessage("<mpv> Throttling message send, so sleeping for {}".format(constants.MPV_SENDMESSAGE_COOLDOWN_TIME))
time.sleep(constants.MPV_SENDMESSAGE_COOLDOWN_TIME)
try:
lineToSend = self.sendQueue.pop()
if lineToSend:
self.lastSendTime = time.time()
self.actuallySendLine(lineToSend)
except IndexError:
pass
def actuallySendLine(self, line):
try:
if not isinstance(line, unicode):
line = line.decode('utf8')

View File

@ -3,6 +3,7 @@ import subprocess
from syncplay.players.mplayer import MplayerPlayer
from syncplay.messages import getMessage
from syncplay import constants
from syncplay.utils import isURL
import os, sys, time
class MpvPlayer(MplayerPlayer):
@ -75,14 +76,14 @@ class OldMpvPlayer(MpvPlayer):
OSD_QUERY = 'show_text'
def _setProperty(self, property_, value):
self._listener.sendLine("no-osd set {} {}".format(property_, value))
self._listener.sendLine(u"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):
def mpvErrorCheck(self, line):
if "Error parsing option" in line or "Error parsing commandline option" in line:
self.quitReason = getMessage("mpv-version-error")
@ -90,11 +91,11 @@ class OldMpvPlayer(MpvPlayer):
self.reactor.callFromThread(self._client.ui.showErrorMessage, getMessage("mpv-version-error"), True)
self.drop()
elif "[ytdl_hook] Your version of youtube-dl is too old" in line:
if constants and any(errormsg in line for errormsg in constants.MPV_ERROR_MESSAGES_TO_REPEAT):
self._client.ui.showErrorMessage(line)
def _handleUnknownLine(self, line):
self.mpvVersionErrorCheck(line)
self.mpvErrorCheck(line)
if "Playing: " in line:
newpath = line[9:]
oldpath = self._filepath
@ -129,7 +130,12 @@ class NewMpvPlayer(OldMpvPlayer):
if self.lastMPVPositionUpdate is None:
return self._client.getGlobalPosition()
if self._recentlyReset:
return self._position
diff = time.time() - self.lastMPVPositionUpdate
if diff > constants.MPV_UNRESPONSIVE_THRESHOLD:
self.reactor.callFromThread(self._client.ui.showErrorMessage, getMessage("mpv-unresponsive-error").format(int(diff)), True)
self.drop()
@ -144,7 +150,7 @@ class NewMpvPlayer(OldMpvPlayer):
if self._recentlyReset():
self._position = 0
elif self._fileIsLoaded():
self._position = value
self._position = max(value,0)
else:
self._position = self._client.getGlobalPosition()
@ -157,12 +163,18 @@ class NewMpvPlayer(OldMpvPlayer):
def askForStatus(self):
self._positionAsk.clear()
self._pausedAsk.clear()
self._getPaused()
self._getPosition()
if not self._listener.isReadyForSend:
self._client.ui.showDebugMessage("mpv not ready for update")
return
self._getPausedAndPosition()
self._positionAsk.wait(constants.MPV_LOCK_WAIT_TIME)
self._pausedAsk.wait(constants.MPV_LOCK_WAIT_TIME)
self._client.updatePlayerStatus(self._paused if self.fileLoaded else self._client.getGlobalPaused(), self.getCalculatedPosition())
def _getPausedAndPosition(self):
self._listener.sendLine(u"print_text ANS_pause=${pause}\r\nprint_text ANS_time-pos=${=time-pos}")
def _preparePlayer(self):
if self.delayedFilePath:
self.openFile(self.delayedFilePath)
@ -175,7 +187,7 @@ class NewMpvPlayer(OldMpvPlayer):
def _loadFile(self, filePath):
self._clearFileLoaded()
self._listener.sendLine(u'loadfile {}'.format(self._quoteArg(filePath)))
self._listener.sendLine(u'loadfile {}'.format(self._quoteArg(filePath)), notReadyAfterThis=True)
def setPosition(self, value):
super(self.__class__, self).setPosition(value)
@ -184,19 +196,29 @@ class NewMpvPlayer(OldMpvPlayer):
def openFile(self, filePath, resetPosition=False):
if resetPosition:
self.lastResetTime = time.time()
if isURL(filePath):
self.lastResetTime += constants.STREAM_ADDITIONAL_IGNORE_TIME
self._loadFile(filePath)
if self._paused != self._client.getGlobalPaused():
self.setPaused(self._client.getGlobalPaused())
if resetPosition == False:
self.setPosition(self._client.getGlobalPosition())
else:
self._storePosition(0)
def _handleUnknownLine(self, line):
self.mpvVersionErrorCheck(line)
self.mpvErrorCheck(line)
if line == "<SyncplayUpdateFile>" or "Playing:" in line:
self._listener.setReadyToSend(False)
self._clearFileLoaded()
elif line == "</SyncplayUpdateFile>":
self._onFileUpdate()
self._listener.setReadyToSend(True)
elif "Failed" in line or "failed" in line or "No video or audio streams selected" in line or "error" in line:
self._listener.setReadyToSend(True)
def _recentlyReset(self):
if not self.lastResetTime:

View File

@ -22,7 +22,6 @@ class VlcPlayer(BasePlayer):
SLAVE_ARGS = constants.VLC_SLAVE_ARGS
if not sys.platform.startswith('darwin'):
SLAVE_ARGS.extend(constants.VLC_SLAVE_NONOSX_ARGS)
random.seed()
vlcport = random.randrange(constants.VLC_MIN_PORT, constants.VLC_MAX_PORT) if (constants.VLC_MIN_PORT < constants.VLC_MAX_PORT) else constants.VLC_MIN_PORT
def __init__(self, client, playerPath, filePath, args):
@ -141,6 +140,10 @@ class VlcPlayer(BasePlayer):
return fileURL
def openFile(self, filePath, resetPosition=False):
if not utils.isURL(filePath):
normedPath = os.path.normpath(filePath)
if os.path.isfile(normedPath):
filePath = normedPath
if utils.isASCII(filePath):
self._listener.sendLine('load-file: {}'.format(filePath.encode('ascii', 'ignore')))
else:

View File

@ -140,6 +140,10 @@ class SyncClientProtocol(JSONCommandProtocol):
user, isReady = values["username"], values["isReady"]
manuallyInitiated = values["manuallyInitiated"] if values.has_key("manuallyInitiated") else True
self._client.setReady(user, isReady, manuallyInitiated)
elif command == "playlistIndex":
self._client.playlist.changeToPlaylistIndex(values['index'], values['user'])
elif command == "playlistChange":
self._client.playlist.changePlaylist(values['files'], values['user'])
def sendSet(self, setting):
self.sendMessage({"Set": setting})
@ -247,6 +251,21 @@ class SyncClientProtocol(JSONCommandProtocol):
}
})
def setPlaylist(self, files):
self.sendSet({
"playlistChange": {
"files": files
}
})
def setPlaylistIndex(self, index):
self.sendSet({
"playlistIndex": {
"index": index
}
})
def handleError(self, error):
self.dropWithError(error["message"])
@ -358,6 +377,10 @@ class SyncServerProtocol(JSONCommandProtocol):
elif command == "ready":
manuallyInitiated = set_[1]['manuallyInitiated'] if set_[1].has_key("manuallyInitiated") else False
self._factory.setReady(self._watcher, set_[1]['isReady'], manuallyInitiated=manuallyInitiated)
elif command == "playlistChange":
self._factory.setPlaylist(self._watcher, set_[1]['files'])
elif command == "playlistIndex":
self._factory.setPlaylistIndex(self._watcher, set_[1]['index'])
def sendSet(self, setting):
self.sendMessage({"Set": setting})
@ -389,6 +412,22 @@ class SyncServerProtocol(JSONCommandProtocol):
}
})
def setPlaylist(self, username, files):
self.sendSet({
"playlistChange": {
"user": username,
"files": files
}
})
def setPlaylistIndex(self, username, index):
self.sendSet({
"playlistIndex": {
"user": username,
"index": index
}
})
def sendUserSetting(self, username, room, file_, event):
room = {"name": room.getName()}
user = {username: {}}

View File

@ -72,8 +72,13 @@ class SyncFactory(Factory):
self.sendJoinMessage(watcher)
else:
self.sendRoomSwitchMessage(watcher)
room = watcher.getRoom()
roomSetByName = room.getSetBy().getName() if room.getSetBy() else None
watcher.setPlaylist(roomSetByName, room.getPlaylist())
watcher.setPlaylistIndex(roomSetByName, room.getPlaylistIndex())
if RoomPasswordProvider.isControlledRoom(roomName):
for controller in watcher.getRoom().getControllers():
for controller in room.getControllers():
watcher.sendControlledRoomAuthStatus(True, controller, roomName)
def sendRoomSwitchMessage(self, watcher):
@ -132,6 +137,24 @@ class SyncFactory(Factory):
watcher.setReady(isReady)
self._roomManager.broadcastRoom(watcher, lambda w: w.sendSetReady(watcher.getName(), watcher.isReady(), manuallyInitiated))
def setPlaylist(self, watcher, files):
room = watcher.getRoom()
if room.canControl(watcher):
watcher.getRoom().setPlaylist(files, watcher)
self._roomManager.broadcastRoom(watcher, lambda w: w.setPlaylist(watcher.getName(), files))
else:
watcher.setPlaylist(room.getName(), room.getPlaylist())
watcher.setPlaylistIndex(room.getName(), room.getPlaylistIndex())
def setPlaylistIndex(self, watcher, index):
room = watcher.getRoom()
if room.canControl(watcher):
watcher.getRoom().setPlaylistIndex(index, watcher)
self._roomManager.broadcastRoom(watcher, lambda w: w.setPlaylistIndex(watcher.getName(), index))
else:
watcher.setPlaylistIndex(room.getName(), room.getPlaylistIndex())
class RoomManager(object):
def __init__(self):
self._rooms = {}
@ -214,6 +237,10 @@ class Room(object):
self._watchers = {}
self._playState = self.STATE_PAUSED
self._setBy = None
self._playlist = []
self._playlistIndex = None
self.__lastUpdate = time.time()
self.__position = 0
def __str__(self, *args, **kwargs):
return self.getName()
@ -222,10 +249,15 @@ class Room(object):
return self._name
def getPosition(self):
if self._watchers:
age = time.time() - self.__lastUpdate
if self._watchers and age > 1:
watcher = min(self._watchers.values())
self._setBy = watcher
return watcher.getPosition()
self.__position = watcher.getPosition()
self.__lastUpdate = time.time()
return self.__position
elif self.__position is not None:
return self.__position + (age if self._playState == self.STATE_PLAYING else 0)
else:
return 0
@ -234,6 +266,7 @@ class Room(object):
self._setBy = setBy
def setPosition(self, position, setBy=None):
self.__position = position
for watcher in self._watchers.itervalues():
watcher.setPosition(position)
self._setBy = setBy
@ -258,6 +291,8 @@ class Room(object):
return
del self._watchers[watcher.getName()]
watcher.setRoom(None)
if not self._watchers:
self.__position = 0
def isEmpty(self):
return not bool(self._watchers)
@ -268,16 +303,33 @@ class Room(object):
def canControl(self, watcher):
return True
def setPlaylist(self, files, setBy=None):
self._playlist = files
def setPlaylistIndex(self, index, setBy=None):
self._playlistIndex = index
def getPlaylist(self):
return self._playlist
def getPlaylistIndex(self):
return self._playlistIndex
class ControlledRoom(Room):
def __init__(self, name):
Room.__init__(self, name)
self._controllers = {}
def getPosition(self):
if self._controllers:
age = time.time() - self.__lastUpdate
if self._controllers and age > 1:
watcher = min(self._controllers.values())
self._setBy = watcher
return watcher.getPosition()
self.__position = watcher.getPosition()
self.__lastUpdate = time.time()
return self.__position
elif self.__position is not None:
return self.__position + (age if self._playState == self.STATE_PLAYING else 0)
else:
return 0
@ -297,6 +349,14 @@ class ControlledRoom(Room):
if self.canControl(setBy):
Room.setPosition(self, position, setBy)
def setPlaylist(self, files, setBy=None):
if self.canControl(setBy):
self._playlist = files
def setPlaylistIndex(self, index, setBy=None):
if self.canControl(setBy):
self._playlistIndex = index
def canControl(self, watcher):
return watcher.getName() in self._controllers
@ -370,6 +430,12 @@ class Watcher(object):
def sendSetReady(self, username, isReady, manuallyInitiated=True):
self._connector.sendSetReady(username, isReady, manuallyInitiated)
def setPlaylistIndex(self, username, index):
self._connector.setPlaylistIndex(username, index)
def setPlaylist(self, username, files):
self._connector.setPlaylist(username, files)
def __lt__(self, b):
if self.getPosition() is None or self._file is None:
return False

View File

@ -34,6 +34,11 @@ class ConfigurationGetter(object):
"playerPath": None,
"perPlayerArguments": None,
"mediaSearchDirectories": None,
"sharedPlaylistEnabled": True,
"loopAtEndOfPlaylist": False,
"loopSingleFiles" : False,
"onlySwitchToTrustedDomains": True,
"trustedDomains": constants.DEFAULT_TRUSTED_DOMAINS,
"file": None,
"playerArgs": [],
"playerClass": None,
@ -51,6 +56,7 @@ class ConfigurationGetter(object):
"unpauseAction": constants.UNPAUSE_IFOTHERSREADY_MODE,
"autoplayInitialState" : None,
"autoplayMinUsers" : -1,
"autoplayRequireSameFilenames": True,
"clearGUIData": False,
"language" : "",
"checkForUpdatesAutomatically" : None,
@ -87,6 +93,7 @@ class ConfigurationGetter(object):
"dontSlowDownWithMe",
"pauseOnLeave",
"readyAtStart",
"autoplayRequireSameFilenames",
"clearGUIData",
"rewindOnDesync",
"slowOnDesync",
@ -100,7 +107,11 @@ class ConfigurationGetter(object):
"showDifferentRoomOSD",
"showSameRoomOSD",
"showNonControllerOSD",
"showDurationNotification"
"showDurationNotification",
"sharedPlaylistEnabled",
"loopAtEndOfPlaylist",
"loopSingleFiles",
"onlySwitchToTrustedDomains"
]
self._tristate = [
"checkForUpdatesAutomatically",
@ -110,6 +121,7 @@ class ConfigurationGetter(object):
self._serialised = [
"perPlayerArguments",
"mediaSearchDirectories",
"trustedDomains",
]
self._numeric = [
@ -121,9 +133,23 @@ class ConfigurationGetter(object):
self._iniStructure = {
"server_data": ["host", "port", "password"],
"client_settings": ["name", "room", "playerPath", "perPlayerArguments", "slowdownThreshold", "rewindThreshold", "fastforwardThreshold", "slowOnDesync", "rewindOnDesync", "fastforwardOnDesync", "dontSlowDownWithMe", "forceGuiPrompt", "filenamePrivacyMode", "filesizePrivacyMode", "unpauseAction", "pauseOnLeave", "readyAtStart", "autoplayMinUsers", "autoplayInitialState", "mediaSearchDirectories"],
"gui": ["showOSD", "showOSDWarnings", "showSlowdownOSD", "showDifferentRoomOSD", "showSameRoomOSD", "showNonControllerOSD", "showDurationNotification"],
"general": ["language", "checkForUpdatesAutomatically", "lastCheckedForUpdates"]
"client_settings": ["name", "room", "playerPath",
"perPlayerArguments", "slowdownThreshold",
"rewindThreshold", "fastforwardThreshold",
"slowOnDesync", "rewindOnDesync",
"fastforwardOnDesync", "dontSlowDownWithMe",
"forceGuiPrompt", "filenamePrivacyMode",
"filesizePrivacyMode", "unpauseAction",
"pauseOnLeave", "readyAtStart", "autoplayMinUsers",
"autoplayInitialState", "mediaSearchDirectories",
"sharedPlaylistEnabled", "loopAtEndOfPlaylist",
"loopSingleFiles",
"onlySwitchToTrustedDomains", "trustedDomains"],
"gui": ["showOSD", "showOSDWarnings", "showSlowdownOSD",
"showDifferentRoomOSD", "showSameRoomOSD",
"showNonControllerOSD", "showDurationNotification"],
"general": ["language", "checkForUpdatesAutomatically",
"lastCheckedForUpdates"]
}
self._playerFactory = PlayerFactory()
@ -352,7 +378,7 @@ class ConfigurationGetter(object):
for name in constants.CONFIG_NAMES:
path = location + os.path.sep + name
if os.path.isfile(path) and (os.name == 'nt' or path != os.path.join(os.getenv('HOME', '.'), constants.DEFAULT_CONFIG_NAME_LINUX)):
loadedPaths.append("'" + os.path.normpath(path) + "'")
loadedPaths.append(u"'{}'".format(os.path.normpath(path)))
self._parseConfigFile(path, createConfig=False)
self._checkConfig()
return loadedPaths
@ -409,6 +435,15 @@ class ConfigurationGetter(object):
qt4reactor.install()
return self._config
def setConfigOption(self, option, value):
path = self._getConfigurationFilePath()
backup = self._config.copy()
self._parseConfigFile(path)
self._config[option] = value
backup[option] = value
self._saveConfig(path)
self._config = backup
class SafeConfigParserUnicode(SafeConfigParser):
def write(self, fp):
"""Write an .ini-format representation of the configuration state."""

View File

@ -71,7 +71,7 @@ class ConfigDialog(QtGui.QDialog):
moreToggling = False
def automaticUpdatePromptCheck(self):
if self.automaticupdatesCheckbox.checkState() == Qt.PartiallyChecked and not self.nostoreCheckbox.isChecked():
if self.automaticupdatesCheckbox.checkState() == Qt.PartiallyChecked:
reply = QtGui.QMessageBox.question(self, "Syncplay",
getMessage("promptforupdate-label"), QtGui.QMessageBox.StandardButton.Yes | QtGui.QMessageBox.StandardButton.No)
if reply == QtGui.QMessageBox.Yes:
@ -87,9 +87,9 @@ class ConfigDialog(QtGui.QDialog):
if self.showmoreCheckbox.isChecked():
self.tabListFrame.show()
self.resetButton.show()
self.nostoreCheckbox.show()
self.playerargsTextbox.show()
self.playerargsLabel.show()
self.runButton.show()
self.saveMoreState(True)
self.tabListWidget.setCurrentRow(0)
self.ensureTabListIsVisible()
@ -97,9 +97,9 @@ class ConfigDialog(QtGui.QDialog):
else:
self.tabListFrame.hide()
self.resetButton.hide()
self.nostoreCheckbox.hide()
self.playerargsTextbox.hide()
self.playerargsLabel.hide()
self.runButton.hide()
self.saveMoreState(False)
self.stackedLayout.setCurrentIndex(0)
newHeight = self.connectionSettingsGroup.minimumSizeHint().height()+self.mediaplayerSettingsGroup.minimumSizeHint().height()+self.bottomButtonFrame.minimumSizeHint().height()+3
@ -112,12 +112,6 @@ class ConfigDialog(QtGui.QDialog):
self.setFixedWidth(self.minimumSizeHint().width())
self.executablepathCombobox.setFixedWidth(self.mediapathTextbox.width())
def runButtonTextUpdate(self):
if self.nostoreCheckbox.isChecked():
self.runButton.setText(getMessage("run-label"))
else:
self.runButton.setText(getMessage("storeandrun-label"))
def openHelp(self):
self.QtGui.QDesktopServices.openUrl(QUrl("http://syncplay.pl/guide/client/"))
@ -365,16 +359,23 @@ class ConfigDialog(QtGui.QDialog):
self.mediadirectory = os.path.dirname(fileName)
self.saveMediaBrowseSettings()
def _saveDataAndLeave(self):
def _runWithoutStoringConfig(self):
self._saveDataAndLeave(storeConfiguration=False)
def _saveDataAndLeave(self, storeConfiguration=True):
self.config['noStore'] = storeConfiguration
if storeConfiguration:
self.automaticUpdatePromptCheck()
self.loadLastUpdateCheckDate()
self.config["perPlayerArguments"] = self.perPlayerArgs
self.config["mediaSearchDirectories"] = utils.convertMultilineStringToList(self.mediasearchTextEdit.toPlainText())
self.config["trustedDomains"] = utils.convertMultilineStringToList(self.trusteddomainsTextEdit.toPlainText())
self.processWidget(self, lambda w: self.saveValues(w))
if self.hostCombobox.currentText():
self.config['host'] = self.hostCombobox.currentText() if ":" in self.hostCombobox.currentText() else self.hostCombobox.currentText() + ":" + unicode(constants.DEFAULT_PORT)
self.config['host'] = self.config['host'].replace(" ","").replace("\t", "").replace("\n","").replace("\r","")
else:
self.config['host'] = None
self.config['playerPath'] = unicode(self.safenormcaseandpath(self.executablepathCombobox.currentText()))
@ -504,6 +505,7 @@ class ConfigDialog(QtGui.QDialog):
self.perPlayerArgs = self.config["perPlayerArguments"]
self.mediaSearchDirectories = self.config["mediaSearchDirectories"]
self.trustedDomains = self.config["trustedDomains"]
self.connectionSettingsGroup = QtGui.QGroupBox(getMessage("connection-group-title"))
self.loadSavedPublicServerList()
@ -794,10 +796,24 @@ class ConfigDialog(QtGui.QDialog):
self.othersyncSettingsLayout.setAlignment(Qt.AlignLeft)
self.othersyncSettingsLayout.addWidget(self.fastforwardCheckbox, 3, 0,1,2, Qt.AlignLeft)
## Trusted domains
self.trusteddomainsSettingsGroup = QtGui.QGroupBox(getMessage("syncplay-trusteddomains-title"))
self.trusteddomainsSettingsLayout = QtGui.QVBoxLayout()
self.trusteddomainsSettingsGroup.setLayout(self.trusteddomainsSettingsLayout)
self.trusteddomainsTextEdit = QPlainTextEdit(utils.getListAsMultilineString(self.trustedDomains))
self.trusteddomainsTextEdit.setObjectName(constants.LOAD_SAVE_MANUALLY_MARKER + "trusteddomains-arguments")
self.trusteddomainsTextEdit.setLineWrapMode(QtGui.QPlainTextEdit.NoWrap)
self.trusteddomainsSettingsLayout.addWidget(self.trusteddomainsTextEdit)
self.trusteddomainsSettingsGroup.setMaximumHeight(self.trusteddomainsSettingsGroup.minimumSizeHint().height())
self.othersyncSettingsGroup.setLayout(self.othersyncSettingsLayout)
self.othersyncSettingsGroup.setMaximumHeight(self.othersyncSettingsGroup.minimumSizeHint().height())
self.syncSettingsLayout.addWidget(self.othersyncSettingsGroup)
self.syncSettingsLayout.addWidget(self.desyncSettingsGroup)
self.syncSettingsLayout.addWidget(self.trusteddomainsSettingsGroup)
self.syncSettingsFrame.setLayout(self.syncSettingsLayout)
self.desyncSettingsGroup.setMaximumHeight(self.desyncSettingsGroup.minimumSizeHint().height())
self.syncSettingsLayout.setAlignment(Qt.AlignTop)
@ -819,27 +835,27 @@ class ConfigDialog(QtGui.QDialog):
self.showSameRoomOSDCheckbox = QCheckBox(getMessage("showsameroomosd-label"))
self.showSameRoomOSDCheckbox.setObjectName("showSameRoomOSD")
self.showSameRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.showSameRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.showSameRoomOSDCheckbox)
self.showNonControllerOSDCheckbox = QCheckBox(getMessage("shownoncontrollerosd-label"))
self.showNonControllerOSDCheckbox.setObjectName("showNonControllerOSD")
self.showNonControllerOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.showNonControllerOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.showNonControllerOSDCheckbox)
self.showDifferentRoomOSDCheckbox = QCheckBox(getMessage("showdifferentroomosd-label"))
self.showDifferentRoomOSDCheckbox.setObjectName("showDifferentRoomOSD")
self.showDifferentRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.showDifferentRoomOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.showDifferentRoomOSDCheckbox)
self.slowdownOSDCheckbox = QCheckBox(getMessage("showslowdownosd-label"))
self.slowdownOSDCheckbox.setObjectName("showSlowdownOSD")
self.slowdownOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.slowdownOSDCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.slowdownOSDCheckbox)
self.showOSDWarningsCheckbox = QCheckBox(getMessage("showosdwarnings-label"))
self.showOSDWarningsCheckbox.setObjectName("showOSDWarnings")
self.showOSDWarningsCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + "chevrons_right.png"))
self.showOSDWarningsCheckbox.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png"))
self.osdSettingsLayout.addWidget(self.showOSDWarningsCheckbox)
self.subitems['showOSD'] = ["showSameRoomOSD", "showDifferentRoomOSD", "showSlowdownOSD", "showOSDWarnings", "showNonControllerOSD"]
@ -899,24 +915,26 @@ class ConfigDialog(QtGui.QDialog):
self.bottomButtonFrame = QtGui.QFrame()
self.bottomButtonLayout = QtGui.QHBoxLayout()
self.helpButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + 'help.png'), getMessage("help-label"))
self.helpButton = QtGui.QPushButton(QtGui.QIcon(self.resourcespath + u'help.png'), getMessage("help-label"))
self.helpButton.setObjectName("help")
self.helpButton.setMaximumSize(self.helpButton.sizeHint())
self.helpButton.pressed.connect(self.openHelp)
self.resetButton = QtGui.QPushButton(QtGui.QIcon(resourcespath + 'cog_delete.png'),getMessage("reset-label"))
self.resetButton = QtGui.QPushButton(QtGui.QIcon(resourcespath + u'cog_delete.png'),getMessage("reset-label"))
self.resetButton.setMaximumSize(self.resetButton.sizeHint())
self.resetButton.setObjectName("reset")
self.resetButton.pressed.connect(self.resetSettings)
self.runButton = QtGui.QPushButton(QtGui.QIcon(resourcespath + 'accept.png'), getMessage("storeandrun-label"))
self.runButton = QtGui.QPushButton(QtGui.QIcon(resourcespath + u'accept.png'), getMessage("run-label"))
self.runButton.pressed.connect(self._saveDataAndLeave)
self.runButton.setToolTip(getMessage("nostore-tooltip"))
self.storeAndRunButton = QtGui.QPushButton(QtGui.QIcon(resourcespath + u'accept.png'), getMessage("storeandrun-label"))
self.storeAndRunButton.pressed.connect(self._runWithoutStoringConfig)
self.bottomButtonLayout.addWidget(self.helpButton)
self.bottomButtonLayout.addWidget(self.resetButton)
self.bottomButtonLayout.addWidget(self.runButton)
self.bottomButtonLayout.addWidget(self.storeAndRunButton)
self.bottomButtonFrame.setLayout(self.bottomButtonLayout)
if config['noStore'] == True:
self.runButton.setText(getMessage("run-label"))
self.bottomButtonLayout.setContentsMargins(5,0,5,0)
self.mainLayout.addWidget(self.bottomButtonFrame, 1, 0, 1, 2)
@ -925,12 +943,11 @@ class ConfigDialog(QtGui.QDialog):
self.bottomCheckboxLayout = QtGui.QGridLayout()
self.alwaysshowCheckbox = QCheckBox(getMessage("forceguiprompt-label"))
self.nostoreCheckbox = QCheckBox(getMessage("nostore-label"))
self.enableplaylistsCheckbox = QCheckBox(getMessage("sharedplaylistenabled-label"))
self.bottomCheckboxLayout.addWidget(self.showmoreCheckbox)
self.bottomCheckboxLayout.addWidget(self.nostoreCheckbox, 0, 2, Qt.AlignRight)
self.bottomCheckboxLayout.addWidget(self.enableplaylistsCheckbox, 0, 2, Qt.AlignRight)
self.alwaysshowCheckbox.setObjectName(constants.INVERTED_STATE_MARKER + "forceGuiPrompt")
self.nostoreCheckbox.setObjectName("noStore")
self.nostoreCheckbox.toggled.connect(self.runButtonTextUpdate)
self.enableplaylistsCheckbox.setObjectName("sharedPlaylistEnabled")
self.bottomCheckboxFrame.setLayout(self.bottomCheckboxLayout)
self.mainLayout.addWidget(self.bottomCheckboxFrame, 2, 0, 1, 2)
@ -939,11 +956,11 @@ class ConfigDialog(QtGui.QDialog):
self.tabListLayout = QtGui.QHBoxLayout()
self.tabListFrame = QtGui.QFrame()
self.tabListWidget = QtGui.QListWidget()
self.tabListWidget.addItem(QtGui.QListWidgetItem(QtGui.QIcon(self.resourcespath + "house.png"),getMessage("basics-label")))
self.tabListWidget.addItem(QtGui.QListWidgetItem(QtGui.QIcon(self.resourcespath + "control_pause_blue.png"),getMessage("readiness-label")))
self.tabListWidget.addItem(QtGui.QListWidgetItem(QtGui.QIcon(self.resourcespath + "film_link.png"),getMessage("sync-label")))
self.tabListWidget.addItem(QtGui.QListWidgetItem(QtGui.QIcon(self.resourcespath + "comments.png"),getMessage("messages-label")))
self.tabListWidget.addItem(QtGui.QListWidgetItem(QtGui.QIcon(self.resourcespath + "cog.png"),getMessage("misc-label")))
self.tabListWidget.addItem(QtGui.QListWidgetItem(QtGui.QIcon(self.resourcespath + u"house.png"),getMessage("basics-label")))
self.tabListWidget.addItem(QtGui.QListWidgetItem(QtGui.QIcon(self.resourcespath + u"control_pause_blue.png"),getMessage("readiness-label")))
self.tabListWidget.addItem(QtGui.QListWidgetItem(QtGui.QIcon(self.resourcespath + u"film_link.png"),getMessage("sync-label")))
self.tabListWidget.addItem(QtGui.QListWidgetItem(QtGui.QIcon(self.resourcespath + u"comments.png"),getMessage("messages-label")))
self.tabListWidget.addItem(QtGui.QListWidgetItem(QtGui.QIcon(self.resourcespath + u"cog.png"),getMessage("misc-label")))
self.tabListLayout.addWidget(self.tabListWidget)
self.tabListFrame.setLayout(self.tabListLayout)
self.tabListFrame.setFixedWidth(self.tabListFrame.minimumSizeHint().width())
@ -1029,15 +1046,15 @@ class ConfigDialog(QtGui.QDialog):
if sys.platform.startswith('win'):
resourcespath = utils.findWorkingDir() + "\\resources\\"
else:
resourcespath = utils.findWorkingDir() + "/resources/"
self.posixresourcespath = utils.findWorkingDir().replace("\\","/") + "/resources/"
resourcespath = utils.findWorkingDir() + u"/resources/"
self.posixresourcespath = utils.findWorkingDir().replace(u"\\","/") + u"/resources/"
self.resourcespath = resourcespath
super(ConfigDialog, self).__init__()
self.setWindowTitle(getMessage("config-window-title"))
self.setWindowFlags(self.windowFlags() & Qt.WindowCloseButtonHint & ~Qt.WindowContextHelpButtonHint)
self.setWindowIcon(QtGui.QIcon(resourcespath + "syncplay.png"))
self.setWindowIcon(QtGui.QIcon(resourcespath + u"syncplay.png"))
self.stackedLayout = QtGui.QStackedLayout()
self.stackedFrame = QtGui.QFrame()
@ -1059,10 +1076,10 @@ class ConfigDialog(QtGui.QDialog):
if self.getMoreState() == False:
self.tabListFrame.hide()
self.nostoreCheckbox.hide()
self.resetButton.hide()
self.playerargsTextbox.hide()
self.playerargsLabel.hide()
self.runButton.hide()
newHeight = self.connectionSettingsGroup.minimumSizeHint().height()+self.mediaplayerSettingsGroup.minimumSizeHint().height()+self.bottomButtonFrame.minimumSizeHint().height()+3
if self.error:
newHeight +=self.errorLabel.height()+3

View File

@ -23,6 +23,12 @@ class ConsoleUI(threading.Thread):
def drop(self):
pass
def setPlaylist(self, newPlaylist, newIndexFilename=None):
pass
def setPlaylistIndexFilename(self, filename):
pass
def run(self):
try:
while True:
@ -77,6 +83,9 @@ class ConsoleUI(threading.Thread):
def userListChange(self):
pass
def fileSwitchFoundFiles(self):
pass
def showMessage(self, message, noTimestamp=False):
message = message.encode(sys.stdout.encoding, 'replace')
if noTimestamp:

File diff suppressed because it is too large Load Diff