Merge branch 'master' into 'qtpy-pyside2'

This commit is contained in:
alby128 2018-01-19 13:40:09 +01:00
commit 1cc13a2c7d
25 changed files with 1874 additions and 230 deletions

View File

@ -47,6 +47,7 @@ common:
chmod 755 $(LIB_PATH)/syncplay/
cp -r resources/hicolor $(SHARE_PATH)/icons/
cp -r resources/*.png $(LIB_PATH)/syncplay/resources/
cp -r resources/*.lua $(LIB_PATH)/syncplay/resources/
cp -r resources/lua/intf/*.lua $(LIB_PATH)/syncplay/resources/lua/intf/
cp resources/hicolor/48x48/apps/syncplay.png $(SHARE_PATH)/app-install/icons/
cp resources/hicolor/48x48/apps/syncplay.png $(SHARE_PATH)/pixmaps/

View File

@ -1,6 +1,6 @@
# Syncplay
Solution to synchronize video playback across multiple instances of mplayer2, mpv, Media Player Classic (MPC-HC) and VLC over the Internet.
Solution to synchronize video playback across multiple instances of mpv, VLC, MPC-HC, MPC-BE and mplayer2 over the Internet.
## Official website
http://syncplay.pl
@ -12,13 +12,14 @@ http://syncplay.pl/download/
Syncplay synchronises the position and play state of multiple media players so that the viewers can watch the same thing at the same time.
This means that when one person pauses/unpauses playback or seeks (jumps position) within their media player then this will be replicated across all media players connected to the same server and in the same 'room' (viewing session).
When a new person joins they will also be synchronised.
When a new person joins they will also be synchronised. Syncplay also includes text-based chat so you can discuss a video as you watch it (or you could use third-party Voice over IP software to talk over a video).
## What it doesn't do
Syncplay does not use video streaming or file sharing so each user must have their own copy of the media to be played. Syncplay does not synchronise player configuration, audio/subtitle track choice, playback rate, volume or filters. Furthermore, users must manually choose what file to play as Syncplay does not synchronise which file is open. Finally, Syncplay does not provide a voice or text-based chat platform to allow for discussion during playback as Syncplay is intended to be used in conjunction with third-party communication solutions such as IRC and Mumble.
Syncplay is not a file sharing service.
## Authors
* *Concept and principal Syncplay developer* - Uriziel.
* *Other Syncplay coders* - daniel-123, Et0h.
* *Initial concept and core internals developer* - Uriziel.
* *GUI design and current lead developer* - Et0h.
* *Original SyncPlay code* - Tomasz Kowalczyk (Fluxid), who developed SyncPlay at https://github.com/fluxid/syncplay
* *Other contributors* - See http://syncplay.pl/about/development/

View File

@ -671,6 +671,8 @@ guiIcons = ['resources/accept.png', 'resources/arrow_undo.png', 'resources/clock
'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/arrow_switch.png',
'resources/film_go.png', 'resources/world_go.png', 'resources/arrow_refresh.png', 'resources/bullet_right_grey.png',
'resources/user_comment.png',
'resources/error.png',
'resources/film_folder_edit.png',
'resources/film_edit.png',
'resources/folder_film.png',
@ -679,7 +681,7 @@ guiIcons = ['resources/accept.png', 'resources/arrow_undo.png', 'resources/clock
'resources/email_go.png',
'resources/world_add.png', 'resources/film_add.png', 'resources/delete.png', 'resources/spinner.mng'
]
resources = ["resources/icon.ico", "resources/syncplay.png", "resources/license.rtf", "resources/third-party-notices.rtf"]
resources = ["resources/icon.ico", "resources/syncplay.png", "resources/syncplayintf.lua", "resources/license.rtf", "resources/third-party-notices.rtf"]
resources.extend(guiIcons)
intf_resources = ["resources/lua/intf/syncplay.lua"]

BIN
resources/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

979
resources/syncplayintf.lua Normal file
View File

@ -0,0 +1,979 @@
-- syncplayintf.lua -- An interface for communication between mpv and Syncplay
-- Author: Etoh, utilising repl.lua code by James Ross-Gowan (see below)
-- Thanks: RiCON, James Ross-Gowan, Argon-, wm4, uau
-- Includes code copied/adapted from repl.lua -- A graphical REPL for mpv input commands
--
-- c 2016, James Ross-Gowan
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-- See https://github.com/rossy/mpv-repl for a copy of repl.lua
local CANVAS_WIDTH = 1920
local CANVAS_HEIGHT = 1080
local ROW_HEIGHT = 100
local chat_format = "{\\fs50}{\an1}"
local max_scrolling_rows = 100
local MOVEMENT_PER_SECOND = 200
local TICK_INTERVAL = 0.01
local CHAT_MODE_CHATROOM = "Chatroom"
local CHAT_MODE_SUBTITLE = "Subtitle"
local CHAT_MODE_SCROLLING = "Scrolling"
local last_chat_time = 0
local use_alpha_rows_for_chat = true
local MOOD_NEUTRAL = 0
local MOOD_BAD = 1
local MOOD_GOOD = 2
local WORDWRAPIFY_MAGICWORD = "{\\\\fscx0} {\\\\fscx100}"
local SCROLLING_ADDITIONAL_BOTTOM_MARGIN = 75
local default_oscvisibility_state = "never"
local ALPHA_WARNING_TEXT_COLOUR = "FF00FF" -- RBG
local HINT_TEXT_COLOUR = "AAAAAA" -- RBG
local NEUTRAL_ALERT_TEXT_COLOUR = "FFFFFF" -- RBG
local BAD_ALERT_TEXT_COLOUR = "0000FF" -- RBG
local GOOD_ALERT_TEXT_COLOUR = "00FF00" -- RBG
local NOTIFICATION_TEXT_COLOUR = "FFFF00" -- RBG
local FONT_SIZE_MULTIPLIER = 2
local chat_log = {}
local assdraw = require "mp.assdraw"
local opt = require 'mp.options'
local repl_active = false
local insert_mode = false
local line = ''
local cursor = 1
local key_hints_enabled = false
function format_scrolling(xpos, ypos, text)
local chat_message = "\n"..chat_format .. "{\\pos("..xpos..","..ypos..")\\q2}"..text.."\\N\\n"
return string.format(chat_message)
end
function format_chatroom(text)
local chat_message = chat_format .. text .."\\N\\n"
return string.format(chat_message)
end
function clear_chat()
chat_log = {}
end
local alert_osd = ""
local last_alert_osd_time = nil
local alert_osd_mood = MOOD_NEUTRAL
local notification_osd = ""
local last_notification_osd_time = nil
local notification_osd_mood = MOOD_NEUTRAL
function set_alert_osd(osd_message, mood)
alert_osd = osd_message
last_alert_osd_time = mp.get_time()
alert_osd_mood = mood
end
function set_notification_osd(osd_message, mood)
notification_osd = osd_message
last_notification_osd_time = mp.get_time()
notification_osd_mood = mood
end
function add_chat(chat_message, mood)
last_chat_time = mp.get_time()
local entry = #chat_log+1
for i = 1, #chat_log do
if chat_log[i].text == '' then
entry = i
break
end
end
local row = ((entry-1) % max_scrolling_rows)+1
if opts['chatOutputMode'] == CHAT_MODE_CHATROOM then
if entry > opts['chatMaxLines'] then
table.remove(chat_log, 1)
entry = entry - 1
end
end
chat_log[entry] = { xpos=CANVAS_WIDTH, timecreated=mp.get_time(), text=tostring(chat_message), row=row }
end
function chat_update()
local ass = assdraw.ass_new()
local chat_ass = ''
local rowsAdded = 0
local to_add = ''
local incrementRow = 0
if opts['chatOutputMode'] == CHAT_MODE_CHATROOM and chat_log ~= {} then
local timedelta = mp.get_time() - last_chat_time
if timedelta >= opts['chatTimeout'] then
clear_chat()
end
end
rowsAdded,to_add = process_alert_osd()
if to_add ~= nil and to_add ~= "" then
chat_ass = to_add
end
incrementRow,to_add = process_notification_osd(rowsAdded)
rowsAdded = rowsAdded + incrementRow
if to_add ~= nil and to_add ~= "" then
chat_ass = chat_ass .. to_add
end
if #chat_log > 0 then
for i = 1, #chat_log do
local to_add = process_chat_item(i,rowsAdded)
if to_add ~= nil and to_add ~= "" then
chat_ass = chat_ass .. to_add
end
end
end
local xpos = opts['chatLeftMargin']
local ypos = opts['chatTopMargin']
chat_ass = "\n".."{\\pos("..xpos..","..ypos..")}".. chat_ass
if use_alpha_rows_for_chat == false and opts['chatDirectInput'] == true then
local alphawarning_ass = assdraw.ass_new()
alphawarning_ass = "{\\a6}{\\1c&H"..ALPHA_WARNING_TEXT_COLOUR.."}"..opts['alphakey-mode-warning-first-line'].."\n{\\a6}{\\1c&H"..ALPHA_WARNING_TEXT_COLOUR.."}"..opts['alphakey-mode-warning-second-line']
ass:append(alphawarning_ass)
elseif opts['chatOutputMode'] == CHAT_MODE_CHATROOM and opts['chatInputPosition'] == "Top" then
ass:append(chat_ass)
ass:append(input_ass())
else
ass:append(input_ass())
ass:append(chat_ass)
end
mp.set_osd_ass(CANVAS_WIDTH,CANVAS_HEIGHT, ass.text)
end
function process_alert_osd()
local rowsCreated = 0
local stringToAdd = ""
if alert_osd ~= "" and mp.get_time() - last_alert_osd_time < opts['alertTimeout'] and last_alert_osd_time ~= nil then
local messageColour
if alert_osd_mood == MOOD_NEUTRAL then
messageColour = "{\\1c&H"..NEUTRAL_ALERT_TEXT_COLOUR.."}"
elseif alert_osd_mood == MOOD_BAD then
messageColour = "{\\1c&H"..BAD_ALERT_TEXT_COLOUR.."}"
elseif alert_osd_mood == MOOD_GOOD then
messageColour = "{\\1c&H"..GOOD_ALERT_TEXT_COLOUR.."}"
end
local messageString = wordwrapify_string(alert_osd)
local startRow = 0
if messageString ~= '' and messageString ~= nil then
local toDisplay
rowsCreated = rowsCreated + 1
messageString = messageColour..messageString
if stringToAdd ~= "" then
stringToAdd = stringToAdd .. format_chatroom(messageString)
else
stringToAdd = format_chatroom(messageString)
end
end
end
return rowsCreated, stringToAdd
end
function process_notification_osd(startRow)
local rowsCreated = 0
local startRow = startRow
local stringToAdd = ""
if notification_osd ~= "" and mp.get_time() - last_notification_osd_time < opts['alertTimeout'] and last_notification_osd_time ~= nil then
local messageColour
messageColour = "{\\1c&H"..NOTIFICATION_TEXT_COLOUR.."}"
local messageString
messageString = wordwrapify_string(notification_osd)
messageString = messageColour..messageString
messageString = format_chatroom(messageString)
stringToAdd = messageString
rowsCreated = 1
end
return rowsCreated, stringToAdd
end
function process_chat_item(i, rowsAdded)
if opts['chatOutputMode'] == CHAT_MODE_CHATROOM then
return process_chat_item_chatroom(i, rowsAdded)
elseif opts['chatOutputMode'] == CHAT_MODE_SCROLLING then
return process_chat_item_scrolling(i)
end
end
function process_chat_item_scrolling(i)
local timecreated = chat_log[i].timecreated
local timedelta = mp.get_time() - timecreated
local xpos = CANVAS_WIDTH - (timedelta*MOVEMENT_PER_SECOND)
local text = chat_log[i].text
if text ~= '' then
local roughlen = string.len(text) * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER) * 1.5
if xpos > (-1*roughlen) then
local row = chat_log[i].row-1+opts['scrollingFirstRowOffset']
local ypos = opts['chatTopMargin']+(row * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER))
return format_scrolling(xpos,ypos,text)
else
chat_log[i].text = ''
end
end
end
function process_chat_item_chatroom(i, startRow)
local text = chat_log[i].text
if text ~= '' then
local text = wordwrapify_string(text)
local rowNumber = i+startRow-1
return(format_chatroom(text))
end
end
function process_chat_item_subtitle(i)
local timecreated = chat_log[i].timecreated
local timedelta = mp.get_time() - timecreated
local xpos = CANVAS_WIDTH - (timedelta*MOVEMENT_PER_SECOND)
local text = chat_log[i].text
if text ~= '' then
local roughlen = string.len(text) * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER)
if xpos > (-1*roughlen) then
local row = chat_log[i].row
local ypos = row * (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER)
return(format_scrolling(xpos,ypos,text))
else
chat_log[i].text = ''
end
end
end
chat_timer=mp.add_periodic_timer(TICK_INTERVAL, chat_update)
mp.register_script_message('chat', function(e)
add_chat(e)
end)
-- Chat OSD
mp.register_script_message('chat-osd-neutral', function(e)
add_chat(e,MOOD_NEUTRAL)
end)
mp.register_script_message('chat-osd-bad', function(e)
add_chat(e,MOOD_BAD)
end)
mp.register_script_message('chat-osd-good', function(e)
add_chat(e,MOOD_GOOD)
end)
-- Alert OSD
mp.register_script_message('alert-osd-neutral', function(e)
set_alert_osd(e,MOOD_NEUTRAL)
end)
mp.register_script_message('alert-osd-bad', function(e)
set_alert_osd(e,MOOD_BAD)
end)
mp.register_script_message('alert-osd-good', function(e)
set_alert_osd(e,MOOD_GOOD)
end)
-- Notification OSD
mp.register_script_message('notification-osd-neutral', function(e)
set_notification_osd(e,MOOD_NEUTRAL)
end)
mp.register_script_message('notification-osd-bad', function(e)
set_notification_osd(e,MOOD_BAD)
end)
mp.register_script_message('notification-osd-good', function(e)
set_notification_osd(e,MOOD_GOOD)
end)
--
mp.register_script_message('set_syncplayintf_options', function(e)
set_syncplayintf_options(e)
end)
-- Default options
local utils = require 'mp.utils'
local options = require 'mp.options'
opts = {
-- All drawing is scaled by this value, including the text borders and the
-- cursor. Change it if you have a high-DPI display.
scale = 1,
-- Set the font used for the REPL and the console. This probably doesn't
-- have to be a monospaced font.
['chatInputFontFamily'] = 'monospace',
-- Enable/Disable
['chatInputEnabled'] = true,
['chatOutputEnabled'] = true,
['OscVisibilityChangeCompatible'] = false,
-- Set the font size used for the REPL and the console. This will be
-- multiplied by "scale."
['chatInputRelativeFontSize'] = 14,
['chatInputFontWeight'] = 1,
['chatInputFontUnderline'] = false,
['chatInputFontColor'] = "#000000",
['chatInputPosition'] = "Top",
['MaxChatMessageLength'] = 500,
['chatOutputFontFamily'] = "sans serif",
['chatOutputFontSize'] = 50,
['chatOutputFontWeight'] = 1,
['chatOutputFontUnderline'] = false,
['chatOutputFontColor'] = "#FFFFFF",
['chatOutputMode'] = "Chatroom",
['scrollingFirstRowOffset'] = 2,
-- Can be "Chatroom", "Subtitle" or "Scrolling" style
['chatMaxLines'] = 7,
['chatTopMargin'] = 25,
['chatLeftMargin'] = 20,
['chatDirectInput'] = true,
--
['notificationTimeout'] = 3,
['alertTimeout'] = 5,
['chatTimeout'] = 7,
--
['inputPromptStartCharacter'] = ">",
['inputPromptEndCharacter'] = "<",
['backslashSubstituteCharacter'] = "|",
--Lang:
['mpv-key-tab-hint'] = "[TAB] to toggle access to alphabet row key shortcuts.",
['mpv-key-hint'] = "[ENTER] to send message. [ESC] to escape chat mode.",
['alphakey-mode-warning-first-line'] = "You can temporarily use old mpv bindings with a-z keys.",
['alphakey-mode-warning-second-line'] = "Press [TAB] to return to Syncplay chat mode.",
}
function detect_platform()
local o = {}
-- Kind of a dumb way of detecting the platform but whatever
if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then
return 'windows'
elseif mp.get_property_native('options/input-app-events', o) ~= o then
return 'macos'
end
return 'linux'
end
-- Pick a better default font for Windows and macOS
local platform = detect_platform()
if platform == 'windows' then
opts.font = 'Consolas'
elseif platform == 'macos' then
opts.font = 'Menlo'
end
-- Apply user-set options
options.read_options(opts)
-- Escape a string for verbatim display on the OSD
function ass_escape(str)
-- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if
-- it isn't followed by a recognised character, so add a zero-width
-- non-breaking space
str = str:gsub('\\', '\\\239\187\191')
str = str:gsub('{', '\\{')
str = str:gsub('}', '\\}')
-- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of
-- consecutive newlines
str = str:gsub('\n', '\239\187\191\n')
return str
end
function update()
return
end
function input_ass()
if not repl_active then
return ""
end
last_chat_time = mp.get_time() -- to keep chat messages showing while entering input
local bold
if opts['chatInputFontWeight'] < 75 then
bold = 0
else
bold = 1
end
local underline = opts['chatInputFontUnderline'] and 1 or 0
local red = string.sub(opts['chatInputFontColor'],2,3)
local green = string.sub(opts['chatInputFontColor'],4,5)
local blue = string.sub(opts['chatInputFontColor'],6,7)
local fontColor = blue .. green .. red
local style = '{\\r' ..
'\\1a&H00&\\3a&H00&\\4a&H99&' ..
'\\1c&H'..fontColor..'&\\3c&H111111&\\4c&H000000&' ..
'\\fn' .. opts['chatInputFontFamily'] .. '\\fs' .. (opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER) .. '\\b' .. bold ..
'\\bord2\\xshad0\\yshad1\\fsp0\\q1}'
local after_style = '{\\u' .. underline .. '}'
local cheight = opts['chatInputRelativeFontSize'] * FONT_SIZE_MULTIPLIER * 8
local cglyph = '_'
local before_cur = wordwrapify_string(ass_escape(line:sub(1, cursor - 1)))
local after_cur = wordwrapify_string(ass_escape(line:sub(cursor)))
local secondary_pos = "10,"..tostring(10+(opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER))
local alignment = 7
local position = "5,5"
local start_marker = opts['inputPromptStartCharacter']
local end_marker = ""
if opts['chatInputPosition'] == "Middle" then
alignment = 5
position = tostring(CANVAS_WIDTH/2)..","..tostring(CANVAS_HEIGHT/2)
secondary_pos = tostring(CANVAS_WIDTH/2)..","..tostring((CANVAS_HEIGHT/2)+20+(opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER))
end_marker = "{\\u0}"..opts['inputPromptEndCharacter']
elseif opts['chatInputPosition'] == "Bottom" then
alignment = 1
position = tostring(5)..","..tostring(CANVAS_HEIGHT-5)
secondary_pos = "10,"..tostring(CANVAS_HEIGHT-(20+(opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER)))
end
local osd_help_message = opts['mpv-key-hint']
if opts['chatDirectInput'] then
osd_help_message = opts['mpv-key-tab-hint'] .. " " .. osd_help_message
end
local help_prompt = '\\N\\n{\\an'..alignment..'\\pos('..secondary_pos..')\\fn' .. opts['chatOutputFontFamily'] .. '\\fs' .. ((opts['chatInputRelativeFontSize']*FONT_SIZE_MULTIPLIER)/1.25) .. '\\1c&H'..HINT_TEXT_COLOUR..'}' .. osd_help_message
local firststyle = "{\\an"..alignment.."}{\\pos("..position..")}"
if opts['chatOutputEnabled'] and opts['chatOutputMode'] == CHAT_MODE_CHATROOM and opts['chatInputPosition'] == "Top" then
firststyle = get_output_style().."{'\\1c&H'"..fontColor.."}"
before_cur = before_cur .. firststyle
after_cur = after_cur .. firststyle
help_prompt = '\\N\\n'..firststyle..'{\\1c&H'..HINT_TEXT_COLOUR..'}' .. osd_help_message .. '\\N\\n'
end
if key_hints_enabled == false then help_prompt = "" end
return firststyle..style..start_marker.." "..after_style..before_cur..style..cglyph..style..after_style..after_cur..end_marker..help_prompt
end
function get_output_style()
local bold
if opts['chatOutputFontWeight'] < 75 then
bold = 0
else
bold = 1
end
local underline = opts['chatOutputFontUnderline'] and 1 or 0
local red = string.sub(opts['chatOutputFontColor'],2,3)
local green = string.sub(opts['chatOutputFontColor'],4,5)
local blue = string.sub(opts['chatOutputFontColor'],6,7)
local fontColor = blue .. green .. red
local style = '{\\r' ..
'\\1a&H00&\\3a&H00&\\4a&H99&' ..
'\\1c&H'..fontColor..'&\\3c&H111111&\\4c&H000000&' ..
'\\fn' .. opts['chatOutputFontFamily'] .. '\\fs' .. (opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER) .. '\\b' .. bold ..
'\\u' .. underline .. '\\a5\\MarginV=500' .. '}'
--mp.osd_message("",0)
return style
end
function escape()
set_active(false)
clear()
end
-- Set the REPL visibility (`, Esc)
function set_active(active)
if use_alpha_rows_for_chat == false then active = false end
if active == repl_active then return end
if active then
repl_active = true
insert_mode = false
mp.enable_key_bindings('repl-input', 'allow-hide-cursor+allow-vo-dragging')
else
repl_active = false
mp.disable_key_bindings('repl-input')
end
if default_oscvisibility_state ~= "never" and opts['OscVisibilityChangeCompatible'] == true then
if active then
mp.commandv("script-message", "osc-visibility","never", "no-osd")
else
mp.commandv("script-message", "osc-visibility",default_oscvisibility_state, "no-osd")
end
end
end
-- Show the repl if hidden and replace its contents with 'text'
-- (script-message-to repl type)
function show_and_type(text)
text = text or ''
line = text
cursor = line:len() + 1
insert_mode = false
if repl_active then
update()
else
set_active(true)
end
end
-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
function next_utf8(str, pos)
if pos > str:len() then return pos end
repeat
pos = pos + 1
until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
return pos
end
-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
-- As above, but finds the previous UTF-8 charcter in 'str' before 'pos'
function prev_utf8(str, pos)
if pos <= 1 then return pos end
repeat
pos = pos - 1
until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
return pos
end
function trim_string(line,maxCharacters)
-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
local str = line
if str == nil or str == "" or str:len() <= maxCharacters then
return str, ""
end
local pos = 0
local oldPos = -1
local chars = 0
repeat
oldPos = pos
pos = next_utf8(str, pos)
chars = chars + 1
until pos == oldPos or chars > maxCharacters
return str:sub(1,pos-1), str:sub(pos)
end
function wordwrapify_string(line)
-- Used to ensure characters wrap on a per-character rather than per-word basis
-- to avoid issues with long filenames, etc.
local str = line
if str == nil or str == "" then
return ""
end
local newstr = ""
local currentChar = 0
local nextChar = 0
local chars = 0
local maxChars = str:len()
repeat
nextChar = next_utf8(str, currentChar)
if nextChar == currentChar then
return newstr
end
local charToTest = str:sub(currentChar,nextChar-1)
if charToTest ~= "\\" and charToTest ~= "{" and charToTest ~= "}" and charToTest ~= "%" then
newstr = newstr .. WORDWRAPIFY_MAGICWORD .. str:sub(currentChar,nextChar-1)
else
newstr = newstr .. str:sub(currentChar,nextChar-1)
end
currentChar = nextChar
until currentChar > maxChars
newstr = string.gsub(newstr,opts['backslashSubstituteCharacter'], '\\\239\187\191') -- Workaround for \ escape issues
return newstr
end
function trim_input()
-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
local str = line
if str == nil or str == "" or str:len() <= opts['MaxChatMessageLength'] then
return
end
local pos = 0
local oldPos = -1
local chars = 0
repeat
oldPos = pos
pos = next_utf8(str, pos)
chars = chars + 1
until pos == oldPos or chars > opts['MaxChatMessageLength']
line = line:sub(1,pos-1)
if cursor > pos then
cursor = pos
end
return
end
-- Insert a character at the current cursor position (' '-'~', Shift+Enter)
function handle_char_input(c)
if c == nil then return end
if c == "\\" then c = opts['backslashSubstituteCharacter'] end
if key_hints_enabled and (string.len(line) > 0 or opts['chatDirectInput'] == false) then
key_hints_enabled = false
end
set_active(true)
if insert_mode then
line = line:sub(1, cursor - 1) .. c .. line:sub(next_utf8(line, cursor))
else
line = line:sub(1, cursor - 1) .. c .. line:sub(cursor)
end
cursor = cursor + c:len()
trim_input()
update()
end
-- Remove the character behind the cursor (Backspace)
function handle_backspace()
if cursor <= 1 then return end
local prev = prev_utf8(line, cursor)
line = line:sub(1, prev - 1) .. line:sub(cursor)
cursor = prev
update()
end
-- Remove the character in front of the cursor (Del)
function handle_del()
if cursor > line:len() then return end
line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor))
update()
end
-- Toggle insert mode (Ins)
function handle_ins()
insert_mode = not insert_mode
end
--local was_active_before_tab = false
function handle_tab()
use_alpha_rows_for_chat = not use_alpha_rows_for_chat
if use_alpha_rows_for_chat then
mp.enable_key_bindings('repl-alpha-input')
--set_active(was_active_before_tab)
else
mp.disable_key_bindings('repl-alpha-input')
--was_active_before_tab = repl_active
--set_active(false)
escape()
end
end
-- Move the cursor to the next character (Right)
function next_char(amount)
cursor = next_utf8(line, cursor)
update()
end
-- Move the cursor to the previous character (Left)
function prev_char(amount)
cursor = prev_utf8(line, cursor)
update()
end
-- Clear the current line (Ctrl+C)
function clear()
line = ''
cursor = 1
insert_mode = false
update()
end
-- Close the REPL if the current line is empty, otherwise do nothing (Ctrl+D)
function maybe_exit()
if line == '' then
set_active(false)
end
end
-- Run the current command and clear the line (Enter)
function handle_enter()
if not repl_active then
set_active(true)
return
end
set_active(false)
if line == '' then
return
end
key_hints_enabled = false
line = string.gsub(line,"\\", "\\\\")
line = string.gsub(line,"\"", "\\\"")
mp.command('print-text "<chat>'..line..'</chat>"')
clear()
end
-- Move the cursor to the beginning of the line (HOME)
function go_home()
cursor = 1
update()
end
-- Move the cursor to the end of the line (END)
function go_end()
cursor = line:len() + 1
update()
end
-- Delete from the cursor to the end of the line (Ctrl+K)
function del_to_eol()
line = line:sub(1, cursor - 1)
update()
end
-- Delete from the cursor back to the start of the line (Ctrl+U)
function del_to_start()
line = line:sub(cursor)
cursor = 1
update()
end
-- Returns a string of UTF-8 text from the clipboard (or the primary selection)
function get_clipboard(clip)
if platform == 'linux' then
local res = utils.subprocess({ args = {
'xclip', '-selection', clip and 'clipboard' or 'primary', '-out'
} })
if not res.error then
return res.stdout
end
elseif platform == 'windows' then
local res = utils.subprocess({ args = {
'powershell', '-NoProfile', '-Command', [[& {
Trap {
Write-Error -ErrorRecord $_
Exit 1
}
$clip = ""
if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) {
$clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
} else {
Add-Type -AssemblyName PresentationCore
$clip = [Windows.Clipboard]::GetText()
}
$clip = $clip -Replace "`r",""
$u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
[Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
}]]
} })
if not res.error then
return res.stdout
end
elseif platform == 'macos' then
local res = utils.subprocess({ args = { 'pbpaste' } })
if not res.error then
return res.stdout
end
end
return ''
end
-- Paste text from the window-system's clipboard. 'clip' determines whether the
-- clipboard or the primary selection buffer is used (on X11 only.)
function paste(clip)
local text = get_clipboard(clip)
local before_cur = line:sub(1, cursor - 1)
local after_cur = line:sub(cursor)
line = before_cur .. text .. after_cur
cursor = cursor + text:len()
trim_input()
update()
end
-- The REPL has pretty specific requirements for key bindings that aren't
-- really satisified by any of mpv's helper methods, since they must be in
-- their own input section, but they must also raise events on key-repeat.
-- Hence, this function manually creates an input section and puts a list of
-- bindings in it.
function add_repl_bindings(bindings)
local cfg = ''
for i, binding in ipairs(bindings) do
local key = binding[1]
local fn = binding[2]
local name = '__repl_binding_' .. i
mp.add_forced_key_binding(nil, name, fn, 'repeatable')
cfg = cfg .. key .. ' script-binding ' .. mp.script_name .. '/' ..
name .. '\n'
end
mp.commandv('define-section', 'repl-input', cfg, 'force')
end
function add_repl_alpharow_bindings(bindings)
local cfg = ''
for i, binding in ipairs(bindings) do
local key = binding[1]
local fn = binding[2]
local name = '__repl_alpha_binding_' .. i
mp.add_forced_key_binding(nil, name, fn, 'repeatable')
cfg = cfg .. key .. ' script-binding ' .. mp.script_name .. '/' ..
name .. '\n'
end
mp.commandv('define-section', 'repl-alpha-input', cfg, 'force')
mp.enable_key_bindings('repl-alpha-input')
end
-- Mapping from characters to mpv key names
local binding_name_map = {
[' '] = 'SPACE',
['#'] = 'SHARP',
}
-- List of input bindings. This is a weird mashup between common GUI text-input
-- bindings and readline bindings.
local bindings = {
{ 'esc', function() escape() end },
{ 'bs', handle_backspace },
{ 'shift+bs', handle_backspace },
{ 'del', handle_del },
{ 'shift+del', handle_del },
{ 'ins', handle_ins },
{ 'left', function() prev_char() end },
{ 'right', function() next_char() end },
{ 'up', function() clear() end },
{ 'home', go_home },
{ 'end', go_end },
{ 'ctrl+c', clear },
{ 'ctrl+d', maybe_exit },
{ 'ctrl+k', del_to_eol },
{ 'ctrl+l', clear_log_buffer },
{ 'ctrl+u', del_to_start },
{ 'ctrl+v', function() paste(true) end },
{ 'meta+v', function() paste(true) end },
}
local alpharowbindings = {}
-- Add bindings for all the printable US-ASCII characters from ' ' to '~'
-- inclusive. Note, this is a pretty hacky way to do text input. mpv's input
-- system was designed for single-key key bindings rather than text input, so
-- things like dead-keys and non-ASCII input won't work. This is probably okay
-- though, since all mpv's commands and properties can be represented in ASCII.
for b = (' '):byte(), ('~'):byte() do
local c = string.char(b)
local binding = binding_name_map[c] or c
bindings[#bindings + 1] = {binding, function() handle_char_input(c) end}
end
function add_alpharowbinding(firstchar,lastchar)
for b = (firstchar):byte(), (lastchar):byte() do
local c = string.char(b)
local alphabinding = binding_name_map[c] or c
alpharowbindings[#alpharowbindings + 1] = {alphabinding, function() handle_char_input(c) end}
end
end
function add_specialalphabindings(charinput)
local alphabindingarray = charinput
for i, alphabinding in ipairs(alphabindingarray) do
alpharowbindings[#alpharowbindings + 1] = {alphabinding, function() handle_char_input(alphabinding) end }
end
end
add_alpharowbinding('a','z')
add_alpharowbinding('A','Z')
add_alpharowbinding('/','/')
add_alpharowbinding(':',':')
add_alpharowbinding('(',')')
add_alpharowbinding('{','}')
add_alpharowbinding(':',';')
add_alpharowbinding('<','>')
add_alpharowbinding(',','.')
add_alpharowbinding('|','|')
add_alpharowbinding('\\','\\')
add_alpharowbinding('?','?')
add_alpharowbinding('[',']')
add_alpharowbinding('#','#')
add_alpharowbinding('~','~')
add_alpharowbinding('\'','\'')
add_alpharowbinding('@','@')
add_specialalphabindings({'à','è','ì','ò','ù','À','È','Ì','Ò','Ù'})
add_specialalphabindings({'á', 'é', 'í', 'ó', 'ú', 'ý', 'Á', 'É', 'Í', 'Ó', 'Ú', 'Ý'})
add_specialalphabindings({'â', 'ê', 'î', 'ô', 'û', 'Â', 'Ê', 'Î', 'Ô', 'Û'})
add_specialalphabindings({'ã', 'ñ', 'õ', 'Ã', 'Ñ', 'Õ'})
add_specialalphabindings({'ä', 'ë', 'ï', 'ö', 'ü', 'ÿ', 'Ä', 'Ë', 'Ï', 'Ö', 'Ü', 'Ÿ'})
add_specialalphabindings({'å', 'Å','æ','Æ','œ','Œ','ç','Ç','ð','Ð','ø','Ø','¿','¡','ß'})
add_specialalphabindings({'¤','','×','÷','','±','','','','§','ˆ','˜','«','»','¦','','©','®',''})
add_specialalphabindings({'ž','Ž'})
add_specialalphabindings({'ª','Þ','þ','ƒ','µ','°','º','','','','','¬','¥','£','','¢','¹','²','³','½','¼','¾'})
add_specialalphabindings({'·','Ĉ','ĉ','Ĝ','ĝ','Ĥ','ĥ','Ĵ','ĵ','Ŝ','ŝ','Ŭ','ŭ'})
add_specialalphabindings({'Б','б','В','в','Г','г','Д','д','Е','е','Ё','ё','Ж','ж','З','з'})
add_specialalphabindings({'И','и','Й','й','К','к','Л','л','М','м','Н','н','О','о','П','п'})
add_specialalphabindings({'Р','р','С','с','Т','т','У','у','Ф','ф','Х','х','Ц','ц','Ч','ч'})
add_specialalphabindings({'Ш','ш','Щ','щ','Ъ','ъ','Ы','ы','Ь','ь','Э','э','Ю','ю','Я','я'})
add_specialalphabindings({'',''})
add_repl_bindings(bindings)
-- Add a script-message to show the REPL and fill it with the provided text
mp.register_script_message('type', function(text)
show_and_type(text)
end)
local syncplayintfSet = false
mp.command('print-text "<get_syncplayintf_options>"')
function readyMpvAfterSettingsKnown()
if syncplayintfSet == false then
local vertical_output_area = CANVAS_HEIGHT-(opts['chatTopMargin']+opts['chatBottomMargin']+((opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER)*opts['scrollingFirstRowOffset'])+SCROLLING_ADDITIONAL_BOTTOM_MARGIN)
max_scrolling_rows = math.floor(vertical_output_area/(opts['chatOutputRelativeFontSize']*FONT_SIZE_MULTIPLIER))
local user_opts = { visibility = "auto", }
opt.read_options(user_opts, "osc")
default_oscvisibility_state = user_opts.visibility
if opts['chatInputEnabled'] == true then
key_hints_enabled = true
mp.add_forced_key_binding('enter', handle_enter)
mp.add_forced_key_binding('kp_enter', handle_enter)
if opts['chatDirectInput'] == true then
add_repl_alpharow_bindings(alpharowbindings)
mp.add_forced_key_binding('tab', handle_tab)
end
end
syncplayintfSet = true
end
end
function set_syncplayintf_options(input)
--mp.command('print-text "<chat>...'..input..'</chat>"')
for option, value in string.gmatch(input, "([^ ,=]+)=([^,]+)") do
local valueType = type(opts[option])
if valueType == "number" then
value = tonumber(value)
elseif valueType == "boolean" then
if value == "True" then
value = true
else
value = false
end
end
opts[option] = value
--mp.command('print-text "<chat>'..option.."="..tostring(value).." - "..valueType..'</chat>"')
end
chat_format = get_output_style()
readyMpvAfterSettingsKnown()
end

BIN
resources/user_comment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

View File

@ -1,4 +1,4 @@
version = '1.5.1'
version = '1.5.2'
milestone = 'Yoitsu'
release_number = '51'
release_number = '55'
projectURL = 'http://syncplay.pl/'

View File

@ -145,8 +145,8 @@ class SyncplayClient(object):
def initPlayer(self, player):
self._player = player
if not self._player.secondaryOSDSupported:
constants.OSD_WARNING_MESSAGE_DURATION = constants.NO_SECONDARY_OSD_WARNING_DURATION
if not self._player.alertOSDSupported:
constants.OSD_WARNING_MESSAGE_DURATION = constants.NO_ALERT_OSD_WARNING_DURATION
self.scheduleAskPlayer()
self.__playerReady.callback(player)
@ -205,7 +205,7 @@ class SyncplayClient(object):
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 self.serverFeatures["readiness"]:
elif pauseChange and self.serverFeatures.has_key("readiness") and self.serverFeatures["readiness"]:
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)
@ -546,15 +546,32 @@ class SyncplayClient(object):
"sharedPlaylists": utils.meetsMinVersion(self.serverVersion, constants.SHARED_PLAYLIST_MIN_VERSION),
"chat": utils.meetsMinVersion(self.serverVersion, constants.CHAT_MIN_VERSION),
"readiness": utils.meetsMinVersion(self.serverVersion, constants.USER_READY_MIN_VERSION),
"managedRooms": utils.meetsMinVersion(self.serverVersion, constants.CONTROLLED_ROOMS_MIN_VERSION)
"managedRooms": utils.meetsMinVersion(self.serverVersion, constants.CONTROLLED_ROOMS_MIN_VERSION),
"maxChatMessageLength": constants.FALLBACK_MAX_CHAT_MESSAGE_LENGTH,
"maxUsernameLength": constants.FALLBACK_MAX_USERNAME_LENGTH,
"maxRoomNameLength": constants.FALLBACK_MAX_ROOM_NAME_LENGTH,
"maxFilenameLength": constants.FALLBACK_MAX_FILENAME_LENGTH
}
if featureList:
self.serverFeatures.update(featureList)
if not utils.meetsMinVersion(self.serverVersion, constants.SHARED_PLAYLIST_MIN_VERSION):
self.ui.showErrorMessage(getMessage("shared-playlists-not-supported-by-server-error").format(constants.SHARED_PLAYLIST_MIN_VERSION, self.serverVersion))
elif not self.serverFeatures["sharedPlaylists"]:
self.ui.showErrorMessage(getMessage("shared-playlists-disabled-by-server-error"))
# TODO: Have messages for all unsupported & disabled features
constants.MAX_CHAT_MESSAGE_LENGTH = self.serverFeatures["maxChatMessageLength"]
constants.MAX_USERNAME_LENGTH = self.serverFeatures["maxUsernameLength"]
constants.MAX_ROOM_NAME_LENGTH = self.serverFeatures["maxRoomNameLength"]
constants.MAX_FILENAME_LENGTH = self.serverFeatures["maxFilenameLength"]
constants.MPV_SYNCPLAYINTF_CONSTANTS_TO_SEND = ["MaxChatMessageLength={}".format(constants.MAX_CHAT_MESSAGE_LENGTH),
u"inputPromptStartCharacter={}".format(constants.MPV_INPUT_PROMPT_START_CHARACTER),
u"inputPromptEndCharacter={}".format(constants.MPV_INPUT_PROMPT_END_CHARACTER),
u"backslashSubstituteCharacter={}".format(
constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER)]
self.ui.setFeatures(self.serverFeatures)
if self._player:
self._player.setFeatures(self.serverFeatures)
def getSanitizedCurrentUserFile(self):
if self.userlist.currentUser.file:
@ -583,16 +600,29 @@ class SyncplayClient(object):
def getUsername(self):
return self.userlist.currentUser.username
def chatIsEnabled(self):
return True
# TODO: Allow chat to be disabled
def getFeatures(self):
features = dict()
# Can change during runtime:
features["sharedPlaylists"] = self.sharedPlaylistIsEnabled() # Can change during runtime
features["chat"] = self.chatIsEnabled() # Can change during runtime
# Static for this version/release of Syncplay:
features["featureList"] = True
features["readiness"] = True
features["managedRooms"] = True
return features
def setRoom(self, roomName, resetAutoplay=False):
self.userlist.currentUser.room = roomName
if resetAutoplay:
self.resetAutoPlayState()
def sendChat(self,message):
if self._protocol and self._protocol.logged:
message = utils.truncateText(message,constants.MAX_CHAT_MESSAGE_LENGTH)
self._protocol.sendChatMessage(message)
def sendRoom(self):
room = self.userlist.currentUser.room
if self._protocol and self._protocol.logged and room:
@ -632,8 +662,8 @@ class SyncplayClient(object):
if self._protocol and self._protocol.logged:
self._protocol.sendList()
def showUserList(self):
self.userlist.showUserList()
def showUserList(self, altUI=None):
self.userlist.showUserList(altUI)
def getPassword(self):
if self.thisIsPublicServer():
@ -700,19 +730,30 @@ class SyncplayClient(object):
if promptForAction:
self.ui.promptFor(getMessage("enter-to-exit-prompt"))
def requireMinServerVersion(minVersion):
def requireMinVersionDecorator(f):
def requireServerFeature(featureRequired):
def requireServerFeatureDecorator(f):
@wraps(f)
def wrapper(self, *args, **kwds):
if not utils.meetsMinVersion(self.serverVersion,minVersion):
if self.serverVersion != "0.0.0":
self.ui.showErrorMessage(getMessage("not-supported-by-server-error").format(minVersion, self.serverVersion))
else:
self.ui.showDebugMessage("Tried to check server version too soon (threshold: {})".format(minVersion))
if self.serverVersion == "0.0.0":
self.ui.showDebugMessage(
"Tried to check server version too soon (testing support for: {})".format(featureRequired))
return None
if not self.serverFeatures.has_key(featureRequired) or not self.serverFeatures[featureRequired]:
featureName = getMessage(u"feature-{}".format(featureRequired))
self.ui.showErrorMessage(getMessage("not-supported-by-server-error").format(featureName))
return
return f(self, *args, **kwds)
return wrapper
return requireMinVersionDecorator
return requireServerFeatureDecorator
@requireServerFeature("chat")
def sendChat(self,message):
if self._protocol and self._protocol.logged:
message = utils.truncateText(message,constants.MAX_CHAT_MESSAGE_LENGTH)
self._protocol.sendChatMessage(message)
def sendFeaturesUpdate(self, features):
self._protocol.sendFeaturesUpdate(features)
def changePlaylistEnabledState(self, newState):
oldState = self.sharedPlaylistIsEnabled()
@ -780,7 +821,7 @@ class SyncplayClient(object):
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)
self.ui.showOSDMessage(countdownMessage, 1, OSDType=constants.OSD_ALERT, mood=constants.MESSAGE_GOODNEWS)
if self.autoplayTimeLeft <= 0:
self.setPaused(False)
self.stopAutoplayCountdown()
@ -792,11 +833,11 @@ class SyncplayClient(object):
self.ui.updateAutoPlayState(False)
self.stopAutoplayCountdown()
@requireMinServerVersion(constants.USER_READY_MIN_VERSION)
@requireServerFeature("readiness")
def toggleReady(self, manuallyInitiated=True):
self._protocol.setReady(not self.userlist.currentUser.isReady(), manuallyInitiated)
@requireMinServerVersion(constants.USER_READY_MIN_VERSION)
@requireServerFeature("readiness")
def changeReadyState(self, newState, manuallyInitiated=True):
oldState = self.userlist.currentUser.isReady()
if newState != oldState:
@ -811,7 +852,12 @@ class SyncplayClient(object):
if oldReadyState != isReady:
self._warnings.checkReadyStates()
@requireMinServerVersion(constants.CONTROLLED_ROOMS_MIN_VERSION)
@requireServerFeature("managedRooms")
def setUserFeatures(self, username, features):
self.userlist.setFeatures(username, features)
self.ui.userListChange()
@requireServerFeature("managedRooms")
def createControlledRoom(self, roomName):
controlPassword = utils.RandomStringGenerator.generate_room_password()
self.lastControlPasswordAttempt = controlPassword
@ -830,7 +876,7 @@ class SyncplayClient(object):
else:
return ""
@requireMinServerVersion(constants.CONTROLLED_ROOMS_MIN_VERSION)
@requireServerFeature("managedRooms")
def identifyAsController(self, controlPassword):
controlPassword = self.stripControlPassword(controlPassword)
self.ui.showMessage(getMessage("identifying-as-controller-notification").format(controlPassword))
@ -924,7 +970,7 @@ class SyncplayClient(object):
def _checkIfYouReAloneInTheRoom(self, OSDOnly):
if self._userlist.areYouAloneInRoom():
self._ui.showOSDMessage(getMessage("alone-in-the-room"), constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, secondaryOSD=True)
self._ui.showOSDMessage(getMessage("alone-in-the-room"), constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, OSDType=constants.OSD_ALERT, mood=constants.MESSAGE_BADNEWS)
if not OSDOnly:
self._ui.showMessage(getMessage("alone-in-the-room"), True)
if constants.SHOW_OSD_WARNINGS and not self._warnings["alone-in-the-room"]['timer'].running:
@ -954,8 +1000,10 @@ class SyncplayClient(object):
if not self._client._player or self._client.autoplayTimerIsRunning():
return
osdMessage = None
messageMood = constants.MESSAGE_GOODNEWS
fileDifferencesForRoom = self._userlist.getFileDifferencesForRoom()
if not self._userlist.areAllFilesInRoomSame() and fileDifferencesForRoom is not None:
messageMood = constants.MESSAGE_BADNEWS
fileDifferencesMessage = getMessage("room-file-differences").format(fileDifferencesForRoom)
if self._userlist.currentUser.canControl() and self._userlist.isReadinessSupported():
if self._userlist.areAllUsersInRoomReady():
@ -970,9 +1018,10 @@ class SyncplayClient(object):
if self._userlist.areAllUsersInRoomReady():
osdMessage = getMessage("all-users-ready").format(self._userlist.readyUserCount())
else:
messageMood = constants.MESSAGE_BADNEWS
osdMessage = getMessage("not-all-ready").format(self._userlist.usersInRoomNotReady())
if osdMessage:
self._ui.showOSDMessage(osdMessage, constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, secondaryOSD=True)
self._ui.showOSDMessage(osdMessage, constants.WARNING_OSD_MESSAGES_LOOP_INTERVAL, OSDType=constants.OSD_ALERT, mood=messageMood)
def __displayMessageOnOSD(self, warningName, warningFunction):
if constants.OSD_WARNING_MESSAGE_DURATION > self._warnings[warningName]["displayedFor"]:
@ -1001,6 +1050,7 @@ class SyncplayUser(object):
self.room = room
self.file = file_
self._controller = False
self._features = {}
def setFile(self, filename, duration, size, path=None):
file_ = {
@ -1054,6 +1104,9 @@ class SyncplayUser(object):
def setReady(self, ready):
self.ready = ready
def setFeatures(self, features):
self._features = features
class SyncplayUserlist(object):
def __init__(self, ui, client):
self.currentUser = SyncplayUser()
@ -1132,7 +1185,7 @@ class SyncplayUserlist(object):
if differentDuration: differences.append(getMessage("file-difference-duration"))
return ", ".join(differences)
def addUser(self, username, room, file_, noMessage=False, isController=None, isReady=None):
def addUser(self, username, room, file_, noMessage=False, isController=None, isReady=None, features={}):
if username == self.currentUser.username:
if isController is not None:
self.currentUser.setControllerStatus(isController)
@ -1143,7 +1196,7 @@ class SyncplayUserlist(object):
user.setControllerStatus(isController)
self._users[username] = user
user.setReady(isReady)
user.setFeatures(features)
if not noMessage:
self.__showUserChangeMessage(username, room, file_)
self.userListChange(room)
@ -1310,7 +1363,7 @@ class SyncplayUserlist(object):
def hasRoomStateChanged(self):
return self._roomUsersChanged
def showUserList(self):
def showUserList(self, altUI=None):
rooms = {}
for user in self._users.itervalues():
if user.room not in rooms:
@ -1320,6 +1373,9 @@ class SyncplayUserlist(object):
rooms[self.currentUser.room] = []
rooms[self.currentUser.room].append(self.currentUser)
rooms = self.sortList(rooms)
if altUI:
altUI.showUserList(self.currentUser, rooms)
else:
self.ui.showUserList(self.currentUser, rooms)
self._client.autoplayCheck()
@ -1336,10 +1392,10 @@ class UiManager(object):
def __init__(self, client, ui):
self._client = client
self.__ui = ui
self.lastPrimaryOSDMessage = None
self.lastPrimaryOSDEndTime = None
self.lastSecondaryOSDMessage = None
self.lastSecondaryOSDEndTime = None
self.lastNotificatinOSDMessage = None
self.lastNotificationOSDEndTime = None
self.lastAlertOSDMessage = None
self.lastAlertOSDEndTime = None
self.lastError = ""
def setPlaylist(self, newPlaylist, newIndexFilename=None):
@ -1358,8 +1414,16 @@ class UiManager(object):
if constants.DEBUG_MODE and message.rstrip():
sys.stderr.write("{}{}\n".format(time.strftime(constants.UI_TIME_FORMAT, time.localtime()).decode('utf-8'),message.rstrip()))
def showMessage(self, message, noPlayer=False, noTimestamp=False, secondaryOSD=False):
if not noPlayer: self.showOSDMessage(message, duration=constants.OSD_DURATION, secondaryOSD=secondaryOSD)
def showChatMessage(self, username, userMessage):
messageString = u"<{}> {}".format(username, userMessage)
if self._client._player.chatOSDSupported and self._client._config["chatOutputEnabled"]:
self._client._player.displayChatMessage(username,userMessage)
else:
self.showOSDMessage(messageString, duration=constants.OSD_DURATION)
self.__ui.showMessage(messageString)
def showMessage(self, message, noPlayer=False, noTimestamp=False, OSDType=constants.OSD_NOTIFICATION,mood=constants.MESSAGE_NEUTRAL):
if not noPlayer: self.showOSDMessage(message, duration=constants.OSD_DURATION, OSDType=OSDType, mood=mood)
self.__ui.showMessage(message, noTimestamp)
def updateAutoPlayState(self, newState):
@ -1368,28 +1432,28 @@ class UiManager(object):
def showUserList(self, currentUser, rooms):
self.__ui.showUserList(currentUser, rooms)
def showOSDMessage(self, message, duration=constants.OSD_DURATION, secondaryOSD=False):
def showOSDMessage(self, message, duration=constants.OSD_DURATION, OSDType=constants.OSD_NOTIFICATION, mood=constants.MESSAGE_NEUTRAL):
autoplayConditionsMet = self._client.autoplayConditionsMet()
if secondaryOSD and not constants.SHOW_OSD_WARNINGS and not self._client.autoplayTimerIsRunning():
if OSDType == constants.OSD_ALERT and not constants.SHOW_OSD_WARNINGS and not self._client.autoplayTimerIsRunning():
return
if not self._client._player:
return
if constants.SHOW_OSD and self._client and self._client._player:
if not self._client._player.secondaryOSDSupported:
if secondaryOSD:
self.lastSecondaryOSDMessage = message
if not self._client._player.alertOSDSupported:
if OSDType == constants.OSD_ALERT:
self.lastAlertOSDMessage = message
if autoplayConditionsMet:
self.lastSecondaryOSDEndTime = time.time() + 1.0
self.lastAlertOSDEndTime = time.time() + 1.0
else:
self.lastSecondaryOSDEndTime = time.time() + constants.NO_SECONDARY_OSD_WARNING_DURATION
if self.lastPrimaryOSDEndTime and time.time() < self.lastPrimaryOSDEndTime:
message = u"{}{}{}".format(message, self._client._player.osdMessageSeparator, self.lastPrimaryOSDMessage)
self.lastAlertOSDEndTime = time.time() + constants.NO_ALERT_OSD_WARNING_DURATION
if self.lastNotificationOSDEndTime and time.time() < self.lastNotificationOSDEndTime:
message = u"{}{}{}".format(message, self._client._player.osdMessageSeparator, self.lastNotificatinOSDMessage)
else:
self.lastPrimaryOSDMessage = message
self.lastPrimaryOSDEndTime = time.time() + constants.OSD_DURATION
if self.lastSecondaryOSDEndTime and time.time() < self.lastSecondaryOSDEndTime:
message = u"{}{}{}".format(self.lastSecondaryOSDMessage, self._client._player.osdMessageSeparator, message)
self._client._player.displayMessage(message, int(duration * 1000), secondaryOSD)
self.lastNotificatinOSDMessage = message
self.lastNotificationOSDEndTime = time.time() + constants.OSD_DURATION
if self.lastAlertOSDEndTime and time.time() < self.lastAlertOSDEndTime:
message = u"{}{}{}".format(self.lastAlertOSDMessage, self._client._player.osdMessageSeparator, message)
self._client._player.displayMessage(message, int(duration * 1000), OSDType, mood)
def setControllerStatus(self, username, isController):
self.__ui.setControllerStatus(username, isController)
@ -1411,6 +1475,9 @@ class UiManager(object):
def updateRoomName(self, room=""):
self.__ui.updateRoomName(room)
def executeCommand(self, command):
self.__ui.executeCommand(command)
def drop(self):
self.__ui.drop()
@ -1799,7 +1866,7 @@ class FileSwitchManager(object):
if os.path.isfile(filepath):
return filepath
if highPriority and self.folderSearchEnabled:
if highPriority and self.folderSearchEnabled and self.mediaDirectories is not None:
directoryList = self.mediaDirectories
# Spin up hard drives to prevent premature timeout
randomFilename = u"RandomFile"+unicode(random.randrange(10000, 99999))+u".txt"

View File

@ -1,8 +1,9 @@
# coding:utf8
# You might want to change these
DEFAULT_PORT = 8999
OSD_DURATION = 3.0
OSD_WARNING_MESSAGE_DURATION = 5.0
NO_SECONDARY_OSD_WARNING_DURATION = 13.0
NO_ALERT_OSD_WARNING_DURATION = 13.0
MPC_OSD_POSITION = 1 #Right corner, 1 for left
MPLAYER_OSD_LEVEL = 1
UI_TIME_FORMAT = "[%X] "
@ -56,12 +57,23 @@ PLAYLIST_MAX_CHARACTERS = 10000
PLAYLIST_MAX_ITEMS = 250
MAXIMUM_TAB_WIDTH = 350
TAB_PADDING = 30
DEFAULT_WINDOWS_MONOSPACE_FONT = "Consolas"
DEFAULT_OSX_MONOSPACE_FONT = "Menlo"
FALLBACK_MONOSPACE_FONT = "Monospace"
DEFAULT_CHAT_FONT_SIZE = 24
DEFAULT_CHAT_INPUT_FONT_COLOR = "#FFFF00"
DEFAULT_CHAT_OUTPUT_FONT_COLOR = "#FFFF00"
DEFAULT_CHAT_FONT_WEIGHT = 1
# Maximum character lengths (for client and server)
MAX_CHAT_MESSAGE_LENGTH = 50 # Number of displayed characters
MAX_USERNAME_LENGTH = 16 # Number of displayed characters
# Max numbers are used by server (and client pre-connection). Once connected client gets values from server featureList (or uses 'fallback' versions for old servers)
MAX_CHAT_MESSAGE_LENGTH = 125 # Number of displayed characters
MAX_USERNAME_LENGTH = 20 # Number of displayed characters
MAX_ROOM_NAME_LENGTH = 35 # Number of displayed characters
MAX_FILENAME_LENGTH = 250 # Number of displayed characters
FALLBACK_MAX_CHAT_MESSAGE_LENGTH = 50 # Number of displayed characters
FALLBACK_MAX_USERNAME_LENGTH = 16 # Number of displayed characters
FALLBACK_MAX_ROOM_NAME_LENGTH = 35 # Number of displayed characters
FALLBACK_MAX_FILENAME_LENGTH = 250 # Number of displayed characters
# Options for the File Switch feature:
FOLDER_SEARCH_FIRST_FILE_TIMEOUT = 25.0 # Secs - How long to wait to find the first file in folder search (to take account of HDD spin up)
@ -133,7 +145,7 @@ MPC_ICONPATH = "mpc-hc.png"
MPC64_ICONPATH = "mpc-hc64.png"
MPC_BE_ICONPATH = "mpc-be.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.']
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.', '[syncplayintf] Lua error']
#Changing these is usually not something you're looking for
PLAYER_ASK_DELAY = 0.1
@ -179,6 +191,19 @@ MPV_ARGS = ['--force-window', '--idle', '--hr-seek=always', '--keep-open']
MPV_SLAVE_ARGS = ['--msg-level=all=error,cplayer=info,term-msg=info', '--input-terminal=no', '--input-file=/dev/stdin']
MPV_SLAVE_ARGS_NEW = ['--term-playing-msg=<SyncplayUpdateFile>\nANS_filename=${filename}\nANS_length=${=duration:${=length:0}}\nANS_path=${path}\n</SyncplayUpdateFile>', '--terminal=yes']
MPV_NEW_VERSION = False
MPV_OSC_VISIBILITY_CHANGE_VERSION = False
MPV_INPUT_PROMPT_START_CHARACTER = u""
MPV_INPUT_PROMPT_END_CHARACTER = u""
MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER = u""
MPV_SYNCPLAYINTF_OPTIONS_TO_SEND = ["chatInputEnabled","chatInputFontFamily", "chatInputRelativeFontSize", "chatInputFontWeight","chatInputFontUnderline",
"chatInputFontColor", "chatInputPosition","chatOutputFontFamily","chatOutputRelativeFontSize",
"chatOutputFontWeight","chatOutputFontUnderline","chatOutputMode","chatMaxLines",
"chatTopMargin","chatLeftMargin","chatBottomMargin","chatDirectInput",
"notificationTimeout","alertTimeout","chatTimeout","chatOutputEnabled"]
MPV_SYNCPLAYINTF_CONSTANTS_TO_SEND = ["MaxChatMessageLength={}".format(MAX_CHAT_MESSAGE_LENGTH),u"inputPromptStartCharacter={}".format(MPV_INPUT_PROMPT_START_CHARACTER),u"inputPromptEndCharacter={}".format(MPV_INPUT_PROMPT_END_CHARACTER),u"backslashSubstituteCharacter={}".format(MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER)]
# Note: Constants updated in client.py->checkForFeatureSupport
MPV_SYNCPLAYINTF_LANGUAGE_TO_SEND = ["mpv-key-tab-hint","mpv-key-hint", "alphakey-mode-warning-first-line", "alphakey-mode-warning-second-line"]
VLC_SLAVE_ARGS = ['--extraintf=luaintf', '--lua-intf=syncplay', '--no-quiet', '--no-input-fast-seek',
'--play-and-pause', '--start-time=0']
VLC_SLAVE_MACOS_ARGS = ['--verbose=2', '--no-file-logging']
@ -200,6 +225,9 @@ UNPAUSE_IFALREADYREADY_MODE = "IfAlreadyReady"
UNPAUSE_IFOTHERSREADY_MODE = "IfOthersReady"
UNPAUSE_IFMINUSERSREADY_MODE = "IfMinUsersReady"
UNPAUSE_ALWAYS_MODE = "Always"
INPUT_POSITION_TOP = "Top"
INPUT_POSITION_MIDDLE = "Middle"
INPUT_POSITION_BOTTOM = "Bottom"
VLC_EOF_DURATION_THRESHOLD = 2.0
@ -217,6 +245,17 @@ FILEITEM_SWITCH_FILE_SWITCH = 1
FILEITEM_SWITCH_STREAM_SWITCH = 2
PLAYLISTITEM_CURRENTLYPLAYING_ROLE = 3
MESSAGE_NEUTRAL = "neutral"
MESSAGE_BADNEWS = "bad"
MESSAGE_GOODNEWS = "good"
OSD_NOTIFICATION = "notification" # Also known as PrimaryOSD
OSD_ALERT = "alert" # Also known as SecondaryOSD
OSD_CHAT = "chat"
CHATROOM_MODE = "Chatroom"
SCROLLING_MODE = "Scrolling"
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

View File

@ -66,4 +66,6 @@ def getMessage(type_, locale=None):
if messages["en"].has_key(type_):
return unicode(messages["en"][type_])
else:
raise KeyError(type_)
print u"WARNING: Cannot find message '{}'!".format(type_)
return "!{}".format(type_) # TODO: Remove
#raise KeyError(type_)

View File

@ -17,23 +17,23 @@ de = {
"connected-successful-notification" : u"Erfolgreich mit Server verbunden",
"retrying-notification" : u"%s, versuche erneut in %d Sekunden...", # Seconds
"rewind-notification" : u"Zurückgespult wegen Zeitdifferenz mit <{}>", # User
"fastforward-notification" : u"Vorgespult wegen Zeitdifferenz mit <{}>", # User
"slowdown-notification" : u"Verlangsamt wegen Zeitdifferenz mit <{}>", # User
"rewind-notification" : u"Zurückgespult wegen Zeitdifferenz mit {}", # User
"fastforward-notification" : u"Vorgespult wegen Zeitdifferenz mit {}", # User
"slowdown-notification" : u"Verlangsamt wegen Zeitdifferenz mit {}", # User
"revert-notification" : u"Normalgeschwindigkeit",
"pause-notification" : u"<{}> pausierte", # User
"unpause-notification" : u"<{}> startete", # User
"seek-notification" : u"<{}> sprang von {} nach {}", # User, from time, to time
"pause-notification" : u"{} pausierte", # User
"unpause-notification" : u"{} startete", # User
"seek-notification" : u"{} sprang von {} nach {}", # User, from time, to time
"current-offset-notification" : u"Aktueller Offset: {} Sekunden", # Offset
"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
"playing-notification" : u"<{}> spielt '{}' ({})", # User, file, duration
"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
"playing-notification" : u"{} spielt '{}' ({})", # User, file, duration
"playing-notification/room-addendum" : u" in Raum: '{}'", # Room
"not-all-ready" : u"Noch nicht bereit: {}", # Usernames
@ -44,11 +44,11 @@ de = {
"autoplaying-notification" : u"Starte in {}...", # Number of seconds until playback will start
"identifying-as-controller-notification" : u"Identifiziere als Raumleiter mit Passwort '{}'...", # TODO: find a better translation to "room operator"
"failed-to-identify-as-controller-notification" : u"<{}> konnte sich nicht als Raumleiter identifizieren.",
"authenticated-as-controller-notification" : u"<{}> authentifizierte sich als Raumleiter",
"failed-to-identify-as-controller-notification" : u"{} konnte sich nicht als Raumleiter identifizieren.",
"authenticated-as-controller-notification" : u"{} authentifizierte sich als Raumleiter",
"created-controlled-room-notification" : u"Gesteuerten Raum '{}' mit Passwort '{}' erstellt. Bitte diese Informationen für die Zukunft aufheben!", # RoomName, operatorPassword
"file-different-notification" : u"Deine Datei scheint sich von <{}>s zu unterscheiden", # User
"file-different-notification" : u"Deine Datei scheint sich von {}s zu unterscheiden", # User
"file-differences-notification" : u"Deine Datei unterscheidet sich auf folgende Art: {}",
"room-file-differences" : u"Unterschiedlich in: {}", # File differences (filename, size, and/or duration)
"file-difference-filename" : u"Name",
@ -225,6 +225,7 @@ de = {
"messages-label" : u"Nachrichten",
"messages-osd-title" : u"OSD-(OnScreenDisplay)-Einstellungen",
"messages-other-title" : u"Weitere Display-Einstellungen",
"chat-label" : u"Chat", # TODO: Translate
"privacy-label" : u"Privatsphäre",
"privacy-title" : u"Privatsphäreneinstellungen",
"unpause-title" : u"Wenn du Play drückst, auf Bereit setzen und:",
@ -234,6 +235,27 @@ de = {
"unpause-always" : u"Immer wiedergeben",
"syncplay-trusteddomains-title": u"Trusted domains (for streaming services and hosted content)", # TODO: Translate into German
"chat-title": u"Chat message input", # TODO: Translate
"chatinputenabled-label": u"Enable chat input via mpv (using enter key)", # TODO: Translate
"chatdirectinput-label" : u"Allow instant chat input (bypass having to press enter key to chat)", # TODO: Translate
"chatinputfont-label": u"Chat input font", # TODO: Translate
"chatfont-label": u"Set font", # TODO: Translate
"chatcolour-label": u"Set colour", # TODO: Translate
"chatinputposition-label": u"Position of message input area in mpv", # TODO: Translate
"chat-top-option": u"Top", # TODO: Translate
"chat-middle-option": u"Middle", # TODO: Translate
"chat-bottom-option": u"Bottom", # TODO: Translate
"chatoutputfont-label": u"Chat output font", # TODO: Translate
"chatoutputenabled-label": u"Enable chat output in media player (mpv only for now)", # TODO: Translate
"chatoutputposition-label": u"Output mode", # TODO: Translate
"chat-chatroom-option": u"Chatroom style", # TODO: Translate
"chat-scrolling-option": u"Scrolling style", # TODO: Translate
"mpv-key-tab-hint": u"[TAB] to toggle access to alphabet row key shortcuts.", # TODO: Translate
"mpv-key-hint": u"[ENTER] to send message. [ESC] to escape chat mode.", # TODO: Translate
"alphakey-mode-warning-first-line": u"You can temporarily use old mpv bindings with a-z keys.", # TODO: Translate
"alphakey-mode-warning-second-line": u"Press [TAB] to return to Syncplay chat mode.", # TODO: Translate
"help-label" : u"Hilfe",
"reset-label" : u"Standardwerte zurücksetzen",
"run-label" : u"Syncplay starten",
@ -357,6 +379,22 @@ de = {
"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
"chatinputenabled-tooltip": u"Enable chat input in mpv (press enter to chat, enter to send, escape to cancel)", # TODO: Translate
"chatdirectinput-tooltip" : u"Skip having to press 'enter' to go into chat input mode in mpv. Press TAB in mpv to temporarily disable this feature.", # TODO: Translate
"font-label-tooltip": u"Font used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.", # TODO: Translate
"set-input-font-tooltip": u"Font family used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.", # TODO: Translate
"set-input-colour-tooltip": u"Font colour used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.", # TODO: Translate
"chatinputposition-tooltip": u"Location in mpv where chat input text will appear when you press enter and type.", # TODO: Translate
"chatinputposition-top-tooltip": u"Place chat input at top of mpv window.", # TODO: Translate
"chatinputposition-middle-tooltip": u"Place chat input in dead centre of mpv window.", # TODO: Translate
"chatinputposition-bottom-tooltip": u"Place chat input at bottom of mpv window.", # TODO: Translate
"chatoutputenabled-tooltip": u"Show chat messages in OSD (if supported by media player).", # TODO: Translate
"font-output-label-tooltip": u"Chat output font.", # TODO: Translate
"set-output-font-tooltip": u"Font used for when displaying chat messages.", # TODO: Translate
"chatoutputmode-tooltip": u"How chat messages are displayed.", # TODO: Translate
"chatoutputmode-chatroom-tooltip": u"Display new lines of chat directly below previous line.", # TODO: Translate
"chatoutputmode-scrolling-tooltip": u"Scroll chat text from right to left.", # TODO: Translate
"help-tooltip" : u"Öffnet Hilfe auf syncplay.pl [Englisch]",
"reset-tooltip" : u"Alle Einstellungen auf Standardwerte zurücksetzen.",
"update-server-list-tooltip" : u"Mit syncplay.pl verbinden um die Liste öffentlicher Server zu aktualisieren.",
@ -420,6 +458,7 @@ de = {
"addurlstoplaylist-menu-label" : u"Add URL(s) to bottom of playlist",
"editplaylist-menu-label": u"Edit playlist",
"open-containing-folder": u"Open folder containing this file",
"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

View File

@ -17,23 +17,23 @@ en = {
"connected-successful-notification" : "Successfully connected to server",
"retrying-notification" : "%s, Retrying in %d seconds...", # Seconds
"rewind-notification" : "Rewinded due to time difference with <{}>", # User
"fastforward-notification" : "Fast-forwarded due to time difference with <{}>", # User
"slowdown-notification" : "Slowing down due to time difference with <{}>", # User
"rewind-notification" : "Rewinded due to time difference with {}", # User
"fastforward-notification" : "Fast-forwarded due to time difference with {}", # User
"slowdown-notification" : "Slowing down due to time difference with {}", # User
"revert-notification" : "Reverting speed back to normal",
"pause-notification" : u"<{}> paused", # User
"unpause-notification" : u"<{}> unpaused", # User
"seek-notification" : u"<{}> jumped from {} to {}", # User, from time, to time
"pause-notification" : u"{} paused", # User
"unpause-notification" : u"{} unpaused", # User
"seek-notification" : u"{} jumped from {} to {}", # User, from time, to time
"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
"playing-notification" : u"<{}> is playing '{}' ({})", # User, file, duration
"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
"playing-notification" : u"{} is playing '{}' ({})", # User, file, duration
"playing-notification/room-addendum" : u" in room: '{}'", # Room
"not-all-ready" : u"Not ready: {}", # Usernames
@ -44,11 +44,11 @@ en = {
"autoplaying-notification" : u"Auto-playing in {}...", # Number of seconds until playback will start
"identifying-as-controller-notification" : u"Identifying as room operator with password '{}'...",
"failed-to-identify-as-controller-notification" : u"<{}> failed to identify as a room operator.",
"authenticated-as-controller-notification" : u"<{}> authenticated as a room operator",
"failed-to-identify-as-controller-notification" : u"{} failed to identify as a room operator.",
"authenticated-as-controller-notification" : u"{} authenticated as a room operator",
"created-controlled-room-notification" : u"Created managed room '{}' with password '{}'. Please save this information for future reference!", # RoomName, operatorPassword
"file-different-notification" : "File you are playing appears to be different from <{}>'s", # User
"file-different-notification" : "File you are playing appears to be different from {}'s", # User
"file-differences-notification" : u"Your file differs in the following way(s): {}", # Differences
"room-file-differences" : u"File differences: {}", # File differences (filename, size, and/or duration)
"file-difference-filename" : u"name",
@ -133,7 +133,12 @@ en = {
"vlc-failed-noscript": "VLC has reported that the syncplay.lua interface script has not been installed. Please refer to http://syncplay.pl/LUA/ for instructions.",
"vlc-failed-versioncheck": "This version of VLC is not supported by Syncplay.",
"not-supported-by-server-error" : "This feature is not supported by the server. The feature requires a server running Syncplay {}+, but the server is running Syncplay {}.", #minVersion, serverVersion
"feature-sharedPlaylists" : u"shared playlists", # used for not-supported-by-server-error
"feature-chat" : u"chat", # used for not-supported-by-server-error
"feature-readiness" : u"readiness", # used for not-supported-by-server-error
"feature-managedRooms" : u"managed rooms", # used for not-supported-by-server-error
"not-supported-by-server-error" : u"The {} feature is not supported by this server..", #feature
"shared-playlists-not-supported-by-server-error" : "The shared playlists feature may not be supported by the server. To ensure that it works correctly requires a server running Syncplay {}+, but the server is running Syncplay {}.", #minVersion, serverVersion
"shared-playlists-disabled-by-server-error" : "The shared playlist feature has been disabled in the server configuration. To use this feature you will need to connect to a different server.",
@ -225,6 +230,7 @@ en = {
"messages-label" : "Messages",
"messages-osd-title" : "On-screen Display settings",
"messages-other-title" : "Other display settings",
"chat-label" : u"Chat",
"privacy-label" : "Privacy", # Currently unused, but will be brought back if more space is needed in Misc tab
"privacy-title" : "Privacy settings",
"unpause-title" : u"If you press play, set as ready and:",
@ -234,6 +240,27 @@ en = {
"unpause-always" : u"Always unpause",
"syncplay-trusteddomains-title": u"Trusted domains (for streaming services and hosted content)",
"chat-title" : u"Chat message input",
"chatinputenabled-label" : u"Enable chat input via mpv",
"chatdirectinput-label" : u"Allow instant chat input (bypass having to press enter key to chat)",
"chatinputfont-label" : u"Chat input font",
"chatfont-label" : u"Set font",
"chatcolour-label" : u"Set colour",
"chatinputposition-label" : u"Position of message input area in mpv",
"chat-top-option" : u"Top",
"chat-middle-option" : u"Middle",
"chat-bottom-option" : u"Bottom",
"chatoutputfont-label": u"Chat output font",
"chatoutputenabled-label": u"Enable chat output in media player (mpv only for now)",
"chatoutputposition-label": u"Output mode",
"chat-chatroom-option": u"Chatroom style",
"chat-scrolling-option": u"Scrolling style",
"mpv-key-tab-hint": u"[TAB] to toggle access to alphabet row key shortcuts.",
"mpv-key-hint": u"[ENTER] to send message. [ESC] to escape chat mode.",
"alphakey-mode-warning-first-line": u"You can temporarily use old mpv bindings with a-z keys.",
"alphakey-mode-warning-second-line": u"Press [TAB] to return to Syncplay chat mode.",
"help-label" : "Help",
"reset-label" : "Restore defaults",
"run-label" : "Run Syncplay",
@ -355,6 +382,22 @@ en = {
"unpause-ifminusersready-tooltip" : u"If you press unpause when not ready, it will only unpause 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.",
"chatinputenabled-tooltip" : u"Enable chat input in mpv (press enter to chat, enter to send, escape to cancel)",
"chatdirectinput-tooltip" : u"Skip having to press 'enter' to go into chat input mode in mpv. Press TAB in mpv to temporarily disable this feature.",
"font-label-tooltip" : u"Font used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.",
"set-input-font-tooltip" : u"Font family used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.",
"set-input-colour-tooltip" : u"Font colour used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.",
"chatinputposition-tooltip" : u"Location in mpv where chat input text will appear when you press enter and type.",
"chatinputposition-top-tooltip" : u"Place chat input at top of mpv window.",
"chatinputposition-middle-tooltip" : u"Place chat input in dead centre of mpv window.",
"chatinputposition-bottom-tooltip" : u"Place chat input at bottom of mpv window.",
"chatoutputenabled-tooltip": u"Show chat messages in OSD (if supported by media player).",
"font-output-label-tooltip": u"Chat output font.",
"set-output-font-tooltip": u"Font used for when displaying chat messages.",
"chatoutputmode-tooltip": u"How chat messages are displayed.",
"chatoutputmode-chatroom-tooltip": u"Display new lines of chat directly below previous line.",
"chatoutputmode-scrolling-tooltip": u"Scroll chat text from right to left.",
"help-tooltip" : "Opens the Syncplay.pl user guide.",
"reset-tooltip" : "Reset all settings to the default configuration.",
"update-server-list-tooltip" : u"Connect to syncplay.pl to update list of public servers.",

View File

@ -17,23 +17,23 @@ ru = {
"connected-successful-notification" : u"Соединение с сервером установлено",
"retrying-notification" : u"%s, следующая попытка через %d секунд(ы)...", # Seconds
"rewind-notification" : u"Перемотано из-за разницы во времени с <{}>", # User
"fastforward-notification" : u"Ускорено из-за разницы во времени с <{}>", # User
"slowdown-notification" : u"Воспроизведение замедлено из-за разницы во времени с <{}>", # User
"rewind-notification" : u"Перемотано из-за разницы во времени с {}", # User
"fastforward-notification" : u"Ускорено из-за разницы во времени с {}", # User
"slowdown-notification" : u"Воспроизведение замедлено из-за разницы во времени с {}", # User
"revert-notification" : u"Возвращаемся к нормальной скорости воспроизведения",
"pause-notification" : u"<{}> приостановил воспроизведение", # User
"unpause-notification" : u"<{}> возобновил воспроизведение", # User
"seek-notification" : u"<{}> перемотал с {} на {}", # User, from time, to time
"pause-notification" : u"{} приостановил воспроизведение", # User
"unpause-notification" : u"{} возобновил воспроизведение", # User
"seek-notification" : u"{} перемотал с {} на {}", # User, from time, to time
"current-offset-notification" : u"Текущее смещение: {} секунд(ы)", # Offset
"media-directory-list-updated-notification" : u"Папки воспроизведения обновлены.",
"room-join-notification" : u"<{}> зашел в комнату: '{}'", # User
"left-notification" : u"<{}> покинул комнату", # User
"left-paused-notification" : u"<{}> покинул комнату, <{}> приостановил воспроизведение", # User who left, User who paused
"playing-notification" : u"<{}> включил '{}' ({})", # User, file, duration
"room-join-notification" : u"{} зашел в комнату: '{}'", # User
"left-notification" : u"{} покинул комнату", # User
"left-paused-notification" : u"{} покинул комнату, {} приостановил воспроизведение", # User who left, User who paused
"playing-notification" : u"{} включил '{}' ({})", # User, file, duration
"playing-notification/room-addendum" : u" в комнате: '{}'", # Room
"not-all-ready" : u"Не готовы: {}", # Usernames
@ -44,11 +44,11 @@ ru = {
"autoplaying-notification" : u"Автовоспроизведение через {}...", # Number of seconds until playback will start
"identifying-as-controller-notification" : u"Идентификация как оператора комнаты с паролем '{}'...",
"failed-to-identify-as-controller-notification" : u"<{}> не прошел идентификацию в качестве оператора комнаты.",
"authenticated-as-controller-notification" : u"<{}> вошел как оператор комнаты.",
"failed-to-identify-as-controller-notification" : u"{} не прошел идентификацию в качестве оператора комнаты.",
"authenticated-as-controller-notification" : u"{} вошел как оператор комнаты.",
"created-controlled-room-notification" : u"Создана управляемая комната '{}' с паролем '{}'. Сохраните эти данные!", # RoomName, operatorPassword
"file-different-notification" : u"Вероятно, файл, который Вы смотрите, отличается от того, который смотрит <{}>.", # User
"file-different-notification" : u"Вероятно, файл, который Вы смотрите, отличается от того, который смотрит {}.", # User
"file-differences-notification" : u"Ваш файл отличается: {}", # Differences
"room-file-differences" : u"Несовпадения файла: {}", # File differences (filename, size, and/or duration)
"file-difference-filename" : u"имя",
@ -135,7 +135,13 @@ ru = {
"vlc-failed-versioncheck" : u"Данная версия VLC не поддерживается Syncplay. Пожалуйста, используйте VLC версии 2 или выше.",
"vlc-failed-other" : u"Во время загрузки скрипта интерфейса syncplay.lua в VLC произошла следующая ошибка: {}", # Syncplay Error
"not-supported-by-server-error" : u"Эта возможность не поддерживается сервером. Требуется сервер Syncplay {}+, вы подключены к серверу Syncplay {}.", #minVersion, serverVersion
"feature-sharedPlaylists": u"shared playlists", # used for not-supported-by-server-error # TODO: Translate
"feature-chat": u"chat", # used for not-supported-by-server-error # TODO: Translate
"feature-readiness": u"readiness", # used for not-supported-by-server-error # TODO: Translate
"feature-managedRooms": u"managed rooms", # used for not-supported-by-server-error # TODO: Translate
"not-supported-by-server-error": u"The {} feature is not supported by this server..", # feature # TODO: Translate
#OLD TRANSLATION: "not-supported-by-server-error" : u"Эта возможность не поддерживается сервером. Требуется сервер Syncplay {}+, вы подключены к серверу Syncplay {}.", #minVersion, serverVersion
"shared-playlists-not-supported-by-server-error" : u"Общие списки воспроизведения могут не поддерживаться сервером. Для корректной работы требуется сервер Syncplay {}+, вы подключены к серверу Syncplay {}.", #minVersion, serverVersion
"shared-playlists-disabled-by-server-error" : "The shared playlist feature has been disabled in the server configuration. To use this feature you will need to connect to a different server.", # TODO: Translate
@ -227,6 +233,7 @@ ru = {
"messages-label" : u"Сообщения",
"messages-osd-title" : u"Настройки OSD",
"messages-other-title" : u"Другие настройки отображения",
"chat-label" : u"Chat", # TODO: Translate
"privacy-label" : u"Приватность",
"privacy-title" : u"Настройки приватности",
"unpause-title" : u"Если вы стартуете, то:",
@ -237,6 +244,27 @@ ru = {
"syncplay-trusteddomains-title": u"Доверенные сайты (стрим-сервисы, видеохостинги, файлы в сети)",
"addtrusteddomain-menu-label" : u"Добавить {} как доверенный сайт", # Domain
"chat-title": u"Chat message input", # TODO: Translate
"chatinputenabled-label": u"Enable chat input via mpv (using enter key)", # TODO: Translate
"chatdirectinput-label" : u"Allow instant chat input (bypass having to press enter key to chat)", # TODO: Translate
"chatinputfont-label": u"Chat input font", # TODO: Translate
"chatfont-label": u"Set font", # TODO: Translate
"chatcolour-label": u"Set colour", # TODO: Translate
"chatinputposition-label": u"Position of message input area in mpv", # TODO: Translate
"chat-top-option": u"Top", # TODO: Translate
"chat-middle-option": u"Middle", # TODO: Translate
"chat-bottom-option": u"Bottom", # TODO: Translate
"chatoutputfont-label": u"Chat output font", # TODO: Translate
"chatoutputenabled-label": u"Enable chat output in media player (mpv only for now)", # TODO: Translate
"chatoutputposition-label": u"Output mode", # TODO: Translate
"chat-chatroom-option": u"Chatroom style", # TODO: Translate
"chat-scrolling-option": u"Scrolling style", # TODO: Translate
"mpv-key-tab-hint": u"[TAB] to toggle access to alphabet row key shortcuts.", # TODO: Translate
"mpv-key-hint": u"[ENTER] to send message. [ESC] to escape chat mode.", # TODO: Translate
"alphakey-mode-warning-first-line": u"You can temporarily use old mpv bindings with a-z keys.", # TODO: Translate
"alphakey-mode-warning-second-line": u"Press [TAB] to return to Syncplay chat mode.", # TODO: Translate
"help-label" : u"Помощь",
"reset-label" : u"Сброс настроек",
"run-label" : u"Запустить",
@ -357,6 +385,22 @@ ru = {
"unpause-ifminusersready-tooltip" : u"Когда вы стартуете не готовым, воспроизведение начнется, если остальные готовы и присутствует достаточное число зрителей.",
"trusteddomains-arguments-tooltip" : u"Сайты, которые разрешены для автоматического воспроизведения из общего списка воспроизведения.",
"chatinputenabled-tooltip": u"Enable chat input in mpv (press enter to chat, enter to send, escape to cancel)",# TODO: Translate
"chatdirectinput-tooltip" : u"Skip having to press 'enter' to go into chat input mode in mpv. Press TAB in mpv to temporarily disable this feature.", # TODO: Translate
"font-label-tooltip": u"Font used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.",# TODO: Translate
"set-input-font-tooltip": u"Font family used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.",# TODO: Translate
"set-input-colour-tooltip": u"Font colour used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.",# TODO: Translate
"chatinputposition-tooltip": u"Location in mpv where chat input text will appear when you press enter and type.",# TODO: Translate
"chatinputposition-top-tooltip": u"Place chat input at top of mpv window.", # TODO: Translate
"chatinputposition-middle-tooltip": u"Place chat input in dead centre of mpv window.", # TODO: Translate
"chatinputposition-bottom-tooltip": u"Place chat input at bottom of mpv window.", # TODO: Translate
"chatoutputenabled-tooltip": u"Show chat messages in OSD (if supported by media player).", # TODO: Translate
"font-output-label-tooltip": u"Chat output font.", # TODO: Translate
"set-output-font-tooltip": u"Font used for when displaying chat messages.", # TODO: Translate
"chatoutputmode-tooltip": u"How chat messages are displayed.", # TODO: Translate
"chatoutputmode-chatroom-tooltip": u"Display new lines of chat directly below previous line.", # TODO: Translate
"chatoutputmode-scrolling-tooltip": u"Scroll chat text from right to left.", # TODO: Translate
"help-tooltip" : u"Открыть Руководство Пользователя на Syncplay.pl.",
"reset-tooltip" : u"Сбрасывает все настройки Syncplay в начальное состояние.",
"update-server-list-tooltip" : u"Обновить список публичных серверов от syncplay.pl.",
@ -419,6 +463,7 @@ ru = {
"addurlstoplaylist-menu-label" : u"Добавить ссылку в очередь",
"editplaylist-menu-label": u"Редактировать список",
"open-containing-folder": u"Open folder containing this file", # TODO: Traslate
"addusersfiletoplaylist-menu-label" : u"Добавить файл {} в список воспроизведения", # item owner indicator
"addusersstreamstoplaylist-menu-label" : u"Добавить поток {} в список воспроизведения", # item owner indicator
"openusersstream-menu-label" : u"Открыть поток от {}", # [username]'s

View File

@ -12,7 +12,7 @@ class BasePlayer(object):
'''
Display given message on player's OSD or similar means
'''
def displayMessage(self, message, duration = (constants.OSD_DURATION*1000)):
def displayMessage(self, message, duration = (constants.OSD_DURATION*1000), secondaryOSD=False, mood=constants.MESSAGE_NEUTRAL):
raise NotImplementedError()
'''
@ -34,6 +34,12 @@ class BasePlayer(object):
def setPaused(self, value):
raise NotImplementedError()
'''
@type value: list
'''
def setFeatures(self, featureList):
raise NotImplementedError()
'''
@type value: float
'''

View File

@ -306,8 +306,9 @@ class MpcHcApi:
class MPCHCAPIPlayer(BasePlayer):
speedSupported = False
secondaryOSDSupported = False
alertOSDSupported = False
customOpenDialog = False
chatOSDSupported = False
osdMessageSeparator = "; "
def __init__(self, client):
@ -399,7 +400,7 @@ class MPCHCAPIPlayer(BasePlayer):
def openFile(self, filePath, resetPosition=False):
self._mpcApi.openFile(filePath)
def displayMessage(self, message, duration = (constants.OSD_DURATION*1000), secondaryOSD=False):
def displayMessage(self, message, duration = (constants.OSD_DURATION*1000), OSDType=constants.OSD_NOTIFICATION, mood=constants.MESSAGE_NEUTRAL):
self._mpcApi.sendOsd(message, constants.MPC_OSD_POSITION, duration)
@retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1)
@ -411,6 +412,8 @@ class MPCHCAPIPlayer(BasePlayer):
self._mpcApi.pause()
else:
self._mpcApi.unpause()
def setFeatures(self, featureList):
pass
@retry(MpcHcApi.PlayerNotReadyException, constants.MPC_MAX_RETRIES, constants.MPC_RETRY_WAIT_TIME, 1)
def setPosition(self, value):

View File

@ -1,3 +1,4 @@
# coding:utf8
import subprocess
import re
import threading
@ -11,7 +12,8 @@ from syncplay.utils import isWindows
class MplayerPlayer(BasePlayer):
speedSupported = True
customOpenDialog = False
secondaryOSDSupported = False
alertOSDSupported = False
chatOSDSupported = False
osdMessageSeparator = "; "
RE_ANSWER = re.compile(constants.MPLAYER_ANSWER_REGEX)
@ -88,8 +90,15 @@ class MplayerPlayer(BasePlayer):
def _getProperty(self, property_):
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, self._stripNewlines(message), duration, constants.MPLAYER_OSD_LEVEL).encode('utf-8'))
def displayMessage(self, message, duration=(constants.OSD_DURATION * 1000), OSDType=constants.OSD_NOTIFICATION, mood=constants.MESSAGE_NEUTRAL):
messageString = self._sanitizeText(message.replace("\\n", "<NEWLINE>")).replace("<NEWLINE>", "\\n")
self._listener.sendLine(u'{} "{!s}" {} {}'.format(self.OSD_QUERY, messageString, duration, constants.MPLAYER_OSD_LEVEL).encode('utf-8'))
def displayChatMessage(self, username, message):
messageString = u"<{}> {}".format(username, message)
messageString = self._sanitizeText(messageString.replace("\\n", "<NEWLINE>")).replace("<NEWLINE>", "\\n")
duration = int(constants.OSD_DURATION * 1000)
self._listener.sendLine(u'{} "{!s}" {} {}'.format(self.OSD_QUERY, messageString, duration, constants.MPLAYER_OSD_LEVEL).encode('utf-8'))
def setSpeed(self, value):
self._setProperty('speed', "{:.2f}".format(value))
@ -105,6 +114,9 @@ class MplayerPlayer(BasePlayer):
self.setPaused(self._client.getGlobalPaused())
self.setPosition(self._client.getGlobalPosition())
def setFeatures(self, featureList):
pass
def setPosition(self, value):
self._position = max(value,0)
self._setProperty(self.POSITION_QUERY, "{}".format(value))
@ -130,9 +142,16 @@ class MplayerPlayer(BasePlayer):
def _getPosition(self):
self._getProperty(self.POSITION_QUERY)
def _stripNewlines(self, text):
def _sanitizeText(self, text):
text = text.replace("\r", "")
text = text.replace("\n", "")
text = text.replace("\\\"", "<SYNCPLAY_QUOTE>")
text = text.replace("\"", "<SYNCPLAY_QUOTE>")
text = text.replace("%", "%%")
text = text.replace("\\", "\\\\")
text = text.replace("{", "\\\\{")
text = text.replace("}", "\\\\}")
text = text.replace("<SYNCPLAY_QUOTE>","\\\"")
return text
def _quoteArg(self, arg):
@ -158,6 +177,8 @@ class MplayerPlayer(BasePlayer):
def lineReceived(self, line):
if line:
self._client.ui.showDebugMessage("player << {}".format(line))
line = line.replace("[cplayer] ", "") # -v workaround
line = line.replace("[term-msg] ", "") # -v workaround
line = line.replace(" cplayer: ","") # --msg-module workaround
line = line.replace(" term-msg: ", "")
if "Failed to get value of property" in line or "=(unavailable)" in line or line == "ANS_filename=" or line == "ANS_length=" or line == "ANS_path=":
@ -277,6 +298,9 @@ class MplayerPlayer(BasePlayer):
self.lastSendTime = None
self.lastNotReadyTime = None
self.__playerController = playerController
if not self.__playerController._client._config["chatOutputEnabled"]:
self.__playerController.alertOSDSupported = False
self.__playerController.chatOSDSupported = False
if self.__playerController.getPlayerPathErrors(playerPath,filePath):
raise ValueError()
if filePath and '://' not in filePath:
@ -332,6 +356,18 @@ class MplayerPlayer(BasePlayer):
self.__playerController.lineReceived(line)
self.__playerController.drop()
def sendChat(self, message):
if message:
if message[:1] == "/" and message <> "/":
command = message[1:]
if command and command[:1] == "/":
message = message[1:]
else:
self.__playerController.reactor.callFromThread(self.__playerController._client.ui.executeCommand,
command)
return
self.__playerController.reactor.callFromThread(self.__playerController._client.sendChat, message)
def isReadyForSend(self):
self.checkForReadinessOverride()
return self.readyToSend

View File

@ -1,14 +1,16 @@
# coding:utf8
import re
import subprocess
from syncplay.players.mplayer import MplayerPlayer
from syncplay.messages import getMessage
from syncplay import constants
from syncplay.utils import isURL
from syncplay.utils import isURL, findResourcePath
import os, sys, time
class MpvPlayer(MplayerPlayer):
RE_VERSION = re.compile('.*mpv (\d+)\.(\d+)\.\d+.*')
osdMessageSeparator = "\\n"
osdMessageSeparator = "; " # TODO: Make conditional
@staticmethod
def run(client, playerPath, filePath, args):
@ -17,6 +19,9 @@ class MpvPlayer(MplayerPlayer):
except:
ver = None
constants.MPV_NEW_VERSION = ver is None or int(ver.group(1)) > 0 or int(ver.group(2)) >= 6
constants.MPV_OSC_VISIBILITY_CHANGE_VERSION = False if ver is None else int(ver.group(1)) > 0 or int(ver.group(2)) >= 28
if not constants.MPV_OSC_VISIBILITY_CHANGE_VERSION:
client.ui.showDebugMessage(u"This version of mpv is not known to be compatible with changing the OSC visibility. Please use mpv >=0.28.0.")
if constants.MPV_NEW_VERSION:
return NewMpvPlayer(client, MpvPlayer.getExpandedPath(playerPath), filePath, args)
else:
@ -30,6 +35,7 @@ class MpvPlayer(MplayerPlayer):
args.extend(constants.MPV_SLAVE_ARGS)
if constants.MPV_NEW_VERSION:
args.extend(constants.MPV_SLAVE_ARGS_NEW)
args.extend([u"--script={}".format(findResourcePath("syncplayintf.lua"))])
return args
@staticmethod
@ -108,6 +114,25 @@ class OldMpvPlayer(MpvPlayer):
class NewMpvPlayer(OldMpvPlayer):
lastResetTime = None
lastMPVPositionUpdate = None
alertOSDSupported = True
chatOSDSupported = True
def displayMessage(self, message, duration=(constants.OSD_DURATION * 1000), OSDType=constants.OSD_NOTIFICATION,
mood=constants.MESSAGE_NEUTRAL):
if not self._client._config["chatOutputEnabled"]:
super(self.__class__, self).displayMessage(message=message,duration=duration,OSDType=OSDType,mood=mood)
return
messageString = self._sanitizeText(message.replace("\\n", "<NEWLINE>")).replace("\\\\",constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER).replace("<NEWLINE>", "\\n")
self._listener.sendLine(u'script-message-to syncplayintf {}-osd-{} "{}"'.format(OSDType, mood, messageString))
def displayChatMessage(self, username, message):
if not self._client._config["chatOutputEnabled"]:
super(self.__class__, self).displayChatMessage(username,message)
return
username = self._sanitizeText(username.replace("\\",constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER))
message = self._sanitizeText(message.replace("\\",constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER))
messageString = u"<{}> {}".format(username, message)
self._listener.sendLine(u'script-message-to syncplayintf chat "{}"'.format(messageString))
def setPaused(self, value):
if self._paused == value:
@ -199,6 +224,9 @@ class NewMpvPlayer(OldMpvPlayer):
self._clearFileLoaded()
self._listener.sendLine(u'loadfile {}'.format(self._quoteArg(filePath)), notReadyAfterThis=True)
def setFeatures(self, featureList):
self.sendMpvOptions()
def setPosition(self, value):
if value < constants.DO_NOT_RESET_POSITION_THRESHOLD and self._recentlyReset():
self._client.ui.showDebugMessage("Did not seek as recently reset and {} below 'do not reset position' threshold".format(value))
@ -222,9 +250,29 @@ class NewMpvPlayer(OldMpvPlayer):
else:
self._storePosition(0)
def sendMpvOptions(self):
options = []
for option in constants.MPV_SYNCPLAYINTF_OPTIONS_TO_SEND:
options.append(u"{}={}".format(option, self._client._config[option]))
for option in constants.MPV_SYNCPLAYINTF_CONSTANTS_TO_SEND:
options.append(option)
for option in constants.MPV_SYNCPLAYINTF_LANGUAGE_TO_SEND:
options.append(u"{}={}".format(option, getMessage(option)))
options.append(u"OscVisibilityChangeCompatible={}".format(constants.MPV_OSC_VISIBILITY_CHANGE_VERSION))
options_string = ", ".join(options)
self._listener.sendLine(u'script-message-to syncplayintf set_syncplayintf_options "{}"'.format(options_string))
self._setOSDPosition()
def _handleUnknownLine(self, line):
self.mpvErrorCheck(line)
if "<chat>" in line:
line = line.decode("utf-8").replace(constants.MPV_INPUT_BACKSLASH_SUBSTITUTE_CHARACTER, "\\").encode("utf-8")
self._listener.sendChat(line[6:-7])
if "<get_syncplayintf_options>" in line:
self.sendMpvOptions()
if line == "<SyncplayUpdateFile>" or "Playing:" in line:
self._listener.setReadyToSend(False)
self._clearFileLoaded()
@ -236,6 +284,11 @@ class NewMpvPlayer(OldMpvPlayer):
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 _setOSDPosition(self):
if self._client._config['chatMoveOSD'] and (self._client._config['chatOutputEnabled'] or (self._client._config['chatInputEnabled'] and self._client._config['chatInputPosition'] == constants.INPUT_POSITION_TOP)):
self._setProperty("osd-align-y", "bottom")
self._setProperty("osd-margin-y", int(self._client._config['chatOSDMargin']))
def _recentlyReset(self):
if not self.lastResetTime:
return False

View File

@ -16,7 +16,8 @@ from syncplay.utils import isBSD, isLinux, isWindows, isMacOS
class VlcPlayer(BasePlayer):
speedSupported = True
customOpenDialog = False
secondaryOSDSupported = True
chatOSDSupported = False
alertOSDSupported = True
osdMessageSeparator = "; "
RE_ANSWER = re.compile(constants.VLC_ANSWER_REGEX)
@ -118,9 +119,9 @@ class VlcPlayer(BasePlayer):
else:
return self._position
def displayMessage(self, message, duration=constants.OSD_DURATION * 1000, secondaryOSD=False):
def displayMessage(self, message, duration=constants.OSD_DURATION * 1000, OSDType=constants.OSD_DURATION, mood=constants.MESSAGE_NEUTRAL):
duration /= 1000
if secondaryOSD == False:
if OSDType != constants.OSD_ALERT:
self._listener.sendLine('display-osd: {}, {}, {}'.format('top-right', duration, message.encode('utf8')))
else:
self._listener.sendLine('display-secondary-osd: {}, {}, {}'.format('center', duration, message.encode('utf8')))
@ -128,6 +129,9 @@ class VlcPlayer(BasePlayer):
def setSpeed(self, value):
self._listener.sendLine("set-rate: {:.2n}".format(value))
def setFeatures(self, featureList):
pass
def setPosition(self, value):
self._lastVLCPositionUpdate = time.time()
self._listener.sendLine("set-position: {}".format(value).replace(".",self.radixChar))

View File

@ -5,8 +5,8 @@ import syncplay
from functools import wraps
import time
from syncplay.messages import getMessage
from syncplay.constants import PING_MOVING_AVERAGE_WEIGHT
from syncplay.constants import PING_MOVING_AVERAGE_WEIGHT, CONTROLLED_ROOMS_MIN_VERSION, USER_READY_MIN_VERSION, SHARED_PLAYLIST_MIN_VERSION, CHAT_MIN_VERSION
from syncplay.utils import meetsMinVersion
class JSONCommandProtocol(LineReceiver):
def handleMessages(self, messages):
@ -107,6 +107,7 @@ class SyncClientProtocol(JSONCommandProtocol):
if room: hello["room"] = {"name" :room}
hello["version"] = "1.2.255" # Used so newer clients work on 1.2.X server
hello["realversion"] = syncplay.version
hello["features"] = self._client.getFeatures()
self.sendMessage({"Hello": hello})
def _SetUser(self, users):
@ -147,6 +148,11 @@ class SyncClientProtocol(JSONCommandProtocol):
self._client.playlist.changeToPlaylistIndex(values['index'], values['user'])
elif command == "playlistChange":
self._client.playlist.changePlaylist(values['files'], values['user'])
elif command == "features":
self._client.setUserFeatures(values["username"],values['features'])
def sendFeaturesUpdate(self, features):
self.sendSet({"features": features})
def sendSet(self, setting):
self.sendMessage({"Set": setting})
@ -173,7 +179,8 @@ class SyncClientProtocol(JSONCommandProtocol):
file_ = user[1]['file'] if user[1]['file'] <> {} else None
isController = user[1]['controller'] if 'controller' in user[1] else False
isReady = user[1]['isReady'] if 'isReady' in user[1] else None
self._client.userlist.addUser(userName, roomName, file_, noMessage=True, isController=isController, isReady=isReady)
features = user[1]['features'] if 'features' in user[1] else None
self._client.userlist.addUser(userName, roomName, file_, noMessage=True, isController=isController, isReady=isReady, features=features)
self._client.userlist.showUserList()
def sendList(self):
@ -249,9 +256,9 @@ class SyncClientProtocol(JSONCommandProtocol):
}
})
def handleChat(self,message):
username = message['username']
userMessage = message['message']
messageString = u"<{}> {}".format(message['username'], userMessage)
self._client.ui.showMessage(messageString)
self._client.ui.showChatMessage(username, userMessage)
def setReady(self, isReady, manuallyInitiated=True):
self.sendSet({
@ -286,6 +293,7 @@ class SyncServerProtocol(JSONCommandProtocol):
def __init__(self, factory):
self._factory = factory
self._version = None
self._features = None
self._logged = False
self.clientIgnoringOnTheFly = 0
self.serverIgnoringOnTheFly = 0
@ -319,6 +327,16 @@ class SyncServerProtocol(JSONCommandProtocol):
def connectionLost(self, reason):
self._factory.removeWatcher(self._watcher)
def getFeatures(self):
if not self._features:
self._features = {}
self._features["sharedPlaylists"] = meetsMinVersion(self._version, SHARED_PLAYLIST_MIN_VERSION)
self._features["chat"] = meetsMinVersion(self._version, CHAT_MIN_VERSION)
self._features["featureList"] = False
self._features["readiness"] = meetsMinVersion(self._version, USER_READY_MIN_VERSION)
self._features["managedRooms"] = meetsMinVersion(self._version, CONTROLLED_ROOMS_MIN_VERSION)
return self._features
def isLogged(self):
return self._logged
@ -345,7 +363,8 @@ class SyncServerProtocol(JSONCommandProtocol):
roomName = None
version = hello["version"] if hello.has_key("version") else None
version = hello["realversion"] if hello.has_key("realversion") else version
return username, serverPassword, roomName, version
features = hello["features"] if hello.has_key("features") else None
return username, serverPassword, roomName, version, features
def _checkPassword(self, serverPassword):
if self._factory.password:
@ -358,7 +377,7 @@ class SyncServerProtocol(JSONCommandProtocol):
return True
def handleHello(self, hello):
username, serverPassword, roomName, version = self._extractHelloArguments(hello)
username, serverPassword, roomName, version, features = self._extractHelloArguments(hello)
if not username or not roomName or not version:
self.dropWithError(getMessage("hello-server-error"))
return
@ -366,14 +385,22 @@ class SyncServerProtocol(JSONCommandProtocol):
if not self._checkPassword(serverPassword):
return
self._version = version
self.setFeatures(features)
self._factory.addWatcher(self, username, roomName)
self._logged = True
self.sendHello(version)
@requireLogged
def handleChat(self,chatMessage):
if not self._factory.disableChat:
self._factory.sendChat(self._watcher,chatMessage)
def setFeatures(self, features):
self._features = features
def sendFeaturesUpdate(self):
self.sendSet({"features": self.getFeatures()})
def setWatcher(self, watcher):
self._watcher = watcher
@ -410,6 +437,9 @@ class SyncServerProtocol(JSONCommandProtocol):
self._factory.setPlaylist(self._watcher, set_[1]['files'])
elif command == "playlistIndex":
self._factory.setPlaylistIndex(self._watcher, set_[1]['index'])
elif command == "features":
#TODO: Check
self._watcher.setFeatures(set_[1])
def sendSet(self, setting):
self.sendMessage({"Set": setting})
@ -476,7 +506,8 @@ class SyncServerProtocol(JSONCommandProtocol):
"position": 0,
"file": watcher.getFile() if watcher.getFile() else {},
"controller": watcher.isController(),
"isReady": watcher.isReady()
"isReady": watcher.isReady(),
"features": watcher.getFeatures()
}
userlist[room.getName()][watcher.getName()] = userFile

View File

@ -48,6 +48,11 @@ class SyncFactory(Factory):
features["readiness"] = not self.disableReady
features["managedRooms"] = True
features["chat"] = not self.disableChat
features["maxChatMessageLength"] = constants.MAX_CHAT_MESSAGE_LENGTH
features["maxUsernameLength"] = constants.MAX_USERNAME_LENGTH
features["maxRoomNameLength"] = constants.MAX_ROOM_NAME_LENGTH
features["maxFilenameLength"] = constants.MAX_FILENAME_LENGTH
return features
def getMotd(self, userIp, username, room, clientVersion):
@ -108,7 +113,7 @@ class SyncFactory(Factory):
self._roomManager.broadcast(watcher, l)
def sendJoinMessage(self, watcher):
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"joined": True, "version": watcher.getVersion()}) if w != watcher else None
l = lambda w: w.sendSetting(watcher.getName(), watcher.getRoom(), None, {"joined": True, "version": watcher.getVersion(), "features": watcher.getFeatures()}) if w != watcher else None
self._roomManager.broadcast(watcher, l)
self._roomManager.broadcastRoom(watcher, lambda w: w.sendSetReady(watcher.getName(), watcher.isReady(), False))
@ -413,6 +418,10 @@ class Watcher(object):
def setReady(self, ready):
self._ready = ready
def getFeatures(self):
features = self._connector.getFeatures()
return features
def isReady(self):
if self._server.disableReady:
return None

View File

@ -8,6 +8,7 @@ from syncplay.messages import getMessage, setLanguage, isValidLanguage
from syncplay.players.playerFactory import PlayerFactory
from syncplay.utils import isMacOS
import codecs
import re
class InvalidConfigValue(Exception):
def __init__(self, message):
@ -64,6 +65,29 @@ class ConfigurationGetter(object):
"showNonControllerOSD" : False,
"showContactInfo" : True,
"showDurationNotification" : True,
"chatInputEnabled" : True,
"chatInputFontFamily" : 'sans-serif',
"chatInputRelativeFontSize" : constants.DEFAULT_CHAT_FONT_SIZE,
"chatInputFontWeight" : constants.DEFAULT_CHAT_FONT_WEIGHT,
"chatInputFontUnderline": False,
"chatInputFontColor": constants.DEFAULT_CHAT_INPUT_FONT_COLOR,
"chatInputPosition": constants.INPUT_POSITION_TOP,
"chatDirectInput": False,
"chatOutputEnabled": True,
"chatOutputFontFamily": 'sans-serif',
"chatOutputRelativeFontSize": constants.DEFAULT_CHAT_FONT_SIZE,
"chatOutputFontWeight": constants.DEFAULT_CHAT_FONT_WEIGHT,
"chatOutputFontUnderline": False,
"chatOutputMode": constants.CHATROOM_MODE,
"chatMaxLines": 7,
"chatTopMargin": 25,
"chatLeftMargin": 20,
"chatBottomMargin": 30,
"chatMoveOSD": True,
"chatOSDMargin": 110,
"notificationTimeout": 3,
"alertTimeout": 5,
"chatTimeout": 7,
"publicServers" : []
}
@ -106,7 +130,13 @@ class ConfigurationGetter(object):
"sharedPlaylistEnabled",
"loopAtEndOfPlaylist",
"loopSingleFiles",
"onlySwitchToTrustedDomains"
"onlySwitchToTrustedDomains",
"chatInputEnabled",
"chatInputFontUnderline",
"chatDirectInput",
"chatMoveOSD",
"chatOutputEnabled",
"chatOutputFontUnderline"
]
self._tristate = [
"checkForUpdatesAutomatically",
@ -125,6 +155,22 @@ class ConfigurationGetter(object):
"rewindThreshold",
"fastforwardThreshold",
"autoplayMinUsers",
"chatInputRelativeFontSize",
"chatInputFontWeight",
"chatOutputFontWeight",
"chatOutputRelativeFontSize",
"chatMaxLines",
"chatTopMargin",
"chatLeftMargin",
"chatBottomMargin",
"chatOSDMargin",
"notificationTimeout",
"alertTimeout",
"chatTimeout"
]
self._hexadecimal = [
"chatInputFontColor"
]
self._iniStructure = {
@ -143,7 +189,19 @@ class ConfigurationGetter(object):
"onlySwitchToTrustedDomains", "trustedDomains","publicServers"],
"gui": ["showOSD", "showOSDWarnings", "showSlowdownOSD",
"showDifferentRoomOSD", "showSameRoomOSD",
"showNonControllerOSD", "showDurationNotification"],
"showNonControllerOSD", "showDurationNotification",
"chatInputEnabled","chatInputFontUnderline",
"chatInputFontFamily", "chatInputRelativeFontSize",
"chatInputFontWeight", "chatInputFontColor",
"chatInputPosition","chatDirectInput",
"chatOutputFontFamily", "chatOutputRelativeFontSize",
"chatOutputFontWeight", "chatOutputFontUnderline",
"chatOutputMode", "chatMaxLines",
"chatTopMargin", "chatLeftMargin",
"chatBottomMargin", "chatDirectInput",
"chatMoveOSD", "chatOSDMargin",
"notificationTimeout", "alertTimeout",
"chatTimeout","chatOutputEnabled"],
"general": ["language", "checkForUpdatesAutomatically",
"lastCheckedForUpdates"]
}
@ -197,6 +255,11 @@ class ConfigurationGetter(object):
for key in self._numeric:
self._config[key] = float(self._config[key])
for key in self._hexadecimal:
match = re.search(r'^#(?:[0-9a-fA-F]){6}$', self._config[key])
if not match:
self._config[key] = u"#FFFFFF"
for key in self._required:
if key == "playerPath":
player = None

View File

@ -12,7 +12,7 @@ import sys
import threading
from syncplay.messages import getMessage, getLanguages, setLanguage, getInitialLanguage
from syncplay import constants
from syncplay.utils import isBSD, isLinux, isMacOS
from syncplay.utils import isBSD, isLinux, isMacOS, isWindows
from syncplay.utils import resourcespath, posixresourcespath
class GuiConfiguration:
def __init__(self, config, error=None, defaultConfig=None):
@ -289,6 +289,7 @@ class ConfigDialog(QtWidgets.QDialog):
def loadLastUpdateCheckDate(self):
settings = QSettings("Syncplay", "Interface")
settings.beginGroup("Update")
try:
self.lastCheckedForUpdates = settings.value("lastCheckedQt", None)
if self.lastCheckedForUpdates:
if self.config["lastCheckedForUpdates"] is not None and self.config["lastCheckedForUpdates"] is not "":
@ -296,6 +297,8 @@ class ConfigDialog(QtWidgets.QDialog):
self.config["lastCheckedForUpdates"] = self.lastCheckedForUpdates.toString("yyyy-MM-d HH:mm:ss.z")
else:
self.config["lastCheckedForUpdates"] = self.lastCheckedForUpdates.toString("yyyy-MM-d HH:mm:ss.z")
except:
self.lastCheckedForUpdates = None
def loadSavedPublicServerList(self):
settings = QSettings("Syncplay", "Interface")
@ -515,7 +518,7 @@ class ConfigDialog(QtWidgets.QDialog):
def connectChildren(self, widget):
widgetName = str(widget.objectName())
if self.subitems.has_key(widgetName) and isinstance(widget, QCheckBox):
if self.subitems.has_key(widgetName):
widget.stateChanged.connect(lambda: self.updateSubwidgets(self, widget))
self.updateSubwidgets(self, widget)
@ -869,6 +872,155 @@ class ConfigDialog(QtWidgets.QDialog):
self.syncSettingsLayout.setAlignment(Qt.AlignTop)
self.stackedLayout.addWidget(self.syncSettingsFrame)
def addChatTab(self):
self.chatFrame = QtWidgets.QFrame()
self.chatLayout = QtWidgets.QVBoxLayout()
self.chatLayout.setAlignment(Qt.AlignTop)
# Input
self.chatInputGroup = QtWidgets.QGroupBox(getMessage("chat-title"))
self.chatInputLayout = QtWidgets.QGridLayout()
self.chatLayout.addWidget(self.chatInputGroup)
self.chatInputGroup.setLayout(self.chatInputLayout)
self.chatInputEnabledCheckbox = QCheckBox(getMessage("chatinputenabled-label"))
self.chatInputEnabledCheckbox.setObjectName("chatInputEnabled")
self.chatInputLayout.addWidget(self.chatInputEnabledCheckbox, 1, 0, 1,1, Qt.AlignLeft)
self.chatDirectInputCheckbox = QCheckBox(getMessage("chatdirectinput-label"))
self.chatDirectInputCheckbox.setObjectName("chatDirectInput")
self.chatDirectInputCheckbox.setStyleSheet(
constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png"))
self.chatInputLayout.addWidget(self.chatDirectInputCheckbox, 2, 0, 1,1, Qt.AlignLeft)
self.inputFontLayout = QtWidgets.QHBoxLayout()
self.inputFontLayout.setContentsMargins(0, 0, 0, 0)
self.inputFontFrame = QtWidgets.QFrame()
self.inputFontFrame.setLayout(self.inputFontLayout)
self.inputFontFrame.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
self.chatFontLabel = QLabel(getMessage("chatinputfont-label"), self)
self.chatFontLabel.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png"))
self.chatFontLabel.setObjectName("font-label")
self.chatInputFontButton = QtWidgets.QPushButton(getMessage("chatfont-label"))
self.chatInputFontButton.setObjectName("set-input-font")
self.chatInputFontButtonGroup = QtWidgets.QButtonGroup()
self.chatInputFontButtonGroup.addButton(self.chatInputFontButton)
self.chatInputFontButton.released.connect(lambda: self.fontDialog("chatInput"))
self.chatInputColourButton = QtWidgets.QPushButton(getMessage("chatcolour-label"))
self.chatInputColourButton.setObjectName("set-input-colour")
self.chatInputColourButtonGroup = QtWidgets.QButtonGroup()
self.chatInputColourButtonGroup.addButton(self.chatInputColourButton)
self.chatInputColourButton.released.connect(lambda: self.colourDialog("chatInput"))
self.inputFontLayout.addWidget(self.chatFontLabel, Qt.AlignLeft)
self.inputFontLayout.addWidget(self.chatInputFontButton, Qt.AlignLeft)
self.inputFontLayout.addWidget(self.chatInputColourButton, Qt.AlignLeft)
self.chatInputLayout.addWidget(self.inputFontFrame, 3, 0, 1, 3, Qt.AlignLeft)
self.chatInputPositionFrame = QtWidgets.QFrame()
self.chatInputPositionLayout = QtWidgets.QHBoxLayout()
self.chatInputPositionLayout.setContentsMargins(0, 0, 0, 0)
self.chatInputPositionFrame.setLayout(self.chatInputPositionLayout)
self.chatInputPositionFrame.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
self.chatInputPositionLabel = QLabel(getMessage("chatinputposition-label"), self)
self.chatInputPositionLabel.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png"))
self.chatInputPositionGroup = QButtonGroup()
self.chatInputTopOption = QRadioButton(getMessage("chat-top-option"))
self.chatInputMiddleOption = QRadioButton(getMessage("chat-middle-option"))
self.chatInputBottomOption = QRadioButton(getMessage("chat-bottom-option"))
self.chatInputPositionGroup.addButton(self.chatInputTopOption)
self.chatInputPositionGroup.addButton(self.chatInputMiddleOption)
self.chatInputPositionGroup.addButton(self.chatInputBottomOption)
self.chatInputPositionLabel.setObjectName("chatinputposition")
self.chatInputTopOption.setObjectName("chatinputposition-top" + constants.CONFIG_NAME_MARKER + "chatInputPosition" + constants.CONFIG_VALUE_MARKER + constants.INPUT_POSITION_TOP)
self.chatInputMiddleOption.setObjectName("chatinputposition-middle" + constants.CONFIG_NAME_MARKER + "chatInputPosition" + constants.CONFIG_VALUE_MARKER + constants.INPUT_POSITION_MIDDLE)
self.chatInputBottomOption.setObjectName("chatinputposition-bottom" + constants.CONFIG_NAME_MARKER + "chatInputPosition" + constants.CONFIG_VALUE_MARKER + constants.INPUT_POSITION_BOTTOM)
self.chatInputPositionLayout.addWidget(self.chatInputPositionLabel)
self.chatInputPositionLayout.addWidget(self.chatInputTopOption)
self.chatInputPositionLayout.addWidget(self.chatInputMiddleOption)
self.chatInputPositionLayout.addWidget(self.chatInputBottomOption)
self.chatInputLayout.addWidget(self.chatInputPositionFrame)
self.subitems['chatInputEnabled'] = [self.chatInputPositionLabel.objectName(), self.chatInputTopOption.objectName(),
self.chatInputMiddleOption.objectName(), self.chatInputBottomOption.objectName(),
self.chatInputFontButton.objectName(), self.chatFontLabel.objectName(),
self.chatInputColourButton.objectName(), self.chatDirectInputCheckbox.objectName()]
# Output
self.chatOutputGroup = QtWidgets.QGroupBox(u"Chat message output")
self.chatOutputLayout = QtWidgets.QGridLayout()
self.chatLayout.addWidget(self.chatOutputGroup)
self.chatOutputGroup.setLayout(self.chatOutputLayout)
self.chatOutputEnabledCheckbox = QCheckBox(getMessage("chatoutputenabled-label"))
self.chatOutputEnabledCheckbox.setObjectName("chatOutputEnabled")
self.chatOutputLayout.addWidget(self.chatOutputEnabledCheckbox, 1, 0, 1,1, Qt.AlignLeft)
self.outputFontLayout = QtWidgets.QHBoxLayout()
self.outputFontLayout.setContentsMargins(0, 0, 0, 0)
self.outputFontFrame = QtWidgets.QFrame()
self.outputFontFrame.setLayout(self.outputFontLayout)
self.outputFontFrame.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
self.chatOutputFontLabel = QLabel(getMessage("chatoutputfont-label"), self)
self.chatOutputFontLabel.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png"))
self.chatOutputFontLabel.setObjectName("font-output-label")
self.chatOutputFontButton = QtWidgets.QPushButton(getMessage("chatfont-label"))
self.chatOutputFontButton.setObjectName("set-output-font")
self.chatOutputFontButtonGroup = QtWidgets.QButtonGroup()
self.chatOutputFontButtonGroup.addButton(self.chatOutputFontButton)
self.chatOutputFontButton.released.connect(lambda: self.fontDialog("chatOutput"))
self.chatOutputColourButton = QtWidgets.QPushButton(getMessage("chatcolour-label"))
self.outputFontLayout.addWidget(self.chatOutputFontLabel, Qt.AlignLeft)
self.outputFontLayout.addWidget(self.chatOutputFontButton, Qt.AlignLeft)
self.chatOutputLayout.addWidget(self.outputFontFrame, 2, 0, 1, 3, Qt.AlignLeft)
self.chatOutputModeLabel = QLabel(getMessage("chatoutputposition-label"), self)
self.chatOutputModeLabel.setStyleSheet(constants.STYLE_SUBCHECKBOX.format(self.posixresourcespath + u"chevrons_right.png"))
self.chatOutputModeGroup = QButtonGroup()
self.chatOutputChatroomOption = QRadioButton(getMessage("chat-chatroom-option"))
self.chatOutputScrollingOption = QRadioButton(getMessage("chat-scrolling-option"))
self.chatOutputModeGroup.addButton(self.chatOutputChatroomOption)
self.chatOutputModeGroup.addButton(self.chatOutputScrollingOption)
self.chatOutputModeLabel.setObjectName("chatoutputmode")
self.chatOutputChatroomOption.setObjectName("chatoutputmode-chatroom" + constants.CONFIG_NAME_MARKER + "chatOutputMode" + constants.CONFIG_VALUE_MARKER + constants.CHATROOM_MODE)
self.chatOutputScrollingOption.setObjectName("chatoutputmode-scrolling" + constants.CONFIG_NAME_MARKER + "chatOutputMode" + constants.CONFIG_VALUE_MARKER + constants.SCROLLING_MODE)
self.chatOutputModeFrame = QtWidgets.QFrame()
self.chatOutputModeLayout = QtWidgets.QHBoxLayout()
self.chatOutputModeLayout.setContentsMargins(0, 0, 0, 0)
self.chatOutputModeFrame.setLayout(self.chatOutputModeLayout)
self.chatOutputModeFrame.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
self.chatOutputModeLayout.addWidget(self.chatOutputModeLabel)
self.chatOutputModeLayout.addWidget(self.chatOutputChatroomOption)
self.chatOutputModeLayout.addWidget(self.chatOutputScrollingOption)
self.chatOutputLayout.addWidget(self.chatOutputModeFrame)
self.subitems['chatOutputEnabled'] = [self.chatOutputModeLabel.objectName(), self.chatOutputChatroomOption.objectName(),
self.chatOutputScrollingOption.objectName(),self.chatOutputFontButton.objectName(),
self.chatOutputFontLabel.objectName()]
# chatFrame
self.chatFrame.setLayout(self.chatLayout)
self.stackedLayout.addWidget(self.chatFrame)
def fontDialog(self, configName):
font = QtGui.QFont()
font.setFamily(self.config[configName+ u"FontFamily"])
font.setPointSize(self.config[configName + u"RelativeFontSize"])
font.setWeight(self.config[configName + u"FontWeight"])
font.setUnderline(self.config[configName + u"FontUnderline"])
value, ok = QtWidgets.QFontDialog.getFont(font)
if ok:
self.config[configName + u"FontFamily"] = value.family()
self.config[configName + u"RelativeFontSize"] = value.pointSize()
self.config[configName + u"FontWeight"] = value.weight()
self.config[configName + u"FontUnderline"] = value.underline()
def colourDialog(self, configName):
oldColour = QtGui.QColor()
oldColour.setNamedColor(self.config[configName+ u"FontColor"])
colour = QtWidgets.QColorDialog.getColor(oldColour, self)
if colour.isValid():
self.config[configName + u"FontColor"] = colour.name()
def addMessageTab(self):
self.messageFrame = QtWidgets.QFrame()
self.messageLayout = QtWidgets.QVBoxLayout()
@ -967,18 +1119,20 @@ class ConfigDialog(QtWidgets.QDialog):
self.helpButton = QtWidgets.QPushButton(QtGui.QIcon(resourcespath + u'help.png'), getMessage("help-label"))
self.helpButton.setObjectName("help")
self.helpButton.setMaximumSize(self.helpButton.sizeHint())
self.helpButton.pressed.connect(self.openHelp)
self.helpButton.released.connect(self.openHelp)
self.resetButton = QtWidgets.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.resetButton.released.connect(self.resetSettings)
self.runButton = QtWidgets.QPushButton(QtGui.QIcon(resourcespath + u'accept.png'), getMessage("run-label"))
self.runButton.released.connect(self._runWithoutStoringConfig)
self.runButton = QtWidgets.QPushButton(QtGui.QIcon(resourcespath + u'accept.png'), getMessage("run-label"))
self.runButton.pressed.connect(self._runWithoutStoringConfig)
self.runButton.setToolTip(getMessage("nostore-tooltip"))
self.storeAndRunButton = QtWidgets.QPushButton(QtGui.QIcon(resourcespath + u'accept.png'), getMessage("storeandrun-label"))
self.storeAndRunButton.pressed.connect(self._saveDataAndLeave)
self.storeAndRunButton.released.connect(self._saveDataAndLeave)
self.bottomButtonLayout.addWidget(self.helpButton)
self.bottomButtonLayout.addWidget(self.resetButton)
self.bottomButtonLayout.addWidget(self.runButton)
@ -1008,7 +1162,8 @@ class ConfigDialog(QtWidgets.QDialog):
self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"house.png"),getMessage("basics-label")))
self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"control_pause_blue.png"),getMessage("readiness-label")))
self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"film_link.png"),getMessage("sync-label")))
self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"comments.png"),getMessage("messages-label")))
self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"user_comment.png"), getMessage("chat-label")))
self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"error.png"),getMessage("messages-label")))
self.tabListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(resourcespath + u"cog.png"),getMessage("misc-label")))
self.tabListLayout.addWidget(self.tabListWidget)
self.tabListFrame.setLayout(self.tabListLayout)
@ -1108,6 +1263,12 @@ class ConfigDialog(QtWidgets.QDialog):
self.QtWidgets = QtWidgets
self.QtGui = QtGui
self.error = error
if isWindows():
resourcespath = utils.findWorkingDir() + "\\resources\\"
else:
resourcespath = utils.findWorkingDir() + u"/resources/"
self.posixresourcespath = utils.findWorkingDir().replace(u"\\","/") + u"/resources/"
self.resourcespath = resourcespath
super(ConfigDialog, self).__init__()
@ -1127,6 +1288,7 @@ class ConfigDialog(QtWidgets.QDialog):
self.addBasicTab()
self.addReadinessTab()
self.addSyncTab()
self.addChatTab()
self.addMessageTab()
self.addMiscTab()
self.tabList()

View File

@ -38,7 +38,7 @@ class ConsoleUI(threading.Thread):
self.PromptResult = data
self.promptMode.set()
elif self._syncplayClient:
self._executeCommand(data)
self.executeCommand(data)
except EOFError:
pass
@ -136,7 +136,7 @@ class ConsoleUI(threading.Thread):
return True
return False
def _executeCommand(self, data):
def executeCommand(self, data):
command = re.match(constants.UI_COMMAND_REGEX, data)
if not command:
return
@ -145,7 +145,7 @@ class ConsoleUI(threading.Thread):
self._syncplayClient.setPosition(self._syncplayClient.playerPositionBeforeLastSeek)
self._syncplayClient.playerPositionBeforeLastSeek = tmp_pos
elif command.group('command') in constants.COMMANDS_LIST:
self._syncplayClient.getUserList()
self.getUserlist()
elif command.group('command') in constants.COMMANDS_CHAT:
message= command.group('parameter')
self._syncplayClient.sendChat(message)
@ -158,8 +158,8 @@ class ConsoleUI(threading.Thread):
room = self._syncplayClient.userlist.currentUser.file["name"]
else:
room = self._syncplayClient.defaultRoom
self._syncplayClient.setRoom(room, resetAutoplay=True)
self._syncplayClient.ui.updateRoomName(room)
self._syncplayClient.sendRoom()
elif command.group('command') in constants.COMMANDS_CREATE:
roombasename = command.group('parameter')
@ -191,3 +191,5 @@ class ConsoleUI(threading.Thread):
self.showMessage(getMessage("syncplay-version-notification").format(syncplay.version), True)
self.showMessage(getMessage("more-info-notification").format(syncplay.projectURL), True)
def getUserlist(self):
self._syncplayClient.getUserList()

View File

@ -16,10 +16,27 @@ import os
from syncplay.utils import formatTime, sameFilename, sameFilesize, sameFileduration, RoomPasswordProvider, formatSize, isURL
from functools import wraps
from twisted.internet import task
from syncplay.ui.consoleUI import ConsoleUI
if isMacOS() and IsPySide:
from Foundation import NSURL
lastCheckedForUpdates = None
class ConsoleInGUI(ConsoleUI):
def showMessage(self, message, noTimestamp=False):
self._syncplayClient.ui.showMessage(message, True)
def showDebugMessage(self, message):
self._syncplayClient.ui.showDebugMessage(message)
def showErrorMessage(self, message, criticalerror=False):
self._syncplayClient.ui.showErrorMessage(message, criticalerror)
def updateRoomName(self, room=""):
self._syncplayClient.ui.updateRoomName(room)
def getUserlist(self):
self._syncplayClient.showUserList(self)
class UserlistItemDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self):
QtWidgets.QStyledItemDelegate.__init__(self)
@ -84,7 +101,6 @@ class UserlistItemDelegate(QtWidgets.QStyledItemDelegate):
QtWidgets.QStyledItemDelegate.paint(self, itemQPainter, optionQStyleOptionViewItem, indexQModelIndex)
class AboutDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(AboutDialog, self).__init__(parent)
if isMacOS():
@ -347,6 +363,8 @@ class MainWindow(QtWidgets.QMainWindow):
def addClient(self, client):
self._syncplayClient = client
if self.console:
self.console.addClient(client)
self.roomInput.setText(self._syncplayClient.getRoom())
self.config = self._syncplayClient.getConfig()
try:
@ -385,6 +403,8 @@ class MainWindow(QtWidgets.QMainWindow):
self.chatInput.setReadOnly(True)
if not featureList["sharedPlaylists"]:
self.playlistGroup.setEnabled(False)
self.chatInput.setMaxLength(constants.MAX_CHAT_MESSAGE_LENGTH)
self.roomInput.setMaxLength(constants.MAX_ROOM_NAME_LENGTH)
def showMessage(self, message, noTimestamp=False):
message = unicode(message)
@ -1126,10 +1146,22 @@ class MainWindow(QtWidgets.QMainWindow):
self._syncplayClient.playlist.changePlaylist(newPlaylist)
self._syncplayClient.fileSwitch.updateInfo()
def executeCommand(self, command):
self.showMessage(u"/{}".format(command))
self.console.executeCommand(command)
def sendChatMessage(self):
if self.chatInput.text() <> "":
self._syncplayClient.sendChat(self.chatInput.text())
chatText = self.chatInput.text()
self.chatInput.setText("")
if chatText <> "":
if chatText[:1] == "/" and chatText <> "/":
command = chatText[1:]
if command and command[:1] == "/":
chatText = chatText[1:]
else:
self.executeCommand(command)
return
self._syncplayClient.sendChat(chatText)
def addTopLayout(self, window):
window.topSplit = self.topSplitter(Qt.Horizontal, self)
@ -1705,6 +1737,8 @@ class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.console = ConsoleInGUI()
self.console.setDaemon(True)
self.newWatchlist = []
self.publicServerList = []
self.lastCheckedForUpdates = None

View File

@ -130,6 +130,13 @@ def formatSize (bytes, precise=False):
def isASCII(s):
return all(ord(c) < 128 for c in s)
def findResourcePath(resourceName):
if resourceName == "syncplay.lua":
resourcePath = os.path.join(findWorkingDir(), "lua", "intf" , "resources", resourceName)
else:
resourcePath = os.path.join(findWorkingDir(),"resources", resourceName)
return resourcePath
def findWorkingDir():
frozen = getattr(sys, 'frozen', '')
if not frozen:
@ -152,6 +159,14 @@ def getResourcesPath():
resourcespath = getResourcesPath()
posixresourcespath = findWorkingDir().replace(u"\\","/") + u"/resources/"
def getDefaultMonospaceFont():
if platform.system() == "Windows":
return constants.DEFAULT_WINDOWS_MONOSPACE_FONT
elif platform.system() == "Darwin":
return constants.DEFAULT_OSX_MONOSPACE_FONT
else:
return constants.FALLBACK_MONOSPACE_FONT
def limitedPowerset(s, minLength):
return itertools.chain.from_iterable(itertools.combinations(s, r) for r in xrange(len(s), minLength, -1))
@ -189,20 +204,28 @@ def blackholeStdoutForFrozenWindow():
def truncateText(unicodeText, maxLength):
try:
unicodeText = unicodedata.normalize('NFC', unicodeText)
unicodeText = unicodeText.decode('utf-8')
except:
pass
try:
maxSaneLength= maxLength*5
if len(unicodeText) > maxSaneLength:
unicodeText = unicode(unicodeText.encode("utf-8")[:maxSaneLength], "utf-8", errors="ignore")
while len(unicodeText) > maxLength:
unicodeText = unicode(unicodeText.encode("utf-8")[:-1], "utf-8", errors="ignore")
return unicodeText
return(unicode(unicodeText.encode("utf-8"), "utf-8", errors="ignore")[:maxLength])
except:
pass
return ""
def splitText(unicodeText, maxLength):
try:
unicodeText = unicodeText.decode('utf-8')
except:
pass
try:
unicodeText = unicode(unicodeText.encode("utf-8"), "utf-8", errors="ignore")
unicodeArray = [unicodeText[i:i + maxLength] for i in range(0, len(unicodeText), maxLength)]
return(unicodeArray)
except:
pass
return [""]
# Relate to file hashing / difference checking:
def stripfilename(filename, stripURL):