diff --git a/docs/syncplay-gui-snapshot-fr.pdf b/docs/syncplay-gui-snapshot-fr.pdf
new file mode 100644
index 0000000..3fcf4b7
Binary files /dev/null and b/docs/syncplay-gui-snapshot-fr.pdf differ
diff --git a/syncplay/ass2messages.py b/syncplay/ass2messages.py
new file mode 100644
index 0000000..6c04920
--- /dev/null
+++ b/syncplay/ass2messages.py
@@ -0,0 +1,86 @@
+# coding:utf8
+
+# ass2messages.py, dictionary to subtitle exporter to automate translation
+# author sosie-js
+# require my pythonfx mofied version adding fixes and del_line facility
+#==========================================
+
+# For relative imports to work
+import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))
+from pyonfx import *
+
+# will include message_en.py a dictiatary of English messages for the syncplay gui
+# https://raw.githubusercontent.com/Syncplay/syncplay/master/syncplay/messages_en.py
+import messages_en
+
+
+lang="fr"
+dict_message_file="messages_"+lang
+ass_message_file=dict_message_file+".ass"
+dict_message_file=dict_message_file+".py"
+print("Welcome on the ass file %s to %s dictionary to exporter" % (dict_message_file,ass_message_file) )
+print("-------------------------------------------------------------------------------------")
+
+io = Ass(ass_message_file)
+meta, styles, lines = io.get_data()
+
+#messages will hold the message dict
+messages=messages_en.en
+i=len(lines)
+pos_time=0
+
+
+dict={}
+#the fist line of Untitled.ass, empty, will serve as template
+for line in lines:
+ dict[str(line.effect)]=str(line.raw_text)
+
+
+script_dir=os.path.dirname(os.path.realpath(__file__))
+path_input=os.path.join(script_dir,"messages_en.py")
+input = open(path_input, "r", encoding="utf-8-sig")
+template=input.read()
+input.close()
+
+note='# This file was mainly auto generated from ass2messages.py applied on '+ass_message_file+' to get these messages\n'
+note+='# its format has been harmonized, values are always stored in doublequotes strings, \n'
+note+='# if double quoted string in the value then they should be esacaped like this \\". There is\n'
+note+='# thus no reason to have single quoted strings. Tabs \\t and newlines \\n need also to be escaped.\n'
+note+='# whith ass2messages.py which handles these issues, this is no more a nightmare to handle. \n'
+note+='# I fixed partially messages_en.py serving as template. an entry should be added in messages.py:\n'
+note+='# "'+lang+'": messages_'+lang+'.'+lang+', . Produced by sosie - sos-productions.com\n\n'
+
+
+template=template.replace('en = {',note+lang+' = {')
+
+#Normalize space (and quotes!), launch the cleaner machine
+template=template.replace('": "','": "')
+template=template.replace('": \'','": \'')
+
+def escapeBackslashAndTabs(s):
+ s = s.replace("\t", "\\t")
+ s = s.replace("\n", "\\n")
+ return s
+
+def escapeDoublequotes(s):
+ s = s.replace('"', '\\"')
+ return s
+
+for key, value in messages.items():
+ value=value.replace("&","&")
+ if(key == "LANGUAGE"):
+ language=value
+ source=('"%s": "%s"' % (key, escapeBackslashAndTabs(value)))
+ target=('"%s": "%s"' % (key,dict[key]))
+ print(key+ ': "'+value+'" => "'+dict[key]+'"')
+ template=template.replace(source,target)
+ source=('"%s": \'%s\'' % (key, escapeBackslashAndTabs(value)))
+ target=('"%s": "%s"' % (key,escapeDoublequotes(dict[key])))
+ template=template.replace(source,target)
+template=template.replace('English dictionary',language+' dictionary')
+
+path_output=os.path.join(script_dir,dict_message_file)
+with open(path_output,"w") as f:
+ f.write(template)
+
+#print(template)
diff --git a/syncplay/messages.py b/syncplay/messages.py
index 23ed80a..af4e101 100755
--- a/syncplay/messages.py
+++ b/syncplay/messages.py
@@ -4,6 +4,7 @@ from syncplay import constants
from . import messages_en
from . import messages_ru
from . import messages_de
+from . import messages_fr
from . import messages_it
from . import messages_es
from . import messages_pt_BR
@@ -15,6 +16,7 @@ messages = {
"de": messages_de.de,
"en": messages_en.en,
"es": messages_es.es,
+ "fr": messages_fr.fr,
"it": messages_it.it,
"pt_PT": messages_pt_PT.pt_PT,
"pt_BR": messages_pt_BR.pt_BR,
diff --git a/syncplay/messages2ass.py b/syncplay/messages2ass.py
new file mode 100644
index 0000000..2d48291
--- /dev/null
+++ b/syncplay/messages2ass.py
@@ -0,0 +1,51 @@
+# coding:utf8
+
+# messages2ass.py, dictionary to subtitle exporter to automate translation
+# author sosie-js
+# require my pythonfx mofied version adding fixes and del_line facility
+#==========================================
+
+# For relative imports to work
+import os, sys; sys.path.append(os.path.dirname(os.path.realpath(__file__)))
+from pyonfx import *
+
+# will include message_en.py a dictiatary of English messages for the syncplay gui
+# https://raw.githubusercontent.com/Syncplay/syncplay/master/syncplay/messages_en.py
+import messages_en
+
+dict_message_file="message_en" #.py
+ass_message_file=dict_message_file+".ass"
+print("Welcome on the %s dictionary to ass file %s exporter" % (dict_message_file,ass_message_file) )
+print("-------------------------------------------------------------------------------------")
+
+io = Ass() #Will use aegisub template Untitled.ass as basis instead of "in.ass"
+io.set_output(ass_message_file)
+meta, styles, lines = io.get_data()
+
+#messages will hold the message dict
+messages=messages_en.en
+i=len(lines)
+pos_time=0
+
+#the fist line of Untitled.ass, empty, will serve as template
+line= lines[0].copy()
+duration=2000
+
+for key, value in messages.items():
+ print("Exporting value of key %s as subtiyle line" % key)
+ l= line.copy()
+ i=i+1
+ l.i=i
+ l.start_time = pos_time
+ l.end_time = pos_time+duration
+ l.effect= key
+ l.text =value
+ io.write_line(l)
+ pos_time=pos_time+duration
+
+#Don't forget to remove the pollution lines of the template
+# in our case remove the empty single line of Untitled.ass.
+io.del_line(1)
+
+io.save()
+io.open_aegisub()
\ No newline at end of file
diff --git a/syncplay/messages_en.ass b/syncplay/messages_en.ass
new file mode 100644
index 0000000..3f4aed2
--- /dev/null
+++ b/syncplay/messages_en.ass
@@ -0,0 +1,450 @@
+[Script Info]
+; Script generated by Aegisub 3.2.2
+; http://www.aegisub.org/
+Title: Default Aegisub file
+ScriptType: v4.00+
+WrapStyle: 0
+ScaledBorderAndShadow: yes
+YCbCr Matrix: None
+
+[Aegisub Project Garbage]
+
+[V4+ Styles]
+Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
+Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1
+
+[Events]
+Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
+
+Dialogue: 0,0:00:00.00,0:00:02.00,Default,,0000,0000,0000,LANGUAGE,English
+Dialogue: 0,0:00:02.00,0:00:04.00,Default,,0000,0000,0000,config-cleared-notification,Settings cleared. Changes will be saved when you store a valid configuration.
+Dialogue: 0,0:00:04.00,0:00:06.00,Default,,0000,0000,0000,relative-config-notification,Loaded relative configuration file(s): {}
+Dialogue: 0,0:00:06.00,0:00:08.00,Default,,0000,0000,0000,connection-attempt-notification,Attempting to connect to {}:{}
+Dialogue: 0,0:00:08.00,0:00:10.00,Default,,0000,0000,0000,reconnection-attempt-notification,Connection with server lost, attempting to reconnect
+Dialogue: 0,0:00:10.00,0:00:12.00,Default,,0000,0000,0000,disconnection-notification,Disconnected from server
+Dialogue: 0,0:00:12.00,0:00:14.00,Default,,0000,0000,0000,connection-failed-notification,Connection with server failed
+Dialogue: 0,0:00:14.00,0:00:16.00,Default,,0000,0000,0000,connected-successful-notification,Successfully connected to server
+Dialogue: 0,0:00:16.00,0:00:18.00,Default,,0000,0000,0000,retrying-notification,%s, Retrying in %d seconds...
+Dialogue: 0,0:00:18.00,0:00:20.00,Default,,0000,0000,0000,reachout-successful-notification,Successfully reached {} ({})
+Dialogue: 0,0:00:20.00,0:00:22.00,Default,,0000,0000,0000,rewind-notification,Rewinded due to time difference with {}
+Dialogue: 0,0:00:22.00,0:00:24.00,Default,,0000,0000,0000,fastforward-notification,Fast-forwarded due to time difference with {}
+Dialogue: 0,0:00:24.00,0:00:26.00,Default,,0000,0000,0000,slowdown-notification,Slowing down due to time difference with {}
+Dialogue: 0,0:00:26.00,0:00:28.00,Default,,0000,0000,0000,revert-notification,Reverting speed back to normal
+Dialogue: 0,0:00:28.00,0:00:30.00,Default,,0000,0000,0000,pause-notification,{} paused
+Dialogue: 0,0:00:30.00,0:00:32.00,Default,,0000,0000,0000,unpause-notification,{} unpaused
+Dialogue: 0,0:00:32.00,0:00:34.00,Default,,0000,0000,0000,seek-notification,{} jumped from {} to {}
+Dialogue: 0,0:00:34.00,0:00:36.00,Default,,0000,0000,0000,current-offset-notification,Current offset: {} seconds
+Dialogue: 0,0:00:36.00,0:00:38.00,Default,,0000,0000,0000,media-directory-list-updated-notification,Syncplay media directories have been updated.
+Dialogue: 0,0:00:38.00,0:00:40.00,Default,,0000,0000,0000,room-join-notification,{} has joined the room: '{}'
+Dialogue: 0,0:00:40.00,0:00:42.00,Default,,0000,0000,0000,left-notification,{} has left
+Dialogue: 0,0:00:42.00,0:00:44.00,Default,,0000,0000,0000,left-paused-notification,{} left, {} paused
+Dialogue: 0,0:00:44.00,0:00:46.00,Default,,0000,0000,0000,playing-notification,{} is playing '{}' ({})
+Dialogue: 0,0:00:46.00,0:00:48.00,Default,,0000,0000,0000,playing-notification/room-addendum, in room: '{}'
+Dialogue: 0,0:00:48.00,0:00:50.00,Default,,0000,0000,0000,not-all-ready,Not ready: {}
+Dialogue: 0,0:00:50.00,0:00:52.00,Default,,0000,0000,0000,all-users-ready,Everyone is ready ({} users)
+Dialogue: 0,0:00:52.00,0:00:54.00,Default,,0000,0000,0000,ready-to-unpause-notification,You are now set as ready - unpause again to unpause
+Dialogue: 0,0:00:54.00,0:00:56.00,Default,,0000,0000,0000,set-as-ready-notification,You are now set as ready
+Dialogue: 0,0:00:56.00,0:00:58.00,Default,,0000,0000,0000,set-as-not-ready-notification,You are now set as not ready
+Dialogue: 0,0:00:58.00,0:01:00.00,Default,,0000,0000,0000,autoplaying-notification,Auto-playing in {}...
+Dialogue: 0,0:01:00.00,0:01:02.00,Default,,0000,0000,0000,identifying-as-controller-notification,Identifying as room operator with password '{}'...
+Dialogue: 0,0:01:02.00,0:01:04.00,Default,,0000,0000,0000,failed-to-identify-as-controller-notification,{} failed to identify as a room operator.
+Dialogue: 0,0:01:04.00,0:01:06.00,Default,,0000,0000,0000,authenticated-as-controller-notification,{} authenticated as a room operator
+Dialogue: 0,0:01:06.00,0:01:08.00,Default,,0000,0000,0000,created-controlled-room-notification,Created managed room '{}' with password '{}'. Please save this information for future reference!
+
+In managed rooms everyone is kept in sync with the room operator(s) who are the only ones who can pause, unpause, seek, and change the playlist.
+
+You should ask regular viewers to join the room '{}' but the room operators can join the room '{}' to automatically authenticate themselves.
+Dialogue: 0,0:01:08.00,0:01:10.00,Default,,0000,0000,0000,file-different-notification,File you are playing appears to be different from {}'s
+Dialogue: 0,0:01:10.00,0:01:12.00,Default,,0000,0000,0000,file-differences-notification,Your file differs in the following way(s): {}
+Dialogue: 0,0:01:12.00,0:01:14.00,Default,,0000,0000,0000,room-file-differences,File differences: {}
+Dialogue: 0,0:01:14.00,0:01:16.00,Default,,0000,0000,0000,file-difference-filename,name
+Dialogue: 0,0:01:16.00,0:01:18.00,Default,,0000,0000,0000,file-difference-filesize,size
+Dialogue: 0,0:01:18.00,0:01:20.00,Default,,0000,0000,0000,file-difference-duration,duration
+Dialogue: 0,0:01:20.00,0:01:22.00,Default,,0000,0000,0000,alone-in-the-room,You're alone in the room
+Dialogue: 0,0:01:22.00,0:01:24.00,Default,,0000,0000,0000,different-filesize-notification, (their file size is different from yours!)
+Dialogue: 0,0:01:24.00,0:01:26.00,Default,,0000,0000,0000,userlist-playing-notification,{} is playing:
+Dialogue: 0,0:01:26.00,0:01:28.00,Default,,0000,0000,0000,file-played-by-notification,File: {} is being played by:
+Dialogue: 0,0:01:28.00,0:01:30.00,Default,,0000,0000,0000,no-file-played-notification,{} is not playing a file
+Dialogue: 0,0:01:30.00,0:01:32.00,Default,,0000,0000,0000,notplaying-notification,People who are not playing any file:
+Dialogue: 0,0:01:32.00,0:01:34.00,Default,,0000,0000,0000,userlist-room-notification,In room '{}':
+Dialogue: 0,0:01:34.00,0:01:36.00,Default,,0000,0000,0000,userlist-file-notification,File
+Dialogue: 0,0:01:36.00,0:01:38.00,Default,,0000,0000,0000,controller-userlist-userflag,Operator
+Dialogue: 0,0:01:38.00,0:01:40.00,Default,,0000,0000,0000,ready-userlist-userflag,Ready
+Dialogue: 0,0:01:40.00,0:01:42.00,Default,,0000,0000,0000,update-check-failed-notification,Could not automatically check whether Syncplay {} is up to date. Want to visit https://syncplay.pl/ to manually check for updates?
+Dialogue: 0,0:01:42.00,0:01:44.00,Default,,0000,0000,0000,syncplay-uptodate-notification,Syncplay is up to date
+Dialogue: 0,0:01:44.00,0:01:46.00,Default,,0000,0000,0000,syncplay-updateavailable-notification,A new version of Syncplay is available. Do you want to visit the release page?
+Dialogue: 0,0:01:46.00,0:01:48.00,Default,,0000,0000,0000,mplayer-file-required-notification,Syncplay using mplayer requires you to provide file when starting
+Dialogue: 0,0:01:48.00,0:01:50.00,Default,,0000,0000,0000,mplayer-file-required-notification/example,Usage example: syncplay [options] [url|path/]filename
+Dialogue: 0,0:01:50.00,0:01:52.00,Default,,0000,0000,0000,mplayer2-required,Syncplay is incompatible with MPlayer 1.x, please use mplayer2 or mpv
+Dialogue: 0,0:01:52.00,0:01:54.00,Default,,0000,0000,0000,unrecognized-command-notification,Unrecognized command
+Dialogue: 0,0:01:54.00,0:01:56.00,Default,,0000,0000,0000,commandlist-notification,Available commands:
+Dialogue: 0,0:01:56.00,0:01:58.00,Default,,0000,0000,0000,commandlist-notification/room, r [name] - change room
+Dialogue: 0,0:01:58.00,0:02:00.00,Default,,0000,0000,0000,commandlist-notification/list, l - show user list
+Dialogue: 0,0:02:00.00,0:02:02.00,Default,,0000,0000,0000,commandlist-notification/undo, u - undo last seek
+Dialogue: 0,0:02:02.00,0:02:04.00,Default,,0000,0000,0000,commandlist-notification/pause, p - toggle pause
+Dialogue: 0,0:02:04.00,0:02:06.00,Default,,0000,0000,0000,commandlist-notification/seek, [s][+-]time - seek to the given value of time, if + or - is not specified it's absolute time in seconds or min:sec
+Dialogue: 0,0:02:06.00,0:02:08.00,Default,,0000,0000,0000,commandlist-notification/help, h - this help
+Dialogue: 0,0:02:08.00,0:02:10.00,Default,,0000,0000,0000,commandlist-notification/toggle, t - toggles whether you are ready to watch or not
+Dialogue: 0,0:02:10.00,0:02:12.00,Default,,0000,0000,0000,commandlist-notification/create, c [name] - create managed room using name of current room
+Dialogue: 0,0:02:12.00,0:02:14.00,Default,,0000,0000,0000,commandlist-notification/auth, a [password] - authenticate as room operator with operator password
+Dialogue: 0,0:02:14.00,0:02:16.00,Default,,0000,0000,0000,commandlist-notification/chat, ch [message] - send a chat message in a room
+Dialogue: 0,0:02:16.00,0:02:18.00,Default,,0000,0000,0000,commandList-notification/queue, qa [file/url] - add file or url to bottom of playlist
+Dialogue: 0,0:02:18.00,0:02:20.00,Default,,0000,0000,0000,commandList-notification/playlist, ql - show the current playlist
+Dialogue: 0,0:02:20.00,0:02:22.00,Default,,0000,0000,0000,commandList-notification/select, qs [index] - select given entry in the playlist
+Dialogue: 0,0:02:22.00,0:02:24.00,Default,,0000,0000,0000,commandList-notification/delete, qd [index] - delete the given entry from the playlist
+Dialogue: 0,0:02:24.00,0:02:26.00,Default,,0000,0000,0000,syncplay-version-notification,Syncplay version: {}
+Dialogue: 0,0:02:26.00,0:02:28.00,Default,,0000,0000,0000,more-info-notification,More info available at: {}
+Dialogue: 0,0:02:28.00,0:02:30.00,Default,,0000,0000,0000,gui-data-cleared-notification,Syncplay has cleared the path and window state data used by the GUI.
+Dialogue: 0,0:02:30.00,0:02:32.00,Default,,0000,0000,0000,language-changed-msgbox-label,Language will be changed when you run Syncplay.
+Dialogue: 0,0:02:32.00,0:02:34.00,Default,,0000,0000,0000,promptforupdate-label,Is it okay for Syncplay to automatically check for updates from time to time?
+Dialogue: 0,0:02:34.00,0:02:36.00,Default,,0000,0000,0000,media-player-latency-warning,Warning: The media player took {} seconds to respond. If you experience syncing issues then close applications to free up system resources, and if that doesn't work then try a different media player.
+Dialogue: 0,0:02:36.00,0:02:38.00,Default,,0000,0000,0000,mpv-unresponsive-error,mpv has not responded for {} seconds so appears to have malfunctioned. Please restart Syncplay.
+Dialogue: 0,0:02:38.00,0:02:40.00,Default,,0000,0000,0000,enter-to-exit-prompt,Press enter to exit
+
+Dialogue: 0,0:02:40.00,0:02:42.00,Default,,0000,0000,0000,missing-arguments-error,Some necessary arguments are missing, refer to --help
+Dialogue: 0,0:02:42.00,0:02:44.00,Default,,0000,0000,0000,server-timeout-error,Connection with server timed out
+Dialogue: 0,0:02:44.00,0:02:46.00,Default,,0000,0000,0000,mpc-slave-error,Unable to start MPC in slave mode!
+Dialogue: 0,0:02:46.00,0:02:48.00,Default,,0000,0000,0000,mpc-version-insufficient-error,MPC version not sufficient, please use `mpc-hc` >= `{}`
+Dialogue: 0,0:02:48.00,0:02:50.00,Default,,0000,0000,0000,mpc-be-version-insufficient-error,MPC version not sufficient, please use `mpc-be` >= `{}`
+Dialogue: 0,0:02:50.00,0:02:52.00,Default,,0000,0000,0000,mpv-version-error,Syncplay is not compatible with this version of mpv. Please use a different version of mpv (e.g. Git HEAD).
+Dialogue: 0,0:02:52.00,0:02:54.00,Default,,0000,0000,0000,mpv-failed-advice,The reason mpv cannot start may be due to the use of unsupported command line arguments or an unsupported version of mpv.
+Dialogue: 0,0:02:54.00,0:02:56.00,Default,,0000,0000,0000,player-file-open-error,Player failed opening file
+Dialogue: 0,0:02:56.00,0:02:58.00,Default,,0000,0000,0000,player-path-error,Player path is not set properly. Supported players are: mpv, mpv.net, VLC, MPC-HC, MPC-BE, mplayer2, and IINA
+Dialogue: 0,0:02:58.00,0:03:00.00,Default,,0000,0000,0000,hostname-empty-error,Hostname can't be empty
+Dialogue: 0,0:03:00.00,0:03:02.00,Default,,0000,0000,0000,empty-error,{} can't be empty
+Dialogue: 0,0:03:02.00,0:03:04.00,Default,,0000,0000,0000,media-player-error,Media player error: "{}"
+Dialogue: 0,0:03:04.00,0:03:06.00,Default,,0000,0000,0000,unable-import-gui-error,Could not import GUI libraries. If you do not have PySide installed then you will need to install it for the GUI to work.
+Dialogue: 0,0:03:06.00,0:03:08.00,Default,,0000,0000,0000,unable-import-twisted-error,Could not import Twisted. Please install Twisted v16.4.0 or later.
+Dialogue: 0,0:03:08.00,0:03:10.00,Default,,0000,0000,0000,arguments-missing-error,Some necessary arguments are missing, refer to --help
+Dialogue: 0,0:03:10.00,0:03:12.00,Default,,0000,0000,0000,unable-to-start-client-error,Unable to start client
+Dialogue: 0,0:03:12.00,0:03:14.00,Default,,0000,0000,0000,player-path-config-error,Player path is not set properly. Supported players are: mpv, mpv.net, VLC, MPC-HC, MPC-BE, mplayer2, and IINA.
+Dialogue: 0,0:03:14.00,0:03:16.00,Default,,0000,0000,0000,no-file-path-config-error,File must be selected before starting your player
+Dialogue: 0,0:03:16.00,0:03:18.00,Default,,0000,0000,0000,no-hostname-config-error,Hostname can't be empty
+Dialogue: 0,0:03:18.00,0:03:20.00,Default,,0000,0000,0000,invalid-port-config-error,Port must be valid
+Dialogue: 0,0:03:20.00,0:03:22.00,Default,,0000,0000,0000,empty-value-config-error,{} can't be empty
+Dialogue: 0,0:03:22.00,0:03:24.00,Default,,0000,0000,0000,not-json-error,Not a json encoded string
+
+Dialogue: 0,0:03:24.00,0:03:26.00,Default,,0000,0000,0000,hello-arguments-error,Not enough Hello arguments
+
+Dialogue: 0,0:03:26.00,0:03:28.00,Default,,0000,0000,0000,version-mismatch-error,Mismatch between versions of client and server
+
+Dialogue: 0,0:03:28.00,0:03:30.00,Default,,0000,0000,0000,vlc-failed-connection,Failed to connect to VLC. If you have not installed syncplay.lua and are using the latest verion of VLC then please refer to https://syncplay.pl/LUA/ for instructions. Syncplay and VLC 4 are not currently compatible, so either use VLC 3 or an alternative such as mpv.
+Dialogue: 0,0:03:30.00,0:03:32.00,Default,,0000,0000,0000,vlc-failed-noscript,VLC has reported that the syncplay.lua interface script has not been installed. Please refer to https://syncplay.pl/LUA/ for instructions.
+Dialogue: 0,0:03:32.00,0:03:34.00,Default,,0000,0000,0000,vlc-failed-versioncheck,This version of VLC is not supported by Syncplay.
+Dialogue: 0,0:03:34.00,0:03:36.00,Default,,0000,0000,0000,vlc-initial-warning,VLC does not always provide accurate position information to Syncplay, especially for .mp4 and .avi files. If you experience problems with erroneous seeking then please try an alternative media player such as mpv (or mpv.net for Windows users).
+Dialogue: 0,0:03:36.00,0:03:38.00,Default,,0000,0000,0000,feature-sharedPlaylists,shared playlists
+Dialogue: 0,0:03:38.00,0:03:40.00,Default,,0000,0000,0000,feature-chat,chat
+Dialogue: 0,0:03:40.00,0:03:42.00,Default,,0000,0000,0000,feature-readiness,readiness
+Dialogue: 0,0:03:42.00,0:03:44.00,Default,,0000,0000,0000,feature-managedRooms,managed rooms
+Dialogue: 0,0:03:44.00,0:03:46.00,Default,,0000,0000,0000,not-supported-by-server-error,The {} feature is not supported by this server..
+Dialogue: 0,0:03:46.00,0:03:48.00,Default,,0000,0000,0000,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 {}.
+Dialogue: 0,0:03:48.00,0:03:50.00,Default,,0000,0000,0000,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.
+Dialogue: 0,0:03:50.00,0:03:52.00,Default,,0000,0000,0000,invalid-seek-value,Invalid seek value
+Dialogue: 0,0:03:52.00,0:03:54.00,Default,,0000,0000,0000,invalid-offset-value,Invalid offset value
+Dialogue: 0,0:03:54.00,0:03:56.00,Default,,0000,0000,0000,switch-file-not-found-error,Could not switch to file '{0}'. Syncplay looks in specified media directories.
+Dialogue: 0,0:03:56.00,0:03:58.00,Default,,0000,0000,0000,folder-search-timeout-error,The search for media in media directories was aborted as it took too long to search through '{}'. This will occur if you select a folder with too many sub-folders in your list of media folders to search through. For automatic file switching to work again please select File->Set Media Directories in the menu bar and remove this directory or replace it with an appropriate sub-folder. If the folder is actually fine then you can re-enable it by selecting File->Set Media Directories and pressing 'OK'.
+Dialogue: 0,0:03:58.00,0:04:00.00,Default,,0000,0000,0000,folder-search-first-file-timeout-error,The search for media in '{}' was aborted as it took too long to access the directory. This could happen if it is a network drive or if you configure your drive to spin down after a period of inactivity. For automatic file switching to work again please go to File->Set Media Directories and either remove the directory or resolve the issue (e.g. by changing power saving settings).
+Dialogue: 0,0:04:00.00,0:04:02.00,Default,,0000,0000,0000,added-file-not-in-media-directory-error,You loaded a file in '{}' which is not a known media directory. You can add this as a media directory by selecting File->Set Media Directories in the menu bar.
+Dialogue: 0,0:04:02.00,0:04:04.00,Default,,0000,0000,0000,no-media-directories-error,No media directories have been set. For shared playlist and file switching features to work properly please select File->Set Media Directories and specify where Syncplay should look to find media files.
+Dialogue: 0,0:04:04.00,0:04:06.00,Default,,0000,0000,0000,cannot-find-directory-error,Could not find media directory '{}'. To update your list of media directories please select File->Set Media Directories from the menu bar and specify where Syncplay should look to find media files.
+Dialogue: 0,0:04:06.00,0:04:08.00,Default,,0000,0000,0000,failed-to-load-server-list-error,Failed to load public server list. Please visit https://www.syncplay.pl/ in your browser.
+Dialogue: 0,0:04:08.00,0:04:10.00,Default,,0000,0000,0000,argument-description,Solution to synchronize playback of multiple media player instances over the network.
+Dialogue: 0,0:04:10.00,0:04:12.00,Default,,0000,0000,0000,argument-epilog,If no options supplied _config values will be used
+Dialogue: 0,0:04:12.00,0:04:14.00,Default,,0000,0000,0000,nogui-argument,show no GUI
+Dialogue: 0,0:04:14.00,0:04:16.00,Default,,0000,0000,0000,host-argument,server's address
+Dialogue: 0,0:04:16.00,0:04:18.00,Default,,0000,0000,0000,name-argument,desired username
+Dialogue: 0,0:04:18.00,0:04:20.00,Default,,0000,0000,0000,debug-argument,debug mode
+Dialogue: 0,0:04:20.00,0:04:22.00,Default,,0000,0000,0000,force-gui-prompt-argument,make configuration prompt appear
+Dialogue: 0,0:04:22.00,0:04:24.00,Default,,0000,0000,0000,no-store-argument,don't store values in .syncplay
+Dialogue: 0,0:04:24.00,0:04:26.00,Default,,0000,0000,0000,room-argument,default room
+Dialogue: 0,0:04:26.00,0:04:28.00,Default,,0000,0000,0000,password-argument,server password
+Dialogue: 0,0:04:28.00,0:04:30.00,Default,,0000,0000,0000,player-path-argument,path to your player executable
+Dialogue: 0,0:04:30.00,0:04:32.00,Default,,0000,0000,0000,file-argument,file to play
+Dialogue: 0,0:04:32.00,0:04:34.00,Default,,0000,0000,0000,args-argument,player options, if you need to pass options starting with - prepend them with single '--' argument
+Dialogue: 0,0:04:34.00,0:04:36.00,Default,,0000,0000,0000,clear-gui-data-argument,resets path and window state GUI data stored as QSettings
+Dialogue: 0,0:04:36.00,0:04:38.00,Default,,0000,0000,0000,language-argument,language for Syncplay messages (de/en/ru/it/es/pt_BR/pt_PT/tr)
+Dialogue: 0,0:04:38.00,0:04:40.00,Default,,0000,0000,0000,version-argument,prints your version
+Dialogue: 0,0:04:40.00,0:04:42.00,Default,,0000,0000,0000,version-message,You're using Syncplay version {} ({})
+Dialogue: 0,0:04:42.00,0:04:44.00,Default,,0000,0000,0000,load-playlist-from-file-argument,loads playlist from text file (one entry per line)
+Dialogue: 0,0:04:44.00,0:04:46.00,Default,,0000,0000,0000,config-window-title,Syncplay configuration
+Dialogue: 0,0:04:46.00,0:04:48.00,Default,,0000,0000,0000,connection-group-title,Connection settings
+Dialogue: 0,0:04:48.00,0:04:50.00,Default,,0000,0000,0000,host-label,Server address:
+Dialogue: 0,0:04:50.00,0:04:52.00,Default,,0000,0000,0000,name-label,Username (optional):
+Dialogue: 0,0:04:52.00,0:04:54.00,Default,,0000,0000,0000,password-label,Server password (if any):
+Dialogue: 0,0:04:54.00,0:04:56.00,Default,,0000,0000,0000,room-label,Default room:
+Dialogue: 0,0:04:56.00,0:04:58.00,Default,,0000,0000,0000,roomlist-msgbox-label,Edit room list (one per line)
+Dialogue: 0,0:04:58.00,0:05:00.00,Default,,0000,0000,0000,media-setting-title,Media player settings
+Dialogue: 0,0:05:00.00,0:05:02.00,Default,,0000,0000,0000,executable-path-label,Path to media player:
+Dialogue: 0,0:05:02.00,0:05:04.00,Default,,0000,0000,0000,media-path-label,Path to video (optional):
+Dialogue: 0,0:05:04.00,0:05:06.00,Default,,0000,0000,0000,player-arguments-label,Player arguments (if any):
+Dialogue: 0,0:05:06.00,0:05:08.00,Default,,0000,0000,0000,browse-label,Browse
+Dialogue: 0,0:05:08.00,0:05:10.00,Default,,0000,0000,0000,update-server-list-label,Update list
+Dialogue: 0,0:05:10.00,0:05:12.00,Default,,0000,0000,0000,more-title,Show more settings
+Dialogue: 0,0:05:12.00,0:05:14.00,Default,,0000,0000,0000,never-rewind-value,Never
+Dialogue: 0,0:05:14.00,0:05:16.00,Default,,0000,0000,0000,seconds-suffix, secs
+Dialogue: 0,0:05:16.00,0:05:18.00,Default,,0000,0000,0000,privacy-sendraw-option,Send raw
+Dialogue: 0,0:05:18.00,0:05:20.00,Default,,0000,0000,0000,privacy-sendhashed-option,Send hashed
+Dialogue: 0,0:05:20.00,0:05:22.00,Default,,0000,0000,0000,privacy-dontsend-option,Don't send
+Dialogue: 0,0:05:22.00,0:05:24.00,Default,,0000,0000,0000,filename-privacy-label,Filename information:
+Dialogue: 0,0:05:24.00,0:05:26.00,Default,,0000,0000,0000,filesize-privacy-label,File size information:
+Dialogue: 0,0:05:26.00,0:05:28.00,Default,,0000,0000,0000,checkforupdatesautomatically-label,Check for Syncplay updates automatically
+Dialogue: 0,0:05:28.00,0:05:30.00,Default,,0000,0000,0000,autosavejoinstolist-label,Add rooms you join to the room list
+Dialogue: 0,0:05:30.00,0:05:32.00,Default,,0000,0000,0000,slowondesync-label,Slow down on minor desync (not supported on MPC-HC/BE)
+Dialogue: 0,0:05:32.00,0:05:34.00,Default,,0000,0000,0000,rewindondesync-label,Rewind on major desync (recommended)
+Dialogue: 0,0:05:34.00,0:05:36.00,Default,,0000,0000,0000,fastforwardondesync-label,Fast-forward if lagging behind (recommended)
+Dialogue: 0,0:05:36.00,0:05:38.00,Default,,0000,0000,0000,dontslowdownwithme-label,Never slow down or rewind others (experimental)
+Dialogue: 0,0:05:38.00,0:05:40.00,Default,,0000,0000,0000,pausing-title,Pausing
+Dialogue: 0,0:05:40.00,0:05:42.00,Default,,0000,0000,0000,pauseonleave-label,Pause when user leaves (e.g. if they are disconnected)
+Dialogue: 0,0:05:42.00,0:05:44.00,Default,,0000,0000,0000,readiness-title,Initial readiness state
+Dialogue: 0,0:05:44.00,0:05:46.00,Default,,0000,0000,0000,readyatstart-label,Set me as 'ready to watch' by default
+Dialogue: 0,0:05:46.00,0:05:48.00,Default,,0000,0000,0000,forceguiprompt-label,Don't always show the Syncplay configuration window
+Dialogue: 0,0:05:48.00,0:05:50.00,Default,,0000,0000,0000,showosd-label,Enable OSD Messages
+Dialogue: 0,0:05:50.00,0:05:52.00,Default,,0000,0000,0000,showosdwarnings-label,Include warnings (e.g. when files are different, users not ready)
+Dialogue: 0,0:05:52.00,0:05:54.00,Default,,0000,0000,0000,showsameroomosd-label,Include events in your room
+Dialogue: 0,0:05:54.00,0:05:56.00,Default,,0000,0000,0000,shownoncontrollerosd-label,Include events from non-operators in managed rooms
+Dialogue: 0,0:05:56.00,0:05:58.00,Default,,0000,0000,0000,showdifferentroomosd-label,Include events in other rooms
+Dialogue: 0,0:05:58.00,0:06:00.00,Default,,0000,0000,0000,showslowdownosd-label,Include slowing down / reverting notifications
+Dialogue: 0,0:06:00.00,0:06:02.00,Default,,0000,0000,0000,language-label,Language:
+Dialogue: 0,0:06:02.00,0:06:04.00,Default,,0000,0000,0000,automatic-language,Default ({})
+Dialogue: 0,0:06:04.00,0:06:06.00,Default,,0000,0000,0000,showdurationnotification-label,Warn about media duration mismatches
+Dialogue: 0,0:06:06.00,0:06:08.00,Default,,0000,0000,0000,basics-label,Basics
+Dialogue: 0,0:06:08.00,0:06:10.00,Default,,0000,0000,0000,readiness-label,Play/Pause
+Dialogue: 0,0:06:10.00,0:06:12.00,Default,,0000,0000,0000,misc-label,Misc
+Dialogue: 0,0:06:12.00,0:06:14.00,Default,,0000,0000,0000,core-behaviour-title,Core room behaviour
+Dialogue: 0,0:06:14.00,0:06:16.00,Default,,0000,0000,0000,syncplay-internals-title,Syncplay internals
+Dialogue: 0,0:06:16.00,0:06:18.00,Default,,0000,0000,0000,syncplay-mediasearchdirectories-title,Directories to search for media
+Dialogue: 0,0:06:18.00,0:06:20.00,Default,,0000,0000,0000,syncplay-mediasearchdirectories-label,Directories to search for media (one path per line)
+Dialogue: 0,0:06:20.00,0:06:22.00,Default,,0000,0000,0000,sync-label,Sync
+Dialogue: 0,0:06:22.00,0:06:24.00,Default,,0000,0000,0000,sync-otherslagging-title,If others are lagging behind...
+Dialogue: 0,0:06:24.00,0:06:26.00,Default,,0000,0000,0000,sync-youlaggging-title,If you are lagging behind...
+Dialogue: 0,0:06:26.00,0:06:28.00,Default,,0000,0000,0000,messages-label,Messages
+Dialogue: 0,0:06:28.00,0:06:30.00,Default,,0000,0000,0000,messages-osd-title,On-screen Display settings
+Dialogue: 0,0:06:30.00,0:06:32.00,Default,,0000,0000,0000,messages-other-title,Other display settings
+Dialogue: 0,0:06:32.00,0:06:34.00,Default,,0000,0000,0000,chat-label,Chat
+Dialogue: 0,0:06:34.00,0:06:36.00,Default,,0000,0000,0000,privacy-label,Privacy
+Dialogue: 0,0:06:36.00,0:06:38.00,Default,,0000,0000,0000,privacy-title,Privacy settings
+Dialogue: 0,0:06:38.00,0:06:40.00,Default,,0000,0000,0000,unpause-title,If you press play, set as ready and:
+Dialogue: 0,0:06:40.00,0:06:42.00,Default,,0000,0000,0000,unpause-ifalreadyready-option,Unpause if already set as ready
+Dialogue: 0,0:06:42.00,0:06:44.00,Default,,0000,0000,0000,unpause-ifothersready-option,Unpause if already ready or others in room are ready (default)
+Dialogue: 0,0:06:44.00,0:06:46.00,Default,,0000,0000,0000,unpause-ifminusersready-option,Unpause if already ready or if all others ready and min users ready
+Dialogue: 0,0:06:46.00,0:06:48.00,Default,,0000,0000,0000,unpause-always,Always unpause
+Dialogue: 0,0:06:48.00,0:06:50.00,Default,,0000,0000,0000,syncplay-trusteddomains-title,Trusted domains (for streaming services and hosted content)
+Dialogue: 0,0:06:50.00,0:06:52.00,Default,,0000,0000,0000,chat-title,Chat message input
+Dialogue: 0,0:06:52.00,0:06:54.00,Default,,0000,0000,0000,chatinputenabled-label,Enable chat input via mpv
+Dialogue: 0,0:06:54.00,0:06:56.00,Default,,0000,0000,0000,chatdirectinput-label,Allow instant chat input (bypass having to press enter key to chat)
+Dialogue: 0,0:06:56.00,0:06:58.00,Default,,0000,0000,0000,chatinputfont-label,Chat input font
+Dialogue: 0,0:06:58.00,0:07:00.00,Default,,0000,0000,0000,chatfont-label,Set font
+Dialogue: 0,0:07:00.00,0:07:02.00,Default,,0000,0000,0000,chatcolour-label,Set colour
+Dialogue: 0,0:07:02.00,0:07:04.00,Default,,0000,0000,0000,chatinputposition-label,Position of message input area in mpv
+Dialogue: 0,0:07:04.00,0:07:06.00,Default,,0000,0000,0000,chat-top-option,Top
+Dialogue: 0,0:07:06.00,0:07:08.00,Default,,0000,0000,0000,chat-middle-option,Middle
+Dialogue: 0,0:07:08.00,0:07:10.00,Default,,0000,0000,0000,chat-bottom-option,Bottom
+Dialogue: 0,0:07:10.00,0:07:12.00,Default,,0000,0000,0000,chatoutputheader-label,Chat message output
+Dialogue: 0,0:07:12.00,0:07:14.00,Default,,0000,0000,0000,chatoutputfont-label,Chat output font
+Dialogue: 0,0:07:14.00,0:07:16.00,Default,,0000,0000,0000,chatoutputenabled-label,Enable chat output in media player (mpv only for now)
+Dialogue: 0,0:07:16.00,0:07:18.00,Default,,0000,0000,0000,chatoutputposition-label,Output mode
+Dialogue: 0,0:07:18.00,0:07:20.00,Default,,0000,0000,0000,chat-chatroom-option,Chatroom style
+Dialogue: 0,0:07:20.00,0:07:22.00,Default,,0000,0000,0000,chat-scrolling-option,Scrolling style
+Dialogue: 0,0:07:22.00,0:07:24.00,Default,,0000,0000,0000,mpv-key-tab-hint,[TAB] to toggle access to alphabet row key shortcuts.
+Dialogue: 0,0:07:24.00,0:07:26.00,Default,,0000,0000,0000,mpv-key-hint,[ENTER] to send message. [ESC] to escape chat mode.
+Dialogue: 0,0:07:26.00,0:07:28.00,Default,,0000,0000,0000,alphakey-mode-warning-first-line,You can temporarily use old mpv bindings with a-z keys.
+Dialogue: 0,0:07:28.00,0:07:30.00,Default,,0000,0000,0000,alphakey-mode-warning-second-line,Press [TAB] to return to Syncplay chat mode.
+Dialogue: 0,0:07:30.00,0:07:32.00,Default,,0000,0000,0000,help-label,Help
+Dialogue: 0,0:07:32.00,0:07:34.00,Default,,0000,0000,0000,reset-label,Restore defaults
+Dialogue: 0,0:07:34.00,0:07:36.00,Default,,0000,0000,0000,run-label,Run Syncplay
+Dialogue: 0,0:07:36.00,0:07:38.00,Default,,0000,0000,0000,storeandrun-label,Store configuration and run Syncplay
+Dialogue: 0,0:07:38.00,0:07:40.00,Default,,0000,0000,0000,contact-label,Feel free to e-mail dev@syncplay.pl, create an issue to report a bug/problem via GitHub, start a discussion to make a suggestion or ask a question via GitHub, like us on Facebook, follow us on Twitter, or visit https://syncplay.pl/. Do not use Syncplay to send sensitive information.
+Dialogue: 0,0:07:40.00,0:07:42.00,Default,,0000,0000,0000,joinroom-label,Join room
+Dialogue: 0,0:07:42.00,0:07:44.00,Default,,0000,0000,0000,joinroom-menu-label,Join room {}
+Dialogue: 0,0:07:44.00,0:07:46.00,Default,,0000,0000,0000,seektime-menu-label,Seek to time
+Dialogue: 0,0:07:46.00,0:07:48.00,Default,,0000,0000,0000,undoseek-menu-label,Undo seek
+Dialogue: 0,0:07:48.00,0:07:50.00,Default,,0000,0000,0000,play-menu-label,Play
+Dialogue: 0,0:07:50.00,0:07:52.00,Default,,0000,0000,0000,pause-menu-label,Pause
+Dialogue: 0,0:07:52.00,0:07:54.00,Default,,0000,0000,0000,playbackbuttons-menu-label,Show playback buttons
+Dialogue: 0,0:07:54.00,0:07:56.00,Default,,0000,0000,0000,autoplay-menu-label,Show auto-play button
+Dialogue: 0,0:07:56.00,0:07:58.00,Default,,0000,0000,0000,autoplay-guipushbuttonlabel,Play when all ready
+Dialogue: 0,0:07:58.00,0:08:00.00,Default,,0000,0000,0000,autoplay-minimum-label,Min users:
+Dialogue: 0,0:08:00.00,0:08:02.00,Default,,0000,0000,0000,sendmessage-label,Send
+Dialogue: 0,0:08:02.00,0:08:04.00,Default,,0000,0000,0000,ready-guipushbuttonlabel,I'm ready to watch!
+Dialogue: 0,0:08:04.00,0:08:06.00,Default,,0000,0000,0000,roomuser-heading-label,Room / User
+Dialogue: 0,0:08:06.00,0:08:08.00,Default,,0000,0000,0000,size-heading-label,Size
+Dialogue: 0,0:08:08.00,0:08:10.00,Default,,0000,0000,0000,duration-heading-label,Length
+Dialogue: 0,0:08:10.00,0:08:12.00,Default,,0000,0000,0000,filename-heading-label,Filename
+Dialogue: 0,0:08:12.00,0:08:14.00,Default,,0000,0000,0000,notifications-heading-label,Notifications
+Dialogue: 0,0:08:14.00,0:08:16.00,Default,,0000,0000,0000,userlist-heading-label,List of who is playing what
+Dialogue: 0,0:08:16.00,0:08:18.00,Default,,0000,0000,0000,browseformedia-label,Browse for media files
+Dialogue: 0,0:08:18.00,0:08:20.00,Default,,0000,0000,0000,file-menu-label,&File
+Dialogue: 0,0:08:20.00,0:08:22.00,Default,,0000,0000,0000,openmedia-menu-label,&Open media file
+Dialogue: 0,0:08:22.00,0:08:24.00,Default,,0000,0000,0000,openstreamurl-menu-label,Open &media stream URL
+Dialogue: 0,0:08:24.00,0:08:26.00,Default,,0000,0000,0000,setmediadirectories-menu-label,Set media &directories
+Dialogue: 0,0:08:26.00,0:08:28.00,Default,,0000,0000,0000,loadplaylistfromfile-menu-label,&Load playlist from file
+Dialogue: 0,0:08:28.00,0:08:30.00,Default,,0000,0000,0000,saveplaylisttofile-menu-label,&Save playlist to file
+Dialogue: 0,0:08:30.00,0:08:32.00,Default,,0000,0000,0000,exit-menu-label,E&xit
+Dialogue: 0,0:08:32.00,0:08:34.00,Default,,0000,0000,0000,advanced-menu-label,&Advanced
+Dialogue: 0,0:08:34.00,0:08:36.00,Default,,0000,0000,0000,window-menu-label,&Window
+Dialogue: 0,0:08:36.00,0:08:38.00,Default,,0000,0000,0000,setoffset-menu-label,Set &offset
+Dialogue: 0,0:08:38.00,0:08:40.00,Default,,0000,0000,0000,createcontrolledroom-menu-label,&Create managed room
+Dialogue: 0,0:08:40.00,0:08:42.00,Default,,0000,0000,0000,identifyascontroller-menu-label,&Identify as room operator
+Dialogue: 0,0:08:42.00,0:08:44.00,Default,,0000,0000,0000,settrusteddomains-menu-label,Set &trusted domains
+Dialogue: 0,0:08:44.00,0:08:46.00,Default,,0000,0000,0000,addtrusteddomain-menu-label,Add {} as trusted domain
+Dialogue: 0,0:08:46.00,0:08:48.00,Default,,0000,0000,0000,edit-menu-label,&Edit
+Dialogue: 0,0:08:48.00,0:08:50.00,Default,,0000,0000,0000,cut-menu-label,Cu&t
+Dialogue: 0,0:08:50.00,0:08:52.00,Default,,0000,0000,0000,copy-menu-label,&Copy
+Dialogue: 0,0:08:52.00,0:08:54.00,Default,,0000,0000,0000,paste-menu-label,&Paste
+Dialogue: 0,0:08:54.00,0:08:56.00,Default,,0000,0000,0000,selectall-menu-label,&Select All
+Dialogue: 0,0:08:56.00,0:08:58.00,Default,,0000,0000,0000,playback-menu-label,&Playback
+Dialogue: 0,0:08:58.00,0:09:00.00,Default,,0000,0000,0000,help-menu-label,&Help
+Dialogue: 0,0:09:00.00,0:09:02.00,Default,,0000,0000,0000,userguide-menu-label,Open user &guide
+Dialogue: 0,0:09:02.00,0:09:04.00,Default,,0000,0000,0000,update-menu-label,Check for &update
+Dialogue: 0,0:09:04.00,0:09:06.00,Default,,0000,0000,0000,startTLS-initiated,Attempting secure connection
+Dialogue: 0,0:09:06.00,0:09:08.00,Default,,0000,0000,0000,startTLS-secure-connection-ok,Secure connection established ({})
+Dialogue: 0,0:09:08.00,0:09:10.00,Default,,0000,0000,0000,startTLS-server-certificate-invalid,Secure connection failed. The server uses an invalid security certificate. This communication could be intercepted by a third party. For further details and troubleshooting see here.
+Dialogue: 0,0:09:10.00,0:09:12.00,Default,,0000,0000,0000,startTLS-server-certificate-invalid-DNS-ID,Syncplay does not trust this server because it uses a certificate that is not valid for its hostname.
+Dialogue: 0,0:09:12.00,0:09:14.00,Default,,0000,0000,0000,startTLS-not-supported-client,This client does not support TLS
+Dialogue: 0,0:09:14.00,0:09:16.00,Default,,0000,0000,0000,startTLS-not-supported-server,This server does not support TLS
+Dialogue: 0,0:09:16.00,0:09:18.00,Default,,0000,0000,0000,tls-information-title,Certificate Details
+Dialogue: 0,0:09:18.00,0:09:20.00,Default,,0000,0000,0000,tls-dialog-status-label,Syncplay is using an encrypted connection to {}.
+Dialogue: 0,0:09:20.00,0:09:22.00,Default,,0000,0000,0000,tls-dialog-desc-label,Encryption with a digital certificate keeps information private as it is sent to or from the
server {}.
+Dialogue: 0,0:09:22.00,0:09:24.00,Default,,0000,0000,0000,tls-dialog-connection-label,Information encrypted using Transport Layer Security (TLS), version {} with the cipher
suite: {}.
+Dialogue: 0,0:09:24.00,0:09:26.00,Default,,0000,0000,0000,tls-dialog-certificate-label,Certificate issued by {} valid until {}.
+Dialogue: 0,0:09:26.00,0:09:28.00,Default,,0000,0000,0000,about-menu-label,&About Syncplay
+Dialogue: 0,0:09:28.00,0:09:30.00,Default,,0000,0000,0000,about-dialog-title,About Syncplay
+Dialogue: 0,0:09:30.00,0:09:32.00,Default,,0000,0000,0000,about-dialog-release,Version {} release {}
+Dialogue: 0,0:09:32.00,0:09:34.00,Default,,0000,0000,0000,about-dialog-license-text,Licensed under the Apache License, Version 2.0
+Dialogue: 0,0:09:34.00,0:09:36.00,Default,,0000,0000,0000,about-dialog-license-button,License
+Dialogue: 0,0:09:36.00,0:09:38.00,Default,,0000,0000,0000,about-dialog-dependencies,Dependencies
+Dialogue: 0,0:09:38.00,0:09:40.00,Default,,0000,0000,0000,setoffset-msgbox-label,Set offset
+Dialogue: 0,0:09:40.00,0:09:42.00,Default,,0000,0000,0000,offsetinfo-msgbox-label,Offset (see https://syncplay.pl/guide/ for usage instructions):
+Dialogue: 0,0:09:42.00,0:09:44.00,Default,,0000,0000,0000,promptforstreamurl-msgbox-label,Open media stream URL
+Dialogue: 0,0:09:44.00,0:09:46.00,Default,,0000,0000,0000,promptforstreamurlinfo-msgbox-label,Stream URL
+Dialogue: 0,0:09:46.00,0:09:48.00,Default,,0000,0000,0000,addfolder-label,Add folder
+Dialogue: 0,0:09:48.00,0:09:50.00,Default,,0000,0000,0000,adduris-msgbox-label,Add URLs to playlist (one per line)
+Dialogue: 0,0:09:50.00,0:09:52.00,Default,,0000,0000,0000,editplaylist-msgbox-label,Set playlist (one per line)
+Dialogue: 0,0:09:52.00,0:09:54.00,Default,,0000,0000,0000,trusteddomains-msgbox-label,Domains it is okay to automatically switch to (one per line)
+Dialogue: 0,0:09:54.00,0:09:56.00,Default,,0000,0000,0000,createcontrolledroom-msgbox-label,Create managed room
+Dialogue: 0,0:09:56.00,0:09:58.00,Default,,0000,0000,0000,controlledroominfo-msgbox-label,Enter name of managed room
+(see https://syncplay.pl/guide/ for usage instructions):
+Dialogue: 0,0:09:58.00,0:10:00.00,Default,,0000,0000,0000,identifyascontroller-msgbox-label,Identify as room operator
+Dialogue: 0,0:10:00.00,0:10:02.00,Default,,0000,0000,0000,identifyinfo-msgbox-label,Enter operator password for this room
+(see https://syncplay.pl/guide/ for usage instructions):
+Dialogue: 0,0:10:02.00,0:10:04.00,Default,,0000,0000,0000,public-server-msgbox-label,Select the public server for this viewing session
+Dialogue: 0,0:10:04.00,0:10:06.00,Default,,0000,0000,0000,megabyte-suffix, MB
+Dialogue: 0,0:10:06.00,0:10:08.00,Default,,0000,0000,0000,host-tooltip,Hostname or IP to connect to, optionally including port (e.g. syncplay.pl:8999). Only synchronised with people on same server/port.
+Dialogue: 0,0:10:08.00,0:10:10.00,Default,,0000,0000,0000,name-tooltip,Nickname you will be known by. No registration, so can easily change later. Random name generated if none specified.
+Dialogue: 0,0:10:10.00,0:10:12.00,Default,,0000,0000,0000,password-tooltip,Passwords are only needed for connecting to private servers.
+Dialogue: 0,0:10:12.00,0:10:14.00,Default,,0000,0000,0000,room-tooltip,Room to join upon connection can be almost anything, but you will only be synchronised with people in the same room.
+Dialogue: 0,0:10:14.00,0:10:16.00,Default,,0000,0000,0000,edit-rooms-tooltip,Edit room list.
+Dialogue: 0,0:10:16.00,0:10:18.00,Default,,0000,0000,0000,executable-path-tooltip,Location of your chosen supported media player (mpv, mpv.net, VLC, MPC-HC/BE, mplayer2 or IINA).
+Dialogue: 0,0:10:18.00,0:10:20.00,Default,,0000,0000,0000,media-path-tooltip,Location of video or stream to be opened. Necessary for mplayer2.
+Dialogue: 0,0:10:20.00,0:10:22.00,Default,,0000,0000,0000,player-arguments-tooltip,Additional command line arguments / switches to pass on to this media player.
+Dialogue: 0,0:10:22.00,0:10:24.00,Default,,0000,0000,0000,mediasearcdirectories-arguments-tooltip,Directories where Syncplay will search for media files, e.g. when you are using the click to switch feature. Syncplay will look recursively through sub-folders.
+Dialogue: 0,0:10:24.00,0:10:26.00,Default,,0000,0000,0000,more-tooltip,Display less frequently used settings.
+Dialogue: 0,0:10:26.00,0:10:28.00,Default,,0000,0000,0000,filename-privacy-tooltip,Privacy mode for sending currently playing filename to server.
+Dialogue: 0,0:10:28.00,0:10:30.00,Default,,0000,0000,0000,filesize-privacy-tooltip,Privacy mode for sending size of currently playing file to server.
+Dialogue: 0,0:10:30.00,0:10:32.00,Default,,0000,0000,0000,privacy-sendraw-tooltip,Send this information without obfuscation. This is the default option with most functionality.
+Dialogue: 0,0:10:32.00,0:10:34.00,Default,,0000,0000,0000,privacy-sendhashed-tooltip,Send a hashed version of the information, making it less visible to other clients.
+Dialogue: 0,0:10:34.00,0:10:36.00,Default,,0000,0000,0000,privacy-dontsend-tooltip,Do not send this information to the server. This provides for maximum privacy.
+Dialogue: 0,0:10:36.00,0:10:38.00,Default,,0000,0000,0000,checkforupdatesautomatically-tooltip,Regularly check with the Syncplay website to see whether a new version of Syncplay is available.
+Dialogue: 0,0:10:38.00,0:10:40.00,Default,,0000,0000,0000,autosavejoinstolist-tooltip,When you join a room in a server, automatically remember the room name in the list of rooms to join.
+Dialogue: 0,0:10:40.00,0:10:42.00,Default,,0000,0000,0000,slowondesync-tooltip,Reduce playback rate temporarily when needed to bring you back in sync with other viewers. Not supported on MPC-HC/BE.
+Dialogue: 0,0:10:42.00,0:10:44.00,Default,,0000,0000,0000,dontslowdownwithme-tooltip,Means others do not get slowed down or rewinded if your playback is lagging. Useful for room operators.
+Dialogue: 0,0:10:44.00,0:10:46.00,Default,,0000,0000,0000,pauseonleave-tooltip,Pause playback if you get disconnected or someone leaves from your room.
+Dialogue: 0,0:10:46.00,0:10:48.00,Default,,0000,0000,0000,readyatstart-tooltip,Set yourself as 'ready' at start (otherwise you are set as 'not ready' until you change your readiness state)
+Dialogue: 0,0:10:48.00,0:10:50.00,Default,,0000,0000,0000,forceguiprompt-tooltip,Configuration dialogue is not shown when opening a file with Syncplay.
+Dialogue: 0,0:10:50.00,0:10:52.00,Default,,0000,0000,0000,nostore-tooltip,Run Syncplay with the given configuration, but do not permanently store the changes.
+Dialogue: 0,0:10:52.00,0:10:54.00,Default,,0000,0000,0000,rewindondesync-tooltip,Jump back when needed to get back in sync. Disabling this option can result in major desyncs!
+Dialogue: 0,0:10:54.00,0:10:56.00,Default,,0000,0000,0000,fastforwardondesync-tooltip,Jump forward when out of sync with room operator (or your pretend position if 'Never slow down or rewind others' enabled).
+Dialogue: 0,0:10:56.00,0:10:58.00,Default,,0000,0000,0000,showosd-tooltip,Sends Syncplay messages to media player OSD.
+Dialogue: 0,0:10:58.00,0:11:00.00,Default,,0000,0000,0000,showosdwarnings-tooltip,Show warnings if playing different file, alone in room, users not ready, etc.
+Dialogue: 0,0:11:00.00,0:11:02.00,Default,,0000,0000,0000,showsameroomosd-tooltip,Show OSD notifications for events relating to room user is in.
+Dialogue: 0,0:11:02.00,0:11:04.00,Default,,0000,0000,0000,shownoncontrollerosd-tooltip,Show OSD notifications for events relating to non-operators who are in managed rooms.
+Dialogue: 0,0:11:04.00,0:11:06.00,Default,,0000,0000,0000,showdifferentroomosd-tooltip,Show OSD notifications for events relating to room user is not in.
+Dialogue: 0,0:11:06.00,0:11:08.00,Default,,0000,0000,0000,showslowdownosd-tooltip,Show notifications of slowing down / reverting on time difference.
+Dialogue: 0,0:11:08.00,0:11:10.00,Default,,0000,0000,0000,showdurationnotification-tooltip,Useful for when a segment in a multi-part file is missing, but can result in false positives.
+Dialogue: 0,0:11:10.00,0:11:12.00,Default,,0000,0000,0000,language-tooltip,Language to be used by Syncplay.
+Dialogue: 0,0:11:12.00,0:11:14.00,Default,,0000,0000,0000,unpause-always-tooltip,If you press unpause it always sets you as ready and unpause, rather than just setting you as ready.
+Dialogue: 0,0:11:14.00,0:11:16.00,Default,,0000,0000,0000,unpause-ifalreadyready-tooltip,If you press unpause when not ready it will set you as ready - press unpause again to unpause.
+Dialogue: 0,0:11:16.00,0:11:18.00,Default,,0000,0000,0000,unpause-ifothersready-tooltip,If you press unpause when not ready, it will only unpause if others are ready.
+Dialogue: 0,0:11:18.00,0:11:20.00,Default,,0000,0000,0000,unpause-ifminusersready-tooltip,If you press unpause when not ready, it will only unpause if others are ready and minimum users threshold is met.
+Dialogue: 0,0:11:20.00,0:11:22.00,Default,,0000,0000,0000,trusteddomains-arguments-tooltip,Domains that it is okay for Syncplay to automatically switch to when shared playlists is enabled.
+Dialogue: 0,0:11:22.00,0:11:24.00,Default,,0000,0000,0000,chatinputenabled-tooltip,Enable chat input in mpv (press enter to chat, enter to send, escape to cancel)
+Dialogue: 0,0:11:24.00,0:11:26.00,Default,,0000,0000,0000,chatdirectinput-tooltip,Skip having to press 'enter' to go into chat input mode in mpv. Press TAB in mpv to temporarily disable this feature.
+Dialogue: 0,0:11:26.00,0:11:28.00,Default,,0000,0000,0000,font-label-tooltip,Font used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.
+Dialogue: 0,0:11:28.00,0:11:30.00,Default,,0000,0000,0000,set-input-font-tooltip,Font family used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.
+Dialogue: 0,0:11:30.00,0:11:32.00,Default,,0000,0000,0000,set-input-colour-tooltip,Font colour used for when entering chat messages in mpv. Client-side only, so doesn't affect what other see.
+Dialogue: 0,0:11:32.00,0:11:34.00,Default,,0000,0000,0000,chatinputposition-tooltip,Location in mpv where chat input text will appear when you press enter and type.
+Dialogue: 0,0:11:34.00,0:11:36.00,Default,,0000,0000,0000,chatinputposition-top-tooltip,Place chat input at top of mpv window.
+Dialogue: 0,0:11:36.00,0:11:38.00,Default,,0000,0000,0000,chatinputposition-middle-tooltip,Place chat input in dead centre of mpv window.
+Dialogue: 0,0:11:38.00,0:11:40.00,Default,,0000,0000,0000,chatinputposition-bottom-tooltip,Place chat input at bottom of mpv window.
+Dialogue: 0,0:11:40.00,0:11:42.00,Default,,0000,0000,0000,chatoutputenabled-tooltip,Show chat messages in OSD (if supported by media player).
+Dialogue: 0,0:11:42.00,0:11:44.00,Default,,0000,0000,0000,font-output-label-tooltip,Chat output font.
+Dialogue: 0,0:11:44.00,0:11:46.00,Default,,0000,0000,0000,set-output-font-tooltip,Font used for when displaying chat messages.
+Dialogue: 0,0:11:46.00,0:11:48.00,Default,,0000,0000,0000,chatoutputmode-tooltip,How chat messages are displayed.
+Dialogue: 0,0:11:48.00,0:11:50.00,Default,,0000,0000,0000,chatoutputmode-chatroom-tooltip,Display new lines of chat directly below previous line.
+Dialogue: 0,0:11:50.00,0:11:52.00,Default,,0000,0000,0000,chatoutputmode-scrolling-tooltip,Scroll chat text from right to left.
+Dialogue: 0,0:11:52.00,0:11:54.00,Default,,0000,0000,0000,help-tooltip,Opens the Syncplay.pl user guide.
+Dialogue: 0,0:11:54.00,0:11:56.00,Default,,0000,0000,0000,reset-tooltip,Reset all settings to the default configuration.
+Dialogue: 0,0:11:56.00,0:11:58.00,Default,,0000,0000,0000,update-server-list-tooltip,Connect to syncplay.pl to update list of public servers.
+Dialogue: 0,0:11:58.00,0:12:00.00,Default,,0000,0000,0000,sslconnection-tooltip,Securely connected to server. Click for certificate details.
+Dialogue: 0,0:12:00.00,0:12:02.00,Default,,0000,0000,0000,joinroom-tooltip,Leave current room and joins specified room.
+Dialogue: 0,0:12:02.00,0:12:04.00,Default,,0000,0000,0000,seektime-msgbox-label,Jump to specified time (in seconds / min:sec). Use +/- for relative seek.
+Dialogue: 0,0:12:04.00,0:12:06.00,Default,,0000,0000,0000,ready-tooltip,Indicates whether you are ready to watch.
+Dialogue: 0,0:12:06.00,0:12:08.00,Default,,0000,0000,0000,autoplay-tooltip,Auto-play when all users who have readiness indicator are ready and minimum user threshold met.
+Dialogue: 0,0:12:08.00,0:12:10.00,Default,,0000,0000,0000,switch-to-file-tooltip,Double click to switch to {}
+Dialogue: 0,0:12:10.00,0:12:12.00,Default,,0000,0000,0000,sendmessage-tooltip,Send message to room
+Dialogue: 0,0:12:12.00,0:12:14.00,Default,,0000,0000,0000,differentsize-note,Different size!
+Dialogue: 0,0:12:14.00,0:12:16.00,Default,,0000,0000,0000,differentsizeandduration-note,Different size and duration!
+Dialogue: 0,0:12:16.00,0:12:18.00,Default,,0000,0000,0000,differentduration-note,Different duration!
+Dialogue: 0,0:12:18.00,0:12:20.00,Default,,0000,0000,0000,nofile-note,(No file being played)
+Dialogue: 0,0:12:20.00,0:12:22.00,Default,,0000,0000,0000,new-syncplay-available-motd-message,You are using Syncplay {} but a newer version is available from https://syncplay.pl
+Dialogue: 0,0:12:22.00,0:12:24.00,Default,,0000,0000,0000,welcome-server-notification,Welcome to Syncplay server, ver. {0}
+Dialogue: 0,0:12:24.00,0:12:26.00,Default,,0000,0000,0000,client-connected-room-server-notification,{0}({2}) connected to room '{1}'
+Dialogue: 0,0:12:26.00,0:12:28.00,Default,,0000,0000,0000,client-left-server-notification,{0} left server
+Dialogue: 0,0:12:28.00,0:12:30.00,Default,,0000,0000,0000,no-salt-notification,PLEASE NOTE: To allow room operator passwords generated by this server instance to still work when the server is restarted, please add the following command line argument when running the Syncplay server in the future: --salt {}
+Dialogue: 0,0:12:30.00,0:12:32.00,Default,,0000,0000,0000,server-argument-description,Solution to synchronize playback of multiple media player instances over the network. Server instance
+Dialogue: 0,0:12:32.00,0:12:34.00,Default,,0000,0000,0000,server-argument-epilog,If no options supplied _config values will be used
+Dialogue: 0,0:12:34.00,0:12:36.00,Default,,0000,0000,0000,server-port-argument,server TCP port
+Dialogue: 0,0:12:36.00,0:12:38.00,Default,,0000,0000,0000,server-password-argument,server password
+Dialogue: 0,0:12:38.00,0:12:40.00,Default,,0000,0000,0000,server-isolate-room-argument,should rooms be isolated?
+Dialogue: 0,0:12:40.00,0:12:42.00,Default,,0000,0000,0000,server-salt-argument,random string used to generate managed room passwords
+Dialogue: 0,0:12:42.00,0:12:44.00,Default,,0000,0000,0000,server-disable-ready-argument,disable readiness feature
+Dialogue: 0,0:12:44.00,0:12:46.00,Default,,0000,0000,0000,server-motd-argument,path to file from which motd will be fetched
+Dialogue: 0,0:12:46.00,0:12:48.00,Default,,0000,0000,0000,server-chat-argument,Should chat be disabled?
+Dialogue: 0,0:12:48.00,0:12:50.00,Default,,0000,0000,0000,server-chat-maxchars-argument,Maximum number of characters in a chat message (default is {})
+Dialogue: 0,0:12:50.00,0:12:52.00,Default,,0000,0000,0000,server-maxusernamelength-argument,Maximum number of characters in a username (default is {})
+Dialogue: 0,0:12:52.00,0:12:54.00,Default,,0000,0000,0000,server-stats-db-file-argument,Enable server stats using the SQLite db file provided
+Dialogue: 0,0:12:54.00,0:12:56.00,Default,,0000,0000,0000,server-startTLS-argument,Enable TLS connections using the certificate files in the path provided
+Dialogue: 0,0:12:56.00,0:12:58.00,Default,,0000,0000,0000,server-messed-up-motd-unescaped-placeholders,Message of the Day has unescaped placeholders. All $ signs should be doubled ($$).
+Dialogue: 0,0:12:58.00,0:13:00.00,Default,,0000,0000,0000,server-messed-up-motd-too-long,Message of the Day is too long - maximum of {} chars, {} given.
+Dialogue: 0,0:13:00.00,0:13:02.00,Default,,0000,0000,0000,unknown-command-server-error,Unknown command {}
+Dialogue: 0,0:13:02.00,0:13:04.00,Default,,0000,0000,0000,not-json-server-error,Not a json encoded string {}
+Dialogue: 0,0:13:04.00,0:13:06.00,Default,,0000,0000,0000,line-decode-server-error,Not a utf-8 string
+Dialogue: 0,0:13:06.00,0:13:08.00,Default,,0000,0000,0000,not-known-server-error,You must be known to server before sending this command
+Dialogue: 0,0:13:08.00,0:13:10.00,Default,,0000,0000,0000,client-drop-server-error,Client drop: {} -- {}
+Dialogue: 0,0:13:10.00,0:13:12.00,Default,,0000,0000,0000,password-required-server-error,Password required
+Dialogue: 0,0:13:12.00,0:13:14.00,Default,,0000,0000,0000,wrong-password-server-error,Wrong password supplied
+Dialogue: 0,0:13:14.00,0:13:16.00,Default,,0000,0000,0000,hello-server-error,Not enough Hello arguments
+Dialogue: 0,0:13:16.00,0:13:18.00,Default,,0000,0000,0000,playlist-selection-changed-notification,{} changed the playlist selection
+Dialogue: 0,0:13:18.00,0:13:20.00,Default,,0000,0000,0000,playlist-contents-changed-notification,{} updated the playlist
+Dialogue: 0,0:13:20.00,0:13:22.00,Default,,0000,0000,0000,cannot-find-file-for-playlist-switch-error,Could not find file {} in media directories for playlist switch!
+Dialogue: 0,0:13:22.00,0:13:24.00,Default,,0000,0000,0000,cannot-add-duplicate-error,Could not add second entry for '{}' to the playlist as no duplicates are allowed.
+Dialogue: 0,0:13:24.00,0:13:26.00,Default,,0000,0000,0000,cannot-add-unsafe-path-error,Could not automatically load {} because it is not on a trusted domain. You can switch to the URL manually by double clicking it in the playlist, and add trusted domains via File->Advanced->Set Trusted Domains. If you right click on a URL then you can add its domain as a trusted domain via the context menu.
+Dialogue: 0,0:13:26.00,0:13:28.00,Default,,0000,0000,0000,sharedplaylistenabled-label,Enable shared playlists
+Dialogue: 0,0:13:28.00,0:13:30.00,Default,,0000,0000,0000,removefromplaylist-menu-label,Remove from playlist
+Dialogue: 0,0:13:30.00,0:13:32.00,Default,,0000,0000,0000,shuffleremainingplaylist-menu-label,Shuffle remaining playlist
+Dialogue: 0,0:13:32.00,0:13:34.00,Default,,0000,0000,0000,shuffleentireplaylist-menu-label,Shuffle entire playlist
+Dialogue: 0,0:13:34.00,0:13:36.00,Default,,0000,0000,0000,undoplaylist-menu-label,Undo last change to playlist
+Dialogue: 0,0:13:36.00,0:13:38.00,Default,,0000,0000,0000,addfilestoplaylist-menu-label,Add file(s) to bottom of playlist
+Dialogue: 0,0:13:38.00,0:13:40.00,Default,,0000,0000,0000,addurlstoplaylist-menu-label,Add URL(s) to bottom of playlist
+Dialogue: 0,0:13:40.00,0:13:42.00,Default,,0000,0000,0000,editplaylist-menu-label,Edit playlist
+Dialogue: 0,0:13:42.00,0:13:44.00,Default,,0000,0000,0000,open-containing-folder,Open folder containing this file
+Dialogue: 0,0:13:44.00,0:13:46.00,Default,,0000,0000,0000,addyourfiletoplaylist-menu-label,Add your file to playlist
+Dialogue: 0,0:13:46.00,0:13:48.00,Default,,0000,0000,0000,addotherusersfiletoplaylist-menu-label,Add {}'s file to playlist
+Dialogue: 0,0:13:48.00,0:13:50.00,Default,,0000,0000,0000,addyourstreamstoplaylist-menu-label,Add your stream to playlist
+Dialogue: 0,0:13:50.00,0:13:52.00,Default,,0000,0000,0000,addotherusersstreamstoplaylist-menu-label,Add {}' stream to playlist
+Dialogue: 0,0:13:52.00,0:13:54.00,Default,,0000,0000,0000,openusersstream-menu-label,Open {}'s stream
+Dialogue: 0,0:13:54.00,0:13:56.00,Default,,0000,0000,0000,openusersfile-menu-label,Open {}'s file
+Dialogue: 0,0:13:56.00,0:13:58.00,Default,,0000,0000,0000,playlist-instruction-item-message,Drag file here to add it to the shared playlist.
+Dialogue: 0,0:13:58.00,0:14:00.00,Default,,0000,0000,0000,sharedplaylistenabled-tooltip,Room operators can add files to a synced playlist to make it easy for everyone to watching the same thing. Configure media directories under 'Misc'.
+Dialogue: 0,0:14:00.00,0:14:02.00,Default,,0000,0000,0000,playlist-empty-error,Playlist is currently empty.
+Dialogue: 0,0:14:02.00,0:14:04.00,Default,,0000,0000,0000,playlist-invalid-index-error,Invalid playlist index
diff --git a/syncplay/messages_en.py b/syncplay/messages_en.py
old mode 100755
new mode 100644
index a546a45..d792774
--- a/syncplay/messages_en.py
+++ b/syncplay/messages_en.py
@@ -35,7 +35,7 @@ en = {
"left-notification": "{} has left", # User
"left-paused-notification": "{} left, {} paused", # User who left, User who paused
"playing-notification": "{} is playing '{}' ({})", # User, file, duration
- "playing-notification/room-addendum": " in room: '{}'", # Room
+ "playing-notification/room-addendum": " in room: '{}'", # Room
"not-all-ready": "Not ready: {}", # Usernames
"all-users-ready": "Everyone is ready ({} users)", # Number of ready users
@@ -163,11 +163,11 @@ en = {
"argument-description": 'Solution to synchronize playback of multiple media player instances over the network.',
"argument-epilog": 'If no options supplied _config values will be used',
"nogui-argument": 'show no GUI',
- "host-argument": 'server\'s address',
+ "host-argument": "server's address",
"name-argument": 'desired username',
"debug-argument": 'debug mode',
"force-gui-prompt-argument": 'make configuration prompt appear',
- "no-store-argument": 'don\'t store values in .syncplay',
+ "no-store-argument": "don't store values in .syncplay",
"room-argument": 'default room',
"password-argument": 'server password',
"player-path-argument": 'path to your player executable',
diff --git a/syncplay/messages_fr.ass b/syncplay/messages_fr.ass
new file mode 100644
index 0000000..21c45c7
--- /dev/null
+++ b/syncplay/messages_fr.ass
@@ -0,0 +1,441 @@
+[Script Info]
+; Script generated by Aegisub 3.2.2
+; http://www.aegisub.org/
+Title: Default Aegisub file
+ScriptType: v4.00+
+WrapStyle: 0
+ScaledBorderAndShadow: yes
+YCbCr Matrix: None
+
+[Aegisub Project Garbage]
+Scroll Position: 104
+Active Line: 122
+
+[V4+ Styles]
+Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
+Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1
+
+[Events]
+Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
+Dialogue: 0,0:00:00.00,0:00:02.00,Default,,0,0,0,LANGUAGE,Français
+Dialogue: 0,0:00:02.00,0:00:04.00,Default,,0,0,0,config-cleared-notification,Paramètres effacés. Les modifications seront enregistrées lorsque vous enregistrez une configuration valide.
+Dialogue: 0,0:00:04.00,0:00:06.00,Default,,0,0,0,relative-config-notification,Fichiers de configuration relatifs chargés: {}
+Dialogue: 0,0:00:06.00,0:00:08.00,Default,,0,0,0,connection-attempt-notification,Tentative de connexion à {}:{}
+Dialogue: 0,0:00:08.00,0:00:10.00,Default,,0,0,0,reconnection-attempt-notification,Connexion avec le serveur perdue, tentative de reconnexion
+Dialogue: 0,0:00:10.00,0:00:12.00,Default,,0,0,0,disconnection-notification,Déconnecté du serveur
+Dialogue: 0,0:00:12.00,0:00:14.00,Default,,0,0,0,connection-failed-notification,Échec de la connexion avec le serveur
+Dialogue: 0,0:00:14.00,0:00:16.00,Default,,0,0,0,connected-successful-notification,Connexion réussie au serveur
+Dialogue: 0,0:00:16.00,0:00:18.00,Default,,0,0,0,retrying-notification,%s, nouvelle tentative dans %d secondes...
+Dialogue: 0,0:00:18.00,0:00:20.00,Default,,0,0,0,reachout-successful-notification,Vous avez atteint {} ({})
+Dialogue: 0,0:00:20.00,0:00:22.00,Default,,0,0,0,rewind-notification,Retour en arrière en raison du décalage de temps avec {}
+Dialogue: 0,0:00:22.00,0:00:24.00,Default,,0,0,0,fastforward-notification,Avance rapide en raison du décalage de temps avec {}
+Dialogue: 0,0:00:24.00,0:00:26.00,Default,,0,0,0,slowdown-notification,Ralentissement dû au décalage de temps avec {}
+Dialogue: 0,0:00:26.00,0:00:28.00,Default,,0,0,0,revert-notification,Retour à la vitesse normale
+Dialogue: 0,0:00:28.00,0:00:30.00,Default,,0,0,0,pause-notification,{} en pause
+Dialogue: 0,0:00:30.00,0:00:32.00,Default,,0,0,0,unpause-notification,{} non suspendu
+Dialogue: 0,0:00:32.00,0:00:34.00,Default,,0,0,0,seek-notification,{} est passé de {} à {}
+Dialogue: 0,0:00:34.00,0:00:36.00,Default,,0,0,0,current-offset-notification,Décalage actuel: {}secondes
+Dialogue: 0,0:00:36.00,0:00:38.00,Default,,0,0,0,media-directory-list-updated-notification,Les répertoires multimédias Syncplay ont été mis à jour.
+Dialogue: 0,0:00:38.00,0:00:40.00,Default,,0,0,0,room-join-notification,{} a rejoint la salle: '{}'
+Dialogue: 0,0:00:40.00,0:00:42.00,Default,,0,0,0,left-notification,{} est parti
+Dialogue: 0,0:00:42.00,0:00:44.00,Default,,0,0,0,left-paused-notification,{} restants, {} en pause
+Dialogue: 0,0:00:44.00,0:00:46.00,Default,,0,0,0,playing-notification,{} est en train de jouer '{}' ({})
+Dialogue: 0,0:00:46.00,0:00:48.00,Default,,0,0,0,playing-notification/room-addendum,dans le salon: '{}'
+Dialogue: 0,0:00:48.00,0:00:50.00,Default,,0,0,0,not-all-ready,Pas prêt: {}
+Dialogue: 0,0:00:50.00,0:00:52.00,Default,,0,0,0,all-users-ready,Tout le monde est prêt ({} utilisateurs)
+Dialogue: 0,0:00:52.00,0:00:54.00,Default,,0,0,0,ready-to-unpause-notification,Vous êtes maintenant défini comme prêt - réactivez la pause pour réactiver
+Dialogue: 0,0:00:54.00,0:00:56.00,Default,,0,0,0,set-as-ready-notification,Vous êtes maintenant défini comme prêt
+Dialogue: 0,0:00:56.00,0:00:58.00,Default,,0,0,0,set-as-not-ready-notification,Vous êtes maintenant défini comme non prêt
+Dialogue: 0,0:00:58.00,0:01:00.00,Default,,0,0,0,autoplaying-notification,Lecture automatique dans {}...
+Dialogue: 0,0:01:00.00,0:01:02.00,Default,,0,0,0,identifying-as-controller-notification,Identification en tant qu'opérateur de salle avec le mot de passe '{}'...
+Dialogue: 0,0:01:02.00,0:01:04.00,Default,,0,0,0,failed-to-identify-as-controller-notification,{} n'a pas réussi à s'identifier en tant qu'opérateur de salle.
+Dialogue: 0,0:01:04.00,0:01:06.00,Default,,0,0,0,authenticated-as-controller-notification,{} authentifié en tant qu'opérateur de salle
+Dialogue: 0,0:01:06.00,0:01:08.00,Default,,0,0,0,created-controlled-room-notification,Salle gérée créée «{}» avec le mot de passe «{}». Veuillez conserver ces informations pour référence future !\n\nDans les salons gérés, tout le monde est synchronisé avec le ou les opérateurs de salon qui sont les seuls à pouvoir mettre en pause, reprendre, se déplacer dans la lecture et modifier la liste de lecture.\n\nVous devez demander aux spectateurs réguliers de rejoindre le salon '{}' mais les opérateurs de salon peuvent rejoindre le salon '{}' pour s'authentifier automatiquement.
+Dialogue: 0,0:01:08.00,0:01:10.00,Default,,0,0,0,file-different-notification,Le fichier que vous lisez semble être différent de celui de {}
+Dialogue: 0,0:01:10.00,0:01:12.00,Default,,0,0,0,file-differences-notification,Votre fichier diffère de la (des) manière(s) suivante(s): {}
+Dialogue: 0,0:01:12.00,0:01:14.00,Default,,0,0,0,room-file-differences,Différences de fichiers: {}
+Dialogue: 0,0:01:14.00,0:01:16.00,Default,,0,0,0,file-difference-filename,Nom
+Dialogue: 0,0:01:16.00,0:01:18.00,Default,,0,0,0,file-difference-filesize,Taille
+Dialogue: 0,0:01:18.00,0:01:20.00,Default,,0,0,0,file-difference-duration,durée
+Dialogue: 0,0:01:20.00,0:01:22.00,Default,,0,0,0,alone-in-the-room,Vous êtes seul dans le salon
+Dialogue: 0,0:01:22.00,0:01:24.00,Default,,0,0,0,different-filesize-notification,(leur taille de fichier est différente de la vôtre!)
+Dialogue: 0,0:01:24.00,0:01:26.00,Default,,0,0,0,userlist-playing-notification,{} est en train de jouer:
+Dialogue: 0,0:01:26.00,0:01:28.00,Default,,0,0,0,file-played-by-notification,Fichier: {} est lu par:
+Dialogue: 0,0:01:28.00,0:01:30.00,Default,,0,0,0,no-file-played-notification,{} ne lit pas de fichier
+Dialogue: 0,0:01:30.00,0:01:32.00,Default,,0,0,0,notplaying-notification,Les personnes qui ne lisent aucun fichier:
+Dialogue: 0,0:01:32.00,0:01:34.00,Default,,0,0,0,userlist-room-notification,Dans la chambre '{}':
+Dialogue: 0,0:01:34.00,0:01:36.00,Default,,0,0,0,userlist-file-notification,Fichier
+Dialogue: 0,0:01:36.00,0:01:38.00,Default,,0,0,0,controller-userlist-userflag,Opérateur
+Dialogue: 0,0:01:38.00,0:01:40.00,Default,,0,0,0,ready-userlist-userflag,Prêt
+Dialogue: 0,0:01:40.00,0:01:42.00,Default,,0,0,0,update-check-failed-notification,Impossible de vérifier automatiquement si Syncplay {} est à jour. Vous voulez visiter https://syncplay.pl/ pour vérifier manuellement les mises à jour?
+Dialogue: 0,0:01:42.00,0:01:44.00,Default,,0,0,0,syncplay-uptodate-notification,Syncplay est à jour
+Dialogue: 0,0:01:44.00,0:01:46.00,Default,,0,0,0,syncplay-updateavailable-notification,Une nouvelle version de Syncplay est disponible. Voulez-vous visiter la page de publication?
+Dialogue: 0,0:01:46.00,0:01:48.00,Default,,0,0,0,mplayer-file-required-notification,Syncplay à l'aide de mplayer nécessite que vous fournissiez un fichier au démarrage
+Dialogue: 0,0:01:48.00,0:01:50.00,Default,,0,0,0,mplayer-file-required-notification/example,Exemple d'utilisation: syncplay [options] [url|chemin/]nom de fichier
+Dialogue: 0,0:01:50.00,0:01:52.00,Default,,0,0,0,mplayer2-required,Syncplay est incompatible avec MPlayer 1.x, veuillez utiliser mplayer2 ou mpv
+Dialogue: 0,0:01:52.00,0:01:54.00,Default,,0,0,0,unrecognized-command-notification,commande non reconnue
+Dialogue: 0,0:01:54.00,0:01:56.00,Default,,0,0,0,commandlist-notification,Commandes disponibles:
+Dialogue: 0,0:01:56.00,0:01:58.00,Default,,0,0,0,commandlist-notification/room,\tr [nom] - changer de chambre
+Dialogue: 0,0:01:58.00,0:02:00.00,Default,,0,0,0,commandlist-notification/list,\tl - afficher la liste des utilisateurs
+Dialogue: 0,0:02:00.00,0:02:02.00,Default,,0,0,0,commandlist-notification/undo,\tu - annuler la dernière recherche
+Dialogue: 0,0:02:02.00,0:02:04.00,Default,,0,0,0,commandlist-notification/pause,\tp - basculer sur pause
+Dialogue: 0,0:02:04.00,0:02:06.00,Default,,0,0,0,commandlist-notification/seek,\t[s][+-]temps - recherche la valeur de temps donnée, si + ou - n'est pas spécifié c'est le temps absolu en secondes ou min:sec
+Dialogue: 0,0:02:06.00,0:02:08.00,Default,,0,0,0,commandlist-notification/help,\th - cette aide
+Dialogue: 0,0:02:08.00,0:02:10.00,Default,,0,0,0,commandlist-notification/toggle,\tt - bascule si vous êtes prêt à regarder ou non
+Dialogue: 0,0:02:10.00,0:02:12.00,Default,,0,0,0,commandlist-notification/create,\tc [nom] - crée une salle gérée en utilisant le nom de la salle actuelle
+Dialogue: 0,0:02:12.00,0:02:14.00,Default,,0,0,0,commandlist-notification/auth,\tun [mot de passe] - s'authentifier en tant qu'opérateur de salle avec le mot de passe opérateur
+Dialogue: 0,0:02:14.00,0:02:16.00,Default,,0,0,0,commandlist-notification/chat,\tch [message] - envoyer un message de chat dans une pièce
+Dialogue: 0,0:02:16.00,0:02:18.00,Default,,0,0,0,commandList-notification/queue,\tqa [fichier/url] - ajoute un fichier ou une URL au bas de la liste de lecture
+Dialogue: 0,0:02:18.00,0:02:20.00,Default,,0,0,0,commandList-notification/playlist,\tql - afficher la liste de lecture actuelle
+Dialogue: 0,0:02:20.00,0:02:22.00,Default,,0,0,0,commandList-notification/select,\tqs [index] - sélectionnez l'entrée donnée dans la liste de lecture
+Dialogue: 0,0:02:22.00,0:02:24.00,Default,,0,0,0,commandList-notification/delete,\tqd [index] - supprime l'entrée donnée de la liste de lecture
+Dialogue: 0,0:02:24.00,0:02:26.00,Default,,0,0,0,syncplay-version-notification,Version de Syncplay: {}
+Dialogue: 0,0:02:26.00,0:02:28.00,Default,,0,0,0,more-info-notification,Plus d'informations disponibles sur: {}
+Dialogue: 0,0:02:28.00,0:02:30.00,Default,,0,0,0,gui-data-cleared-notification,Syncplay a effacé les données d'état de chemin et de fenêtre utilisées par l'interface graphique.
+Dialogue: 0,0:02:30.00,0:02:32.00,Default,,0,0,0,language-changed-msgbox-label,La langue sera modifiée lorsque vous exécuterez Syncplay.
+Dialogue: 0,0:02:32.00,0:02:34.00,Default,,0,0,0,promptforupdate-label,Est-ce que Syncplay peut vérifier automatiquement les mises à jour de temps en temps?
+Dialogue: 0,0:02:34.00,0:02:36.00,Default,,0,0,0,media-player-latency-warning,Avertissement: Le lecteur multimédia a mis {}secondes à répondre. Si vous rencontrez des problèmes de synchronisation, fermez les applications pour libérer des ressources système, et si cela ne fonctionne pas, essayez un autre lecteur multimédia.
+Dialogue: 0,0:02:36.00,0:02:38.00,Default,,0,0,0,mpv-unresponsive-error,mpv n'a pas répondu pendant {} secondes et semble donc avoir mal fonctionné. Veuillez redémarrer Syncplay.
+Dialogue: 0,0:02:38.00,0:02:40.00,Default,,0,0,0,enter-to-exit-prompt,Appuyez sur entrée pour quitter
+Dialogue: 0,0:02:40.00,0:02:42.00,Default,,0,0,0,missing-arguments-error,Certains arguments nécessaires sont manquants, reportez-vous à --help
+Dialogue: 0,0:02:42.00,0:02:44.00,Default,,0,0,0,server-timeout-error,La connexion avec le serveur a expiré
+Dialogue: 0,0:02:44.00,0:02:46.00,Default,,0,0,0,mpc-slave-error,Impossible de démarrer MPC en mode esclave!
+Dialogue: 0,0:02:46.00,0:02:48.00,Default,,0,0,0,mpc-version-insufficient-error,La version MPC n'est pas suffisante, veuillez utiliser `mpc-hc` >= `{}`
+Dialogue: 0,0:02:48.00,0:02:50.00,Default,,0,0,0,mpc-be-version-insufficient-error,La version MPC n'est pas suffisante, veuillez utiliser `mpc-be` >= `{}`
+Dialogue: 0,0:02:50.00,0:02:52.00,Default,,0,0,0,mpv-version-error,Syncplay n'est pas compatible avec cette version de mpv. Veuillez utiliser une version différente de mpv (par exemple Git HEAD).
+Dialogue: 0,0:02:52.00,0:02:54.00,Default,,0,0,0,mpv-failed-advice,La raison pour laquelle mpv ne peut pas démarrer peut être due à l'utilisation d'arguments de ligne de commande non pris en charge ou à une version non prise en charge de mpv.
+Dialogue: 0,0:02:54.00,0:02:56.00,Default,,0,0,0,player-file-open-error,Le lecteur n'a pas réussi à ouvrir le fichier
+Dialogue: 0,0:02:56.00,0:02:58.00,Default,,0,0,0,player-path-error,Le chemin du lecteur n'est pas défini correctement. Les lecteurs pris en charge sont : mpv, mpv.net, VLC, MPC-HC, MPC-BE, mplayer2 et IINA
+Dialogue: 0,0:02:58.00,0:03:00.00,Default,,0,0,0,hostname-empty-error,Le nom d'hôte ne peut pas être vide
+Dialogue: 0,0:03:00.00,0:03:02.00,Default,,0,0,0,empty-error,{} ne peut pas être vide
+Dialogue: 0,0:03:02.00,0:03:04.00,Default,,0,0,0,media-player-error,Erreur du lecteur multimédia: "{}"
+Dialogue: 0,0:03:04.00,0:03:06.00,Default,,0,0,0,unable-import-gui-error,Impossible d'importer les bibliothèques GUI. Si vous n'avez pas installé PySide, vous devrez l'installer pour que l'interface graphique fonctionne.
+Dialogue: 0,0:03:06.00,0:03:08.00,Default,,0,0,0,unable-import-twisted-error,Impossible d'importer Twisted. Veuillez installer Twisted v16.4.0 ou une version ultérieure.
+Dialogue: 0,0:03:08.00,0:03:10.00,Default,,0,0,0,arguments-missing-error,Certains arguments nécessaires sont manquants, reportez-vous à --help
+Dialogue: 0,0:03:10.00,0:03:12.00,Default,,0,0,0,unable-to-start-client-error,Impossible de démarrer le client
+Dialogue: 0,0:03:12.00,0:03:14.00,Default,,0,0,0,player-path-config-error,Le chemin du lecteur n'est pas défini correctement. Les lecteurs pris en charge sont : mpv, mpv.net, VLC, MPC-HC, MPC-BE, mplayer2 et IINA.
+Dialogue: 0,0:03:14.00,0:03:16.00,Default,,0,0,0,no-file-path-config-error,Le fichier doit être sélectionné avant de démarrer votre lecteur
+Dialogue: 0,0:03:16.00,0:03:18.00,Default,,0,0,0,no-hostname-config-error,Le nom d'hôte ne peut pas être vide
+Dialogue: 0,0:03:18.00,0:03:20.00,Default,,0,0,0,invalid-port-config-error,Le port doit être valide
+Dialogue: 0,0:03:20.00,0:03:22.00,Default,,0,0,0,empty-value-config-error,{} ne peut pas être vide
+Dialogue: 0,0:03:22.00,0:03:24.00,Default,,0,0,0,not-json-error,Pas une chaîne encodée en json
+Dialogue: 0,0:03:24.00,0:03:26.00,Default,,0,0,0,hello-arguments-error,Pas assez d'arguments pour Hello
+Dialogue: 0,0:03:26.00,0:03:28.00,Default,,0,0,0,version-mismatch-error,Non-concordance entre les versions du client et du serveur
+Dialogue: 0,0:03:28.00,0:03:30.00,Default,,0,0,0,vlc-failed-connection,Échec de la connexion à VLC. Si vous n'avez pas installé syncplay.lua et utilisez la dernière version de VLC, veuillez vous référer à https://syncplay.pl/LUA/ pour obtenir des instructions. Syncplay et VLC 4 ne sont actuellement pas compatibles, utilisez donc VLC 3 ou une alternative telle que mpv.
+Dialogue: 0,0:03:30.00,0:03:32.00,Default,,0,0,0,vlc-failed-noscript,VLC a signalé que le script d'interface syncplay.lua n'a pas été installé. Veuillez vous référer à https://syncplay.pl/LUA/ pour obtenir des instructions.
+Dialogue: 0,0:03:32.00,0:03:34.00,Default,,0,0,0,vlc-failed-versioncheck,Cette version de VLC n'est pas prise en charge par Syncplay.
+Dialogue: 0,0:03:34.00,0:03:36.00,Default,,0,0,0,vlc-initial-warning,VLC ne fournit pas toujours des informations de position précises à Syncplay, en particulier pour les fichiers .mp4 et .avi. Si vous rencontrez des problèmes de recherche erronée, essayez un autre lecteur multimédia tel que mpv (ou mpv.net pour les utilisateurs de Windows).
+Dialogue: 0,0:03:36.00,0:03:38.00,Default,,0,0,0,feature-sharedPlaylists,listes de lecture partagées
+Dialogue: 0,0:03:38.00,0:03:40.00,Default,,0,0,0,feature-chat,chat
+Dialogue: 0,0:03:40.00,0:03:42.00,Default,,0,0,0,feature-readiness,préparation
+Dialogue: 0,0:03:42.00,0:03:44.00,Default,,0,0,0,feature-managedRooms,salons gérés
+Dialogue: 0,0:03:44.00,0:03:46.00,Default,,0,0,0,not-supported-by-server-error,La fonctionnalité {} n'est pas prise en charge par ce serveur.
+Dialogue: 0,0:03:46.00,0:03:48.00,Default,,0,0,0,shared-playlists-not-supported-by-server-error,La fonctionnalité de listes de lecture partagées peut ne pas être prise en charge par le serveur. Pour s'assurer qu'il fonctionne correctement, il faut un serveur exécutant Syncplay {}+, mais le serveur exécute Syncplay {}.
+Dialogue: 0,0:03:48.00,0:03:50.00,Default,,0,0,0,shared-playlists-disabled-by-server-error,La fonctionnalité de liste de lecture partagée a été désactivée dans la configuration du serveur. Pour utiliser cette fonctionnalité, vous devrez vous connecter à un autre serveur.
+Dialogue: 0,0:03:50.00,0:03:52.00,Default,,0,0,0,invalid-seek-value,Valeur de recherche non valide
+Dialogue: 0,0:03:52.00,0:03:54.00,Default,,0,0,0,invalid-offset-value,Valeur de décalage non valide
+Dialogue: 0,0:03:54.00,0:03:56.00,Default,,0,0,0,switch-file-not-found-error,Impossible de passer au fichier ''. Syncplay recherche dans les répertoires multimédias spécifiés.
+Dialogue: 0,0:03:56.00,0:03:58.00,Default,,0,0,0,folder-search-timeout-error,La recherche de médias dans les répertoires de médias a été abandonnée car la recherche dans '{}' a pris trop de temps. Cela se produira si vous sélectionnez un dossier avec trop de sous-dossiers dans votre liste de dossiers multimédias à parcourir. Pour que le basculement automatique des fichiers fonctionne à nouveau, veuillez sélectionner Fichier->Définir les répertoires multimédias dans la barre de menu et supprimez ce répertoire ou remplacez-le par un sous-dossier approprié. Si le dossier est correct, vous pouvez le réactiver en sélectionnant Fichier->Définir les répertoires multimédias et en appuyant sur «OK».
+Dialogue: 0,0:03:58.00,0:04:00.00,Default,,0,0,0,folder-search-first-file-timeout-error,La recherche de média dans '{}' a été abandonnée car elle a pris trop de temps pour accéder au répertoire. Cela peut arriver s'il s'agit d'un lecteur réseau ou si vous configurez votre lecteur pour qu'il ralentisse après une période d'inactivité. Pour que le basculement automatique des fichiers fonctionne à nouveau, accédez à Fichier-> Définir les répertoires multimédias et supprimez le répertoire ou résolvez le problème (par exemple en modifiant les paramètres d'économie d'énergie).
+Dialogue: 0,0:04:00.00,0:04:02.00,Default,,0,0,0,added-file-not-in-media-directory-error,Vous avez chargé un fichier dans '{}' qui n'est pas un répertoire média connu. Vous pouvez l'ajouter en tant que répertoire multimédia en sélectionnant Fichier->Définir les répertoires multimédias dans la barre de menus.
+Dialogue: 0,0:04:02.00,0:04:04.00,Default,,0,0,0,no-media-directories-error,Aucun répertoire multimédia n'a été défini. Pour que les fonctionnalités de liste de lecture partagée et de changement de fichier fonctionnent correctement, sélectionnez Fichier-> Définir les répertoires multimédias et spécifiez où Syncplay doit rechercher les fichiers multimédias.
+Dialogue: 0,0:04:04.00,0:04:06.00,Default,,0,0,0,cannot-find-directory-error,Impossible de trouver le répertoire multimédia '{}'. Pour mettre à jour votre liste de répertoires multimédias, veuillez sélectionner Fichier->Définir les répertoires multimédias dans la barre de menu et spécifiez où Syncplay doit chercher pour trouver les fichiers multimédias.
+Dialogue: 0,0:04:06.00,0:04:08.00,Default,,0,0,0,failed-to-load-server-list-error,Échec du chargement de la liste des serveurs publics. Veuillez visiter https://www.syncplay.pl/ dans votre navigateur.
+Dialogue: 0,0:04:08.00,0:04:10.00,Default,,0,0,0,argument-description,Solution pour synchroniser la lecture de plusieurs instances de lecteur multimédia sur le réseau.
+Dialogue: 0,0:04:10.00,0:04:12.00,Default,,0,0,0,argument-epilog,Si aucune option n'est fournie, les valeurs _config seront utilisées
+Dialogue: 0,0:04:12.00,0:04:14.00,Default,,0,0,0,nogui-argument,masquer l'interface graphique
+Dialogue: 0,0:04:14.00,0:04:16.00,Default,,0,0,0,host-argument,adresse du serveur
+Dialogue: 0,0:04:16.00,0:04:18.00,Default,,0,0,0,name-argument,nom d'utilisateur souhaité
+Dialogue: 0,0:04:18.00,0:04:20.00,Default,,0,0,0,debug-argument,Mode débogage
+Dialogue: 0,0:04:20.00,0:04:22.00,Default,,0,0,0,force-gui-prompt-argument,faire apparaître l'invite de configuration
+Dialogue: 0,0:04:22.00,0:04:24.00,Default,,0,0,0,no-store-argument,ne pas stocker de valeurs dans .syncplay
+Dialogue: 0,0:04:24.00,0:04:26.00,Default,,0,0,0,room-argument,salon par défaut
+Dialogue: 0,0:04:26.00,0:04:28.00,Default,,0,0,0,password-argument,Mot de passe du serveur
+Dialogue: 0,0:04:28.00,0:04:30.00,Default,,0,0,0,player-path-argument,chemin d'accès à l'exécutable de votre lecteur
+Dialogue: 0,0:04:30.00,0:04:32.00,Default,,0,0,0,file-argument,fichier à lire
+Dialogue: 0,0:04:32.00,0:04:34.00,Default,,0,0,0,args-argument,options du lecteur, si vous devez passer des options commençant par - ajoutez-les au début avec un seul argument '--'
+Dialogue: 0,0:04:34.00,0:04:36.00,Default,,0,0,0,clear-gui-data-argument,réinitialise les données GUI du chemin et de l'état de la fenêtre stockées en tant que QSettings
+Dialogue: 0,0:04:36.00,0:04:38.00,Default,,0,0,0,language-argument,langue pour les messages Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)
+Dialogue: 0,0:04:38.00,0:04:40.00,Default,,0,0,0,version-argument,imprime votre version
+Dialogue: 0,0:04:40.00,0:04:42.00,Default,,0,0,0,version-message,Vous utilisez Syncplay version {} ({})
+Dialogue: 0,0:04:42.00,0:04:44.00,Default,,0,0,0,load-playlist-from-file-argument,charge la liste de lecture à partir d'un fichier texte (une entrée par ligne)
+Dialogue: 0,0:04:44.00,0:04:46.00,Default,,0,0,0,config-window-title,configuration Syncplay
+Dialogue: 0,0:04:46.00,0:04:48.00,Default,,0,0,0,connection-group-title,Paramètres de connexion
+Dialogue: 0,0:04:48.00,0:04:50.00,Default,,0,0,0,host-label,Adresse du serveur:
+Dialogue: 0,0:04:50.00,0:04:52.00,Default,,0,0,0,name-label,Nom d'utilisateur (facultatif):
+Dialogue: 0,0:04:52.00,0:04:54.00,Default,,0,0,0,password-label,Mot de passe du serveur (le cas échéant):
+Dialogue: 0,0:04:54.00,0:04:56.00,Default,,0,0,0,room-label,Salon par défaut:
+Dialogue: 0,0:04:56.00,0:04:58.00,Default,,0,0,0,roomlist-msgbox-label,Modifier la liste des salons (une par ligne)
+Dialogue: 0,0:04:58.00,0:05:00.00,Default,,0,0,0,media-setting-title,Paramètres du lecteur multimédia
+Dialogue: 0,0:05:00.00,0:05:02.00,Default,,0,0,0,executable-path-label,Chemin d'accès au lecteur multimédia:
+Dialogue: 0,0:05:02.00,0:05:04.00,Default,,0,0,0,media-path-label,Chemin d'accès à la vidéo (facultatif):
+Dialogue: 0,0:05:04.00,0:05:06.00,Default,,0,0,0,player-arguments-label,Arguments du joueur (le cas échéant):
+Dialogue: 0,0:05:06.00,0:05:08.00,Default,,0,0,0,browse-label,Parcourir
+Dialogue: 0,0:05:08.00,0:05:10.00,Default,,0,0,0,update-server-list-label,Mettre à jour la liste
+Dialogue: 0,0:05:10.00,0:05:12.00,Default,,0,0,0,more-title,Afficher plus de paramètres
+Dialogue: 0,0:05:12.00,0:05:14.00,Default,,0,0,0,never-rewind-value,Jamais
+Dialogue: 0,0:05:14.00,0:05:16.00,Default,,0,0,0,seconds-suffix,secs
+Dialogue: 0,0:05:16.00,0:05:18.00,Default,,0,0,0,privacy-sendraw-option,Envoyer brut
+Dialogue: 0,0:05:18.00,0:05:20.00,Default,,0,0,0,privacy-sendhashed-option,Envoyer haché
+Dialogue: 0,0:05:20.00,0:05:22.00,Default,,0,0,0,privacy-dontsend-option,Ne pas envoyer
+Dialogue: 0,0:05:22.00,0:05:24.00,Default,,0,0,0,filename-privacy-label,Informations sur le nom de fichier:
+Dialogue: 0,0:05:24.00,0:05:26.00,Default,,0,0,0,filesize-privacy-label,Informations sur la taille du fichier:
+Dialogue: 0,0:05:26.00,0:05:28.00,Default,,0,0,0,checkforupdatesautomatically-label,Rechercher automatiquement les mises à jour de Syncplay
+Dialogue: 0,0:05:28.00,0:05:30.00,Default,,0,0,0,autosavejoinstolist-label,Ajouter les salons que vous rejoignez à la liste des salons
+Dialogue: 0,0:05:30.00,0:05:32.00,Default,,0,0,0,slowondesync-label,Ralentissement en cas de désynchronisation mineure (non pris en charge sur MPC-HC/BE)
+Dialogue: 0,0:05:32.00,0:05:34.00,Default,,0,0,0,rewindondesync-label,Retour en arrière en cas de désynchronisation majeure (recommandé)
+Dialogue: 0,0:05:34.00,0:05:36.00,Default,,0,0,0,fastforwardondesync-label,Avance rapide en cas de retard (recommandé)
+Dialogue: 0,0:05:36.00,0:05:38.00,Default,,0,0,0,dontslowdownwithme-label,Ne jamais ralentir ou rembobiner les autres (expérimental)
+Dialogue: 0,0:05:38.00,0:05:40.00,Default,,0,0,0,pausing-title,Pause
+Dialogue: 0,0:05:40.00,0:05:42.00,Default,,0,0,0,pauseonleave-label,Pause lorsque l'utilisateur quitte (par exemple s'il est déconnecté)
+Dialogue: 0,0:05:42.00,0:05:44.00,Default,,0,0,0,readiness-title,État de préparation initial
+Dialogue: 0,0:05:44.00,0:05:46.00,Default,,0,0,0,readyatstart-label,Définissez-moi comme «prêt à regarder» par défaut
+Dialogue: 0,0:05:46.00,0:05:48.00,Default,,0,0,0,forceguiprompt-label,Ne pas toujours afficher la fenêtre de configuration Syncplay
+Dialogue: 0,0:05:48.00,0:05:50.00,Default,,0,0,0,showosd-label,Activer les Messages OSD
+Dialogue: 0,0:05:50.00,0:05:52.00,Default,,0,0,0,showosdwarnings-label,Inclure des avertissements (par exemple, lorsque les fichiers sont différents, les utilisateurs ne sont pas prêts)
+Dialogue: 0,0:05:52.00,0:05:54.00,Default,,0,0,0,showsameroomosd-label,Inclure des événements dans votre salon
+Dialogue: 0,0:05:54.00,0:05:56.00,Default,,0,0,0,shownoncontrollerosd-label,Inclure les événements des non-opérateurs dans les salons gérés
+Dialogue: 0,0:05:56.00,0:05:58.00,Default,,0,0,0,showdifferentroomosd-label,Inclure des événements dans d'autres salons
+Dialogue: 0,0:05:58.00,0:06:00.00,Default,,0,0,0,showslowdownosd-label,Inclure les notifications de ralentissement/annulation
+Dialogue: 0,0:06:00.00,0:06:02.00,Default,,0,0,0,language-label,Langue:
+Dialogue: 0,0:06:02.00,0:06:04.00,Default,,0,0,0,automatic-language,Défaut ({})
+Dialogue: 0,0:06:04.00,0:06:06.00,Default,,0,0,0,showdurationnotification-label,Avertir des incohérences de durée de média
+Dialogue: 0,0:06:06.00,0:06:08.00,Default,,0,0,0,basics-label,Réglages de base
+Dialogue: 0,0:06:08.00,0:06:10.00,Default,,0,0,0,readiness-label,Jouer pause
+Dialogue: 0,0:06:10.00,0:06:12.00,Default,,0,0,0,misc-label,Divers
+Dialogue: 0,0:06:12.00,0:06:14.00,Default,,0,0,0,core-behaviour-title,Comportement du salon principal
+Dialogue: 0,0:06:14.00,0:06:16.00,Default,,0,0,0,syncplay-internals-title,procédures internes
+Dialogue: 0,0:06:16.00,0:06:18.00,Default,,0,0,0,syncplay-mediasearchdirectories-title,Répertoires pour rechercher des médias
+Dialogue: 0,0:06:18.00,0:06:20.00,Default,,0,0,0,syncplay-mediasearchdirectories-label,Répertoires pour rechercher des médias (un chemin par ligne)
+Dialogue: 0,0:06:20.00,0:06:22.00,Default,,0,0,0,sync-label,Synchroniser
+Dialogue: 0,0:06:22.00,0:06:24.00,Default,,0,0,0,sync-otherslagging-title,Si d'autres sont à la traîne...
+Dialogue: 0,0:06:24.00,0:06:26.00,Default,,0,0,0,sync-youlaggging-title,Si vous êtes à la traîne...
+Dialogue: 0,0:06:26.00,0:06:28.00,Default,,0,0,0,messages-label,Messages
+Dialogue: 0,0:06:28.00,0:06:30.00,Default,,0,0,0,messages-osd-title,Paramètres d'affichage à l'écran
+Dialogue: 0,0:06:30.00,0:06:32.00,Default,,0,0,0,messages-other-title,Autres paramètres d'affichage
+Dialogue: 0,0:06:32.00,0:06:34.00,Default,,0,0,0,chat-label,Chat
+Dialogue: 0,0:06:34.00,0:06:36.00,Default,,0,0,0,privacy-label,Sécurité données
+Dialogue: 0,0:06:36.00,0:06:38.00,Default,,0,0,0,privacy-title,Paramètres de confidentialité
+Dialogue: 0,0:06:38.00,0:06:40.00,Default,,0,0,0,unpause-title,Si vous appuyez sur play, définissez comme prêt et:
+Dialogue: 0,0:06:40.00,0:06:42.00,Default,,0,0,0,unpause-ifalreadyready-option,Annuler la pause si déjà défini comme prêt
+Dialogue: 0,0:06:42.00,0:06:44.00,Default,,0,0,0,unpause-ifothersready-option,Reprendre la pause si déjà prêt ou si d'autres personnes dans la pièce sont prêtes (par défaut)
+Dialogue: 0,0:06:44.00,0:06:46.00,Default,,0,0,0,unpause-ifminusersready-option,Annuler la pause si déjà prêt ou si tous les autres sont prêts et utilisateurs minimum prêts
+Dialogue: 0,0:06:46.00,0:06:48.00,Default,,0,0,0,unpause-always,Toujours reprendre
+Dialogue: 0,0:06:48.00,0:06:50.00,Default,,0,0,0,syncplay-trusteddomains-title,Domaines de confiance (pour les services de streaming et le contenu hébergé)
+Dialogue: 0,0:06:50.00,0:06:52.00,Default,,0,0,0,chat-title,Saisie du message de discussion
+Dialogue: 0,0:06:52.00,0:06:54.00,Default,,0,0,0,chatinputenabled-label,Activer la saisie de discussion via mpv
+Dialogue: 0,0:06:54.00,0:06:56.00,Default,,0,0,0,chatdirectinput-label,Autoriser la saisie de discussion instantanée (éviter d'avoir à appuyer sur la touche Entrée pour discuter)
+Dialogue: 0,0:06:56.00,0:06:58.00,Default,,0,0,0,chatinputfont-label,Police de caractères pour la saisie sur le Chat
+Dialogue: 0,0:06:58.00,0:07:00.00,Default,,0,0,0,chatfont-label,Définir la fonte
+Dialogue: 0,0:07:00.00,0:07:02.00,Default,,0,0,0,chatcolour-label,Définir la couleur
+Dialogue: 0,0:07:02.00,0:07:04.00,Default,,0,0,0,chatinputposition-label,Position de la zone de saisie des messages dans mpv
+Dialogue: 0,0:07:04.00,0:07:06.00,Default,,0,0,0,chat-top-option,Haut
+Dialogue: 0,0:07:06.00,0:07:08.00,Default,,0,0,0,chat-middle-option,Milieu
+Dialogue: 0,0:07:08.00,0:07:10.00,Default,,0,0,0,chat-bottom-option,Bas
+Dialogue: 0,0:07:10.00,0:07:12.00,Default,,0,0,0,chatoutputheader-label,Sortie du message de discussion
+Dialogue: 0,0:07:12.00,0:07:14.00,Default,,0,0,0,chatoutputfont-label,Police de sortie du chat
+Dialogue: 0,0:07:14.00,0:07:16.00,Default,,0,0,0,chatoutputenabled-label,Activer la sortie du chat dans le lecteur multimédia (mpv uniquement pour l'instant)
+Dialogue: 0,0:07:16.00,0:07:18.00,Default,,0,0,0,chatoutputposition-label,Mode de sortie
+Dialogue: 0,0:07:18.00,0:07:20.00,Default,,0,0,0,chat-chatroom-option,Style de salon de discussion
+Dialogue: 0,0:07:20.00,0:07:22.00,Default,,0,0,0,chat-scrolling-option,Style de défilement
+Dialogue: 0,0:07:22.00,0:07:24.00,Default,,0,0,0,mpv-key-tab-hint,[TAB] pour basculer l'accès aux raccourcis des touches de la ligne alphabétique.
+Dialogue: 0,0:07:24.00,0:07:26.00,Default,,0,0,0,mpv-key-hint,[ENTER] pour envoyer un message. [ESC] pour quitter le mode chat.
+Dialogue: 0,0:07:26.00,0:07:28.00,Default,,0,0,0,alphakey-mode-warning-first-line,Vous pouvez temporairement utiliser les anciennes liaisons mpv avec les touches az.
+Dialogue: 0,0:07:28.00,0:07:30.00,Default,,0,0,0,alphakey-mode-warning-second-line,Appuyez sur [TAB] pour revenir au mode de discussion Syncplay.
+Dialogue: 0,0:07:30.00,0:07:32.00,Default,,0,0,0,help-label,Aider
+Dialogue: 0,0:07:32.00,0:07:34.00,Default,,0,0,0,reset-label,Réinitialiser
+Dialogue: 0,0:07:34.00,0:07:36.00,Default,,0,0,0,run-label,Exécuter Syncplay
+Dialogue: 0,0:07:36.00,0:07:38.00,Default,,0,0,0,storeandrun-label,Stocker la configuration et exécuter Syncplay
+Dialogue: 0,0:07:38.00,0:07:40.00,Default,,0,0,0,contact-label,N'hésitez pas à envoyer un e-mail à dev@syncplay.pl , à créer un problème pour signaler un bug/problème via GitHub, à démarrer une discussion pour faire une suggestion ou à poser une question via GitHub, à nous aimer sur Facebook , à nous suivre sur Twitter ou à visiter https://syncplay.pl/ . N'utilisez pas Syncplay pour envoyer des informations sensibles.
+Dialogue: 0,0:07:40.00,0:07:42.00,Default,,0,0,0,joinroom-label,Rejoindre la salle
+Dialogue: 0,0:07:42.00,0:07:44.00,Default,,0,0,0,joinroom-menu-label,Rejoindre la salle {}
+Dialogue: 0,0:07:44.00,0:07:46.00,Default,,0,0,0,seektime-menu-label,Chercher le temps
+Dialogue: 0,0:07:46.00,0:07:48.00,Default,,0,0,0,undoseek-menu-label,Annuler la recherche
+Dialogue: 0,0:07:48.00,0:07:50.00,Default,,0,0,0,play-menu-label,Jouer
+Dialogue: 0,0:07:50.00,0:07:52.00,Default,,0,0,0,pause-menu-label,Pause
+Dialogue: 0,0:07:52.00,0:07:54.00,Default,,0,0,0,playbackbuttons-menu-label,Afficher les boutons de lecture
+Dialogue: 0,0:07:54.00,0:07:56.00,Default,,0,0,0,autoplay-menu-label,Afficher le bouton de lecture automatique
+Dialogue: 0,0:07:56.00,0:07:58.00,Default,,0,0,0,autoplay-guipushbuttonlabel,Jouer quand tout est prêt
+Dialogue: 0,0:07:58.00,0:08:00.00,Default,,0,0,0,autoplay-minimum-label,Utilisateurs minimum:
+Dialogue: 0,0:08:00.00,0:08:02.00,Default,,0,0,0,sendmessage-label,Envoyer
+Dialogue: 0,0:08:02.00,0:08:04.00,Default,,0,0,0,ready-guipushbuttonlabel,Je suis prêt à regarder !
+Dialogue: 0,0:08:04.00,0:08:06.00,Default,,0,0,0,roomuser-heading-label,Salon / Utilisateur
+Dialogue: 0,0:08:06.00,0:08:08.00,Default,,0,0,0,size-heading-label,Taille
+Dialogue: 0,0:08:08.00,0:08:10.00,Default,,0,0,0,duration-heading-label,Durée
+Dialogue: 0,0:08:10.00,0:08:12.00,Default,,0,0,0,filename-heading-label,Nom de fichier
+Dialogue: 0,0:08:12.00,0:08:14.00,Default,,0,0,0,notifications-heading-label,Notifications
+Dialogue: 0,0:08:14.00,0:08:16.00,Default,,0,0,0,userlist-heading-label,Liste de qui joue quoi
+Dialogue: 0,0:08:16.00,0:08:18.00,Default,,0,0,0,browseformedia-label,Parcourir les fichiers multimédias
+Dialogue: 0,0:08:18.00,0:08:20.00,Default,,0,0,0,file-menu-label,&Fichier
+Dialogue: 0,0:08:20.00,0:08:22.00,Default,,0,0,0,openmedia-menu-label,&Ouvrir le fichier multimédia
+Dialogue: 0,0:08:22.00,0:08:24.00,Default,,0,0,0,openstreamurl-menu-label,Ouvrir l'URL du &flux multimédia
+Dialogue: 0,0:08:24.00,0:08:26.00,Default,,0,0,0,setmediadirectories-menu-label,Définir les &répertoires multimédias
+Dialogue: 0,0:08:26.00,0:08:28.00,Default,,0,0,0,loadplaylistfromfile-menu-label,&Charger la liste de lecture à partir du fichier
+Dialogue: 0,0:08:28.00,0:08:30.00,Default,,0,0,0,saveplaylisttofile-menu-label,&Enregistrer la liste de lecture dans un fichier
+Dialogue: 0,0:08:30.00,0:08:32.00,Default,,0,0,0,exit-menu-label,Sortir
+Dialogue: 0,0:08:32.00,0:08:34.00,Default,,0,0,0,advanced-menu-label,&Avancée
+Dialogue: 0,0:08:34.00,0:08:36.00,Default,,0,0,0,window-menu-label,&Fenêtre
+Dialogue: 0,0:08:36.00,0:08:38.00,Default,,0,0,0,setoffset-menu-label,Définir &décalage
+Dialogue: 0,0:08:38.00,0:08:40.00,Default,,0,0,0,createcontrolledroom-menu-label,&Créer une salon à gérer
+Dialogue: 0,0:08:40.00,0:08:42.00,Default,,0,0,0,identifyascontroller-menu-label,&Identifier en tant qu'opérateur de salon
+Dialogue: 0,0:08:42.00,0:08:44.00,Default,,0,0,0,settrusteddomains-menu-label,Définir des &domaines de confiance
+Dialogue: 0,0:08:44.00,0:08:46.00,Default,,0,0,0,addtrusteddomain-menu-label,Ajouter {} comme domaine de confiance
+Dialogue: 0,0:08:46.00,0:08:48.00,Default,,0,0,0,edit-menu-label,&Éditer
+Dialogue: 0,0:08:48.00,0:08:50.00,Default,,0,0,0,cut-menu-label,Couper
+Dialogue: 0,0:08:50.00,0:08:52.00,Default,,0,0,0,copy-menu-label,&Copier
+Dialogue: 0,0:08:52.00,0:08:54.00,Default,,0,0,0,paste-menu-label,&Coller
+Dialogue: 0,0:08:54.00,0:08:56.00,Default,,0,0,0,selectall-menu-label,&Tout sélectionner
+Dialogue: 0,0:08:56.00,0:08:58.00,Default,,0,0,0,playback-menu-label,&Relecture
+Dialogue: 0,0:08:58.00,0:09:00.00,Default,,0,0,0,help-menu-label,&Aide
+Dialogue: 0,0:09:00.00,0:09:02.00,Default,,0,0,0,userguide-menu-label,Ouvrir le &guide de l'utilisateur
+Dialogue: 0,0:09:02.00,0:09:04.00,Default,,0,0,0,update-menu-label,Rechercher et mettre à jour
+Dialogue: 0,0:09:04.00,0:09:06.00,Default,,0,0,0,startTLS-initiated,Tentative de connexion sécurisée
+Dialogue: 0,0:09:06.00,0:09:08.00,Default,,0,0,0,startTLS-secure-connection-ok,Connexion sécurisée établie ({})
+Dialogue: 0,0:09:08.00,0:09:10.00,Default,,0,0,0,startTLS-server-certificate-invalid,Échec de la Connexion Sécurisée. Le serveur utilise un certificat de sécurité non valide. Cette communication pourrait être interceptée par un tiers. Pour plus de détails et de dépannage, voir ici .
+Dialogue: 0,0:09:10.00,0:09:12.00,Default,,0,0,0,startTLS-server-certificate-invalid-DNS-ID,Syncplay ne fait pas confiance à ce serveur car il utilise un certificat qui n'est pas valide pour son nom d'hôte.
+Dialogue: 0,0:09:12.00,0:09:14.00,Default,,0,0,0,startTLS-not-supported-client,Ce client ne prend pas en charge TLS
+Dialogue: 0,0:09:14.00,0:09:16.00,Default,,0,0,0,startTLS-not-supported-server,Ce serveur ne prend pas en charge TLS
+Dialogue: 0,0:09:16.00,0:09:18.00,Default,,0,0,0,tls-information-title,Détails du certificat
+Dialogue: 0,0:09:18.00,0:09:20.00,Default,,0,0,0,tls-dialog-status-label,Syncplay utilise une connexion cryptée à {}.
+Dialogue: 0,0:09:20.00,0:09:22.00,Default,,0,0,0,tls-dialog-desc-label,Le cryptage avec un certificat numérique préserve la confidentialité des informations lorsqu'elles sont envoyées vers ou depuis le serveur {}.
+Dialogue: 0,0:09:22.00,0:09:24.00,Default,,0,0,0,tls-dialog-connection-label,Informations chiffrées à l'aide de Transport Layer Security (TLS), version {} avec la suite de chiffrement: {}.
+Dialogue: 0,0:09:24.00,0:09:26.00,Default,,0,0,0,tls-dialog-certificate-label,Certificat délivré par {} valable jusqu'au {}.
+Dialogue: 0,0:09:26.00,0:09:28.00,Default,,0,0,0,about-menu-label,&À propos de la lecture synchronisée
+Dialogue: 0,0:09:28.00,0:09:30.00,Default,,0,0,0,about-dialog-title,À propos de Syncplay
+Dialogue: 0,0:09:30.00,0:09:32.00,Default,,0,0,0,about-dialog-release,Version {} release {}
+Dialogue: 0,0:09:32.00,0:09:34.00,Default,,0,0,0,about-dialog-license-text,Sous licence Apache, version 2.0
+Dialogue: 0,0:09:34.00,0:09:36.00,Default,,0,0,0,about-dialog-license-button,Licence
+Dialogue: 0,0:09:36.00,0:09:38.00,Default,,0,0,0,about-dialog-dependencies,Dépendances
+Dialogue: 0,0:09:38.00,0:09:40.00,Default,,0,0,0,setoffset-msgbox-label,Définir le décalage
+Dialogue: 0,0:09:40.00,0:09:42.00,Default,,0,0,0,offsetinfo-msgbox-label,Offset (voir https://syncplay.pl/guide/ pour les instructions d'utilisation):
+Dialogue: 0,0:09:42.00,0:09:44.00,Default,,0,0,0,promptforstreamurl-msgbox-label,Ouvrir l'URL du flux multimédia
+Dialogue: 0,0:09:44.00,0:09:46.00,Default,,0,0,0,promptforstreamurlinfo-msgbox-label,URL de diffusion
+Dialogue: 0,0:09:46.00,0:09:48.00,Default,,0,0,0,addfolder-label,Ajouter le dossier
+Dialogue: 0,0:09:48.00,0:09:50.00,Default,,0,0,0,adduris-msgbox-label,Ajouter des URL à la liste de lecture (une par ligne)
+Dialogue: 0,0:09:50.00,0:09:52.00,Default,,0,0,0,editplaylist-msgbox-label,Définir la liste de lecture (une par ligne)
+Dialogue: 0,0:09:52.00,0:09:54.00,Default,,0,0,0,trusteddomains-msgbox-label,Domaines vers lesquels vous pouvez basculer automatiquement (un par ligne)
+Dialogue: 0,0:09:54.00,0:09:56.00,Default,,0,0,0,createcontrolledroom-msgbox-label,Créer un salon à gérer
+Dialogue: 0,0:09:56.00,0:09:58.00,Default,,0,0,0,controlledroominfo-msgbox-label,Saisissez le nom du salon à gérer
+Dialogue: 0,0:09:58.00,0:10:00.00,Default,,0,0,0,identifyascontroller-msgbox-label,S'identifier en tant qu'opérateur de salon
+Dialogue: 0,0:10:00.00,0:10:02.00,Default,,0,0,0,identifyinfo-msgbox-label,Entrez le mot de passe de l'opérateur pour ce salon
+Dialogue: 0,0:10:02.00,0:10:04.00,Default,,0,0,0,public-server-msgbox-label,Sélectionnez le serveur public pour cette session de visualisation
+Dialogue: 0,0:10:04.00,0:10:06.00,Default,,0,0,0,megabyte-suffix,Mo
+Dialogue: 0,0:10:06.00,0:10:08.00,Default,,0,0,0,host-tooltip,Nom d'hôte ou IP auquel se connecter, incluant éventuellement le port (par exemple syncplay.pl:8999). Uniquement synchronisé avec des personnes sur le même serveur/port.
+Dialogue: 0,0:10:08.00,0:10:10.00,Default,,0,0,0,name-tooltip,Surnom sous lequel vous serez connu. Pas d'inscription, donc peut facilement changer plus tard. Nom aléatoire généré si aucun n'est spécifié.
+Dialogue: 0,0:10:10.00,0:10:12.00,Default,,0,0,0,password-tooltip,Les mots de passe ne sont nécessaires que pour se connecter à des serveurs privés.
+Dialogue: 0,0:10:12.00,0:10:14.00,Default,,0,0,0,room-tooltip,Le salon à rejoindre lors de la connexion peut être presque n'importe quoi, mais vous ne serez synchronisé qu'avec des personnes dans le même salon.
+Dialogue: 0,0:10:14.00,0:10:16.00,Default,,0,0,0,edit-rooms-tooltip,Modifier la liste des salons.
+Dialogue: 0,0:10:16.00,0:10:18.00,Default,,0,0,0,executable-path-tooltip,Emplacement du lecteur multimédia pris en charge que vous avez choisi (mpv, mpv.net, VLC, MPC-HC/BE, mplayer2 ou IINA).
+Dialogue: 0,0:10:18.00,0:10:20.00,Default,,0,0,0,media-path-tooltip,Emplacement de la vidéo ou du flux à ouvrir. Nécessaire pour mplayer2.
+Dialogue: 0,0:10:20.00,0:10:22.00,Default,,0,0,0,player-arguments-tooltip,Arguments/commutateurs de ligne de commande supplémentaires à transmettre à ce lecteur multimédia.
+Dialogue: 0,0:10:22.00,0:10:24.00,Default,,0,0,0,mediasearcdirectories-arguments-tooltip,Répertoires dans lesquels Syncplay recherchera les fichiers multimédias, par exemple lorsque vous utilisez la fonctionalité cliquer pour basculer. Syncplay recherchera récursivement dans les sous-dossiers.
+Dialogue: 0,0:10:24.00,0:10:26.00,Default,,0,0,0,more-tooltip,Afficher les paramètres moins fréquemment utilisés.
+Dialogue: 0,0:10:26.00,0:10:28.00,Default,,0,0,0,filename-privacy-tooltip,Mode de confidentialité pour l'envoi du nom de fichier en cours de lecture au serveur.
+Dialogue: 0,0:10:28.00,0:10:30.00,Default,,0,0,0,filesize-privacy-tooltip,Mode de confidentialité pour l'envoi de la taille du fichier en cours de lecture au serveur.
+Dialogue: 0,0:10:30.00,0:10:32.00,Default,,0,0,0,privacy-sendraw-tooltip,Envoyez ces informations sans brouillage. Il s'agit de l'option par défaut avec la plupart des fonctionnalités.
+Dialogue: 0,0:10:32.00,0:10:34.00,Default,,0,0,0,privacy-sendhashed-tooltip,Envoyez une version hachée des informations, les rendant moins visibles pour les autres clients.
+Dialogue: 0,0:10:34.00,0:10:36.00,Default,,0,0,0,privacy-dontsend-tooltip,N'envoyez pas ces informations au serveur. Cela garantit une confidentialité maximale.
+Dialogue: 0,0:10:36.00,0:10:38.00,Default,,0,0,0,checkforupdatesautomatically-tooltip,Vérifiez régulièrement sur le site Web de Syncplay si une nouvelle version de Syncplay est disponible.
+Dialogue: 0,0:10:38.00,0:10:40.00,Default,,0,0,0,autosavejoinstolist-tooltip,Lorsque vous rejoignez un salon sur un serveur, mémorisez automatiquement le nom de la salle dans la liste des salons à rejoindre.
+Dialogue: 0,0:10:40.00,0:10:42.00,Default,,0,0,0,slowondesync-tooltip,Réduisez temporairement le taux de lecture si nécessaire pour vous synchroniser avec les autres téléspectateurs. Non pris en charge sur MPC-HC/BE.
+Dialogue: 0,0:10:42.00,0:10:44.00,Default,,0,0,0,dontslowdownwithme-tooltip,Cela signifie que les autres ne sont pas ralentis ou rembobinés si votre lecture est en retard. Utile pour les opérateurs de salon.
+Dialogue: 0,0:10:44.00,0:10:46.00,Default,,0,0,0,pauseonleave-tooltip,Mettez la lecture en pause si vous êtes déconnecté ou si quelqu'un quitte votre salon.
+Dialogue: 0,0:10:46.00,0:10:48.00,Default,,0,0,0,readyatstart-tooltip,Définissez-vous comme «prêt» au début (sinon, vous êtes défini comme «pas prêt» jusqu'à ce que vous changiez votre état de préparation)
+Dialogue: 0,0:10:48.00,0:10:50.00,Default,,0,0,0,forceguiprompt-tooltip,La boîte de dialogue de configuration ne s'affiche pas lors de l'ouverture d'un fichier avec Syncplay.
+Dialogue: 0,0:10:50.00,0:10:52.00,Default,,0,0,0,nostore-tooltip,Exécutez Syncplay avec la configuration donnée, mais ne stockez pas les modifications de manière permanente.
+Dialogue: 0,0:10:52.00,0:10:54.00,Default,,0,0,0,rewindondesync-tooltip,Revenez en arrière au besoin pour vous synchroniser. La désactivation de cette option peut entraîner des désynchronisations majeures!
+Dialogue: 0,0:10:54.00,0:10:56.00,Default,,0,0,0,fastforwardondesync-tooltip,Avancez en cas de désynchronisation avec l'opérateur de la salle (ou votre position factice si l'option «Ne jamais ralentir ou rembobiner les autres» est activée).
+Dialogue: 0,0:10:56.00,0:10:58.00,Default,,0,0,0,showosd-tooltip,Envoie des messages Syncplay à l'OSD du lecteur multimédia.
+Dialogue: 0,0:10:58.00,0:11:00.00,Default,,0,0,0,showosdwarnings-tooltip,Afficher des avertissements en cas de lecture d'un fichier différent, seul dans la pièce, utilisateurs non prêts, etc.
+Dialogue: 0,0:11:00.00,0:11:02.00,Default,,0,0,0,showsameroomosd-tooltip,Afficher les notifications OSD pour les événements liés à l'utilisateur du salon.
+Dialogue: 0,0:11:02.00,0:11:04.00,Default,,0,0,0,shownoncontrollerosd-tooltip,Afficher les notifications OSD pour les événements relatifs aux non-opérateurs qui se trouvent dans les salles gérées.
+Dialogue: 0,0:11:04.00,0:11:06.00,Default,,0,0,0,showdifferentroomosd-tooltip,Afficher les notifications OSD pour les événements liés à l'absence de l'utilisateur du salon.
+Dialogue: 0,0:11:06.00,0:11:08.00,Default,,0,0,0,showslowdownosd-tooltip,Afficher les notifications de ralentissement / de retour au décalage temps.
+Dialogue: 0,0:11:08.00,0:11:10.00,Default,,0,0,0,showdurationnotification-tooltip,Utile lorsqu'un segment dans un fichier en plusieurs parties est manquant, mais peut entraîner des faux positifs.
+Dialogue: 0,0:11:10.00,0:11:12.00,Default,,0,0,0,language-tooltip,Langue à utiliser par Syncplay.
+Dialogue: 0,0:11:12.00,0:11:14.00,Default,,0,0,0,unpause-always-tooltip,Si vous appuyez sur unpause, cela vous définit toujours comme prêt et non-pause, plutôt que de simplement vous définir comme prêt.
+Dialogue: 0,0:11:14.00,0:11:16.00,Default,,0,0,0,unpause-ifalreadyready-tooltip,Si vous appuyez sur unpause lorsque vous n'êtes pas prêt, cela vous mettra comme prêt - appuyez à nouveau sur unpause pour reprendre la pause.
+Dialogue: 0,0:11:16.00,0:11:18.00,Default,,0,0,0,unpause-ifothersready-tooltip,Si vous appuyez sur unpause lorsque vous n'êtes pas prêt, il ne reprendra la pause que si d'autres sont prêts.
+Dialogue: 0,0:11:18.00,0:11:20.00,Default,,0,0,0,unpause-ifminusersready-tooltip,Si vous appuyez sur annuler la pause lorsqu'il n'est pas prêt, il ne s'arrêtera que si d'autres personnes sont prêtes et que le seuil minimal d'utilisateurs est atteint.
+Dialogue: 0,0:11:20.00,0:11:22.00,Default,,0,0,0,trusteddomains-arguments-tooltip,Domaines vers lesquels Syncplay peut basculer automatiquement lorsque les listes de lecture partagées sont activées.
+Dialogue: 0,0:11:22.00,0:11:24.00,Default,,0,0,0,chatinputenabled-tooltip,Activer la saisie du chat dans mpv (appuyez sur Entrée pour discuter, Entrée pour envoyer, Échap pour annuler)
+Dialogue: 0,0:11:24.00,0:11:26.00,Default,,0,0,0,chatdirectinput-tooltip,Évitez d'avoir à appuyer sur «enter» pour passer en mode de saisie de discussion dans mpv. Appuyez sur TAB dans mpv pour désactiver temporairement cette fonctionnalité.
+Dialogue: 0,0:11:26.00,0:11:28.00,Default,,0,0,0,font-label-tooltip,Police utilisée lors de la saisie de messages de discussion dans mpv. Côté client uniquement, n'affecte donc pas ce que les autres voient.
+Dialogue: 0,0:11:28.00,0:11:30.00,Default,,0,0,0,set-input-font-tooltip,Famille de polices utilisée lors de la saisie de messages de discussion dans mpv. Côté client uniquement, n'affecte donc pas ce que les autres voient.
+Dialogue: 0,0:11:30.00,0:11:32.00,Default,,0,0,0,set-input-colour-tooltip,Couleur de police utilisée lors de la saisie de messages de discussion dans mpv. Côté client uniquement, n'affecte donc pas ce que les autres voient.
+Dialogue: 0,0:11:32.00,0:11:34.00,Default,,0,0,0,chatinputposition-tooltip,Emplacement dans mpv où le texte d'entrée de discussion apparaîtra lorsque vous appuyez sur Entrée et tapez.
+Dialogue: 0,0:11:34.00,0:11:36.00,Default,,0,0,0,chatinputposition-top-tooltip,Placez l'entrée de discussion en haut de la fenêtre mpv.
+Dialogue: 0,0:11:36.00,0:11:38.00,Default,,0,0,0,chatinputposition-middle-tooltip,Placez l'entrée de discussion au point mort de la fenêtre mpv.
+Dialogue: 0,0:11:38.00,0:11:40.00,Default,,0,0,0,chatinputposition-bottom-tooltip,Placez l'entrée de discussion en bas de la fenêtre mpv.
+Dialogue: 0,0:11:40.00,0:11:42.00,Default,,0,0,0,chatoutputenabled-tooltip,Afficher les messages de discussion dans l'OSD (si pris en charge par le lecteur multimédia).
+Dialogue: 0,0:11:42.00,0:11:44.00,Default,,0,0,0,font-output-label-tooltip,Police de sortie du chat.
+Dialogue: 0,0:11:44.00,0:11:46.00,Default,,0,0,0,set-output-font-tooltip,Police utilisée pour l'affichage des messages de discussion.
+Dialogue: 0,0:11:46.00,0:11:48.00,Default,,0,0,0,chatoutputmode-tooltip,Comment les messages de chat sont affichés.
+Dialogue: 0,0:11:48.00,0:11:50.00,Default,,0,0,0,chatoutputmode-chatroom-tooltip,Affichez les nouvelles lignes de discussion directement sous la ligne précédente.
+Dialogue: 0,0:11:50.00,0:11:52.00,Default,,0,0,0,chatoutputmode-scrolling-tooltip,Faites défiler le texte du chat de droite à gauche.
+Dialogue: 0,0:11:52.00,0:11:54.00,Default,,0,0,0,help-tooltip,Ouvre le guide de l'utilisateur de Syncplay.pl.
+Dialogue: 0,0:11:54.00,0:11:56.00,Default,,0,0,0,reset-tooltip,Réinitialisez tous les paramètres à la configuration par défaut.
+Dialogue: 0,0:11:56.00,0:11:58.00,Default,,0,0,0,update-server-list-tooltip,Connectez-vous à syncplay.pl pour mettre à jour la liste des serveurs publics.
+Dialogue: 0,0:11:58.00,0:12:00.00,Default,,0,0,0,sslconnection-tooltip,Connecté en toute sécurité au serveur. Cliquez pour obtenir les détails du certificat.
+Dialogue: 0,0:12:00.00,0:12:02.00,Default,,0,0,0,joinroom-tooltip,Quitter la salle actuelle et rejoindre le salon spécifié.
+Dialogue: 0,0:12:02.00,0:12:04.00,Default,,0,0,0,seektime-msgbox-label,Aller au temps spécifié (en secondes / min:sec). Utilisez +/- pour la recherche relative.
+Dialogue: 0,0:12:04.00,0:12:06.00,Default,,0,0,0,ready-tooltip,Indique si vous êtes prêt à regarder.
+Dialogue: 0,0:12:06.00,0:12:08.00,Default,,0,0,0,autoplay-tooltip,Lecture automatique lorsque tous les utilisateurs qui ont un indicateur de disponibilité sont prêts et que le seuil d'utilisateur minimum est atteint.
+Dialogue: 0,0:12:08.00,0:12:10.00,Default,,0,0,0,switch-to-file-tooltip,Double-cliquez pour passer à {}
+Dialogue: 0,0:12:10.00,0:12:12.00,Default,,0,0,0,sendmessage-tooltip,Envoyer un message au salon
+Dialogue: 0,0:12:12.00,0:12:14.00,Default,,0,0,0,differentsize-note,Différentes tailles!
+Dialogue: 0,0:12:14.00,0:12:16.00,Default,,0,0,0,differentsizeandduration-note,Taille et durée différentes !
+Dialogue: 0,0:12:16.00,0:12:18.00,Default,,0,0,0,differentduration-note,Durée différente !
+Dialogue: 0,0:12:18.00,0:12:20.00,Default,,0,0,0,nofile-note,(Aucun fichier en cours de lecture)
+Dialogue: 0,0:12:20.00,0:12:22.00,Default,,0,0,0,new-syncplay-available-motd-message,Vous utilisez Syncplay {} mais une version plus récente est disponible sur https://syncplay.pl
+Dialogue: 0,0:12:22.00,0:12:24.00,Default,,0,0,0,welcome-server-notification,Bienvenue sur le serveur Syncplay, ver.
+Dialogue: 0,0:12:24.00,0:12:26.00,Default,,0,0,0,client-connected-room-server-notification,({2}) connecté à la salle '{1}'
+Dialogue: 0,0:12:26.00,0:12:28.00,Default,,0,0,0,client-left-server-notification,a quitté le serveur
+Dialogue: 0,0:12:28.00,0:12:30.00,Default,,0,0,0,no-salt-notification,VEUILLEZ NOTER: Pour permettre aux mots de passe d'opérateur de salle générés par cette instance de serveur de fonctionner lorsque le serveur est redémarré, veuillez ajouter l'argument de ligne de commande suivant lors de l'exécution du serveur Syncplay à l'avenir: --salt {}
+Dialogue: 0,0:12:30.00,0:12:32.00,Default,,0,0,0,server-argument-description,Solution pour synchroniser la lecture de plusieurs instances de lecteur multimédia sur le réseau. Instance de serveur
+Dialogue: 0,0:12:32.00,0:12:34.00,Default,,0,0,0,server-argument-epilog,Si aucune option n'est fournie, les valeurs _config seront utilisées
+Dialogue: 0,0:12:34.00,0:12:36.00,Default,,0,0,0,server-port-argument,port TCP du serveur
+Dialogue: 0,0:12:36.00,0:12:38.00,Default,,0,0,0,server-password-argument,Mot de passe du serveur
+Dialogue: 0,0:12:38.00,0:12:40.00,Default,,0,0,0,server-isolate-room-argument,faut-il isoler les salons ?
+Dialogue: 0,0:12:40.00,0:12:42.00,Default,,0,0,0,server-salt-argument,chaîne aléatoire utilisée pour générer les mots de passe des salons gérés
+Dialogue: 0,0:12:42.00,0:12:44.00,Default,,0,0,0,server-disable-ready-argument,désactiver la fonction de préparation
+Dialogue: 0,0:12:44.00,0:12:46.00,Default,,0,0,0,server-motd-argument,chemin vers le fichier à partir duquel motd sera récupéré
+Dialogue: 0,0:12:46.00,0:12:48.00,Default,,0,0,0,server-chat-argument,Le chat doit-il être désactivé?
+Dialogue: 0,0:12:48.00,0:12:50.00,Default,,0,0,0,server-chat-maxchars-argument,Nombre maximum de caractères dans un message de discussion (la valeur par défaut est {})
+Dialogue: 0,0:12:50.00,0:12:52.00,Default,,0,0,0,server-maxusernamelength-argument,Nombre maximum de caractères dans un nom d'utilisateur (la valeur par défaut est {})
+Dialogue: 0,0:12:52.00,0:12:54.00,Default,,0,0,0,server-stats-db-file-argument,Activer les statistiques du serveur à l'aide du fichier db SQLite fourni
+Dialogue: 0,0:12:54.00,0:12:56.00,Default,,0,0,0,server-startTLS-argument,Activer les connexions TLS à l'aide des fichiers de certificat dans le chemin fourni
+Dialogue: 0,0:12:56.00,0:12:58.00,Default,,0,0,0,server-messed-up-motd-unescaped-placeholders,Le message du jour a des espaces réservés non échappés. Tous les signes $ doivent être doublés ($$).
+Dialogue: 0,0:12:58.00,0:13:00.00,Default,,0,0,0,server-messed-up-motd-too-long,Le message du jour est trop long: {}caractères maximum, {} donnés.
+Dialogue: 0,0:13:00.00,0:13:02.00,Default,,0,0,0,unknown-command-server-error,Commande inconnue {}
+Dialogue: 0,0:13:02.00,0:13:04.00,Default,,0,0,0,not-json-server-error,Pas une chaîne encodée json {}
+Dialogue: 0,0:13:04.00,0:13:06.00,Default,,0,0,0,line-decode-server-error,Pas une chaîne utf-8
+Dialogue: 0,0:13:06.00,0:13:08.00,Default,,0,0,0,not-known-server-error,Vous devez être connu du serveur avant d'envoyer cette commande
+Dialogue: 0,0:13:08.00,0:13:10.00,Default,,0,0,0,client-drop-server-error,Client drop: {} -- {}
+Dialogue: 0,0:13:10.00,0:13:12.00,Default,,0,0,0,password-required-server-error,Mot de passe requis
+Dialogue: 0,0:13:12.00,0:13:14.00,Default,,0,0,0,wrong-password-server-error,Mauvais mot de passe fourni
+Dialogue: 0,0:13:14.00,0:13:16.00,Default,,0,0,0,hello-server-error,Pas assez d'arguments pour Hello
+Dialogue: 0,0:13:16.00,0:13:18.00,Default,,0,0,0,playlist-selection-changed-notification,{} a modifié la sélection de la liste de lecture
+Dialogue: 0,0:13:18.00,0:13:20.00,Default,,0,0,0,playlist-contents-changed-notification,{} a mis à jour la liste de lecture
+Dialogue: 0,0:13:20.00,0:13:22.00,Default,,0,0,0,cannot-find-file-for-playlist-switch-error,Impossible de trouver le fichier {} dans les répertoires multimédias pour le changement de liste de lecture!
+Dialogue: 0,0:13:22.00,0:13:24.00,Default,,0,0,0,cannot-add-duplicate-error,Impossible d'ajouter la deuxième entrée pour '{}' à la liste de lecture car aucun doublon n'est autorisé.
+Dialogue: 0,0:13:24.00,0:13:26.00,Default,,0,0,0,cannot-add-unsafe-path-error,Impossible de charger automatiquement {}, car il ne se trouve pas sur un domaine approuvé. Vous pouvez basculer manuellement vers l'URL en double-cliquant dessus dans la liste de lecture et ajouter des domaines de confiance via Fichier->Avancé->Définir les domaines de confiance. Si vous faites un clic droit sur une URL, vous pouvez ajouter son domaine en tant que domaine de confiance via le menu contextuel.
+Dialogue: 0,0:13:26.00,0:13:28.00,Default,,0,0,0,sharedplaylistenabled-label,Activer les listes de lecture partagées
+Dialogue: 0,0:13:28.00,0:13:30.00,Default,,0,0,0,removefromplaylist-menu-label,Supprimer de la liste de lecture
+Dialogue: 0,0:13:30.00,0:13:32.00,Default,,0,0,0,shuffleremainingplaylist-menu-label,Mélanger la liste de lecture restante
+Dialogue: 0,0:13:32.00,0:13:34.00,Default,,0,0,0,shuffleentireplaylist-menu-label,Mélanger toute la liste de lecture
+Dialogue: 0,0:13:34.00,0:13:36.00,Default,,0,0,0,undoplaylist-menu-label,Annuler la dernière modification de la liste de lecture
+Dialogue: 0,0:13:36.00,0:13:38.00,Default,,0,0,0,addfilestoplaylist-menu-label,Ajouter des fichiers au bas de la liste de lecture
+Dialogue: 0,0:13:38.00,0:13:40.00,Default,,0,0,0,addurlstoplaylist-menu-label,Ajouter des URL au bas de la liste de lecture
+Dialogue: 0,0:13:40.00,0:13:42.00,Default,,0,0,0,editplaylist-menu-label,Modifier la liste de lecture
+Dialogue: 0,0:13:42.00,0:13:44.00,Default,,0,0,0,open-containing-folder,Ouvrir le dossier contenant ce fichier
+Dialogue: 0,0:13:44.00,0:13:46.00,Default,,0,0,0,addyourfiletoplaylist-menu-label,Ajoutez votre fichier à la liste de lecture
+Dialogue: 0,0:13:46.00,0:13:48.00,Default,,0,0,0,addotherusersfiletoplaylist-menu-label,Ajouter le fichier de {} à la liste de lecture
+Dialogue: 0,0:13:48.00,0:13:50.00,Default,,0,0,0,addyourstreamstoplaylist-menu-label,Ajoutez votre flux à la liste de lecture
+Dialogue: 0,0:13:50.00,0:13:52.00,Default,,0,0,0,addotherusersstreamstoplaylist-menu-label,Ajouter {}' stream à la playlist
+Dialogue: 0,0:13:52.00,0:13:54.00,Default,,0,0,0,openusersstream-menu-label,Ouvrir le flux de {}
+Dialogue: 0,0:13:54.00,0:13:56.00,Default,,0,0,0,openusersfile-menu-label,Ouvrir le fichier de {}
+Dialogue: 0,0:13:56.00,0:13:58.00,Default,,0,0,0,playlist-instruction-item-message,Faites glisser le fichier ici pour l'ajouter à la liste de lecture partagée.
+Dialogue: 0,0:13:58.00,0:14:00.00,Default,,0,0,0,sharedplaylistenabled-tooltip,Les opérateurs de salle peuvent ajouter des fichiers à une liste de lecture synchronisée pour permettre à tout le monde de regarder facilement la même chose. Configurez les répertoires multimédias sous «Divers».
+Dialogue: 0,0:14:00.00,0:14:02.00,Default,,0,0,0,playlist-empty-error,La liste de lecture est actuellement vide.
+Dialogue: 0,0:14:02.00,0:14:04.00,Default,,0,0,0,playlist-invalid-index-error,Index de liste de lecture non valide
diff --git a/syncplay/messages_fr.py b/syncplay/messages_fr.py
new file mode 100644
index 0000000..121f675
--- /dev/null
+++ b/syncplay/messages_fr.py
@@ -0,0 +1,530 @@
+# coding:utf8
+
+"""French dictionary"""
+
+# This file was mainly auto generated from ass2messages.py applied on messages_fr.ass to get these messages
+# its format has been harmonized, values are always stored in doublequotes strings,
+# if double quoted string in the value then they should be esacaped like this \". There is
+# thus no reason to have single quoted strings. Tabs \t and newlines \n need also to be escaped.
+# whith ass2messages.py which handles these issues, this is no more a nightmare to handle.
+# I fixed partially messages_en.py serving as template. an entry should be added in messages.py:
+# "fr": messages_fr.fr, . Produced by sosie - sos-productions.com
+
+fr = {
+ "LANGUAGE": "Français",
+
+ # Client notifications
+ "config-cleared-notification": "Paramètres effacés. Les modifications seront enregistrées lorsque vous enregistrez une configuration valide.",
+
+ "relative-config-notification": "Fichiers de configuration relatifs chargés: {}",
+
+ "connection-attempt-notification": "Tentative de connexion à {}:{}", # Port, IP
+ "reconnection-attempt-notification": "Connexion avec le serveur perdue, tentative de reconnexion",
+ "disconnection-notification": "Déconnecté du serveur",
+ "connection-failed-notification": "Échec de la connexion avec le serveur",
+ "connected-successful-notification": "Connexion réussie au serveur",
+ "retrying-notification": "%s, nouvelle tentative dans %d secondes...", # Seconds
+ "reachout-successful-notification": "Vous avez atteint {} ({})",
+
+ "rewind-notification": "Retour en arrière en raison du décalage de temps avec {}", # User
+ "fastforward-notification": "Avance rapide en raison du décalage de temps avec {}", # User
+ "slowdown-notification": "Ralentissement dû au décalage de temps avec {}", # User
+ "revert-notification": "Retour à la vitesse normale",
+
+ "pause-notification": "{} en pause", # User
+ "unpause-notification": "{} non suspendu", # User
+ "seek-notification": "{} est passé de {} à {}", # User, from time, to time
+
+ "current-offset-notification": "Décalage actuel: {}secondes", # Offset
+
+ "media-directory-list-updated-notification": "Les répertoires multimédias Syncplay ont été mis à jour.",
+
+ "room-join-notification": "{} a rejoint la salle: '{}'", # User
+ "left-notification": "{} est parti", # User
+ "left-paused-notification": "{} restants, {} en pause", # User who left, User who paused
+ "playing-notification": "{} est en train de jouer '{}' ({})", # User, file, duration
+ "playing-notification/room-addendum": "dans le salon: '{}'", # Room
+
+ "not-all-ready": "Pas prêt: {}", # Usernames
+ "all-users-ready": "Tout le monde est prêt ({} utilisateurs)", # Number of ready users
+ "ready-to-unpause-notification": "Vous êtes maintenant défini comme prêt - réactivez la pause pour réactiver",
+ "set-as-ready-notification": "Vous êtes maintenant défini comme prêt",
+ "set-as-not-ready-notification": "Vous êtes maintenant défini comme non prêt",
+ "autoplaying-notification": "Lecture automatique dans {}...", # Number of seconds until playback will start
+
+ "identifying-as-controller-notification": "Identification en tant qu'opérateur de salle avec le mot de passe '{}'...",
+ "failed-to-identify-as-controller-notification": "{} n'a pas réussi à s'identifier en tant qu'opérateur de salle.",
+ "authenticated-as-controller-notification": "{} authentifié en tant qu'opérateur de salle",
+ "created-controlled-room-notification": "Salle gérée créée «{}» avec le mot de passe «{}». Veuillez conserver ces informations pour référence future !\n\nDans les salons gérés, tout le monde est synchronisé avec le ou les opérateurs de salon qui sont les seuls à pouvoir mettre en pause, reprendre, se déplacer dans la lecture et modifier la liste de lecture.\n\nVous devez demander aux spectateurs réguliers de rejoindre le salon '{}' mais les opérateurs de salon peuvent rejoindre le salon '{}' pour s'authentifier automatiquement.", # RoomName, operatorPassword, roomName, roomName:operatorPassword
+
+ "file-different-notification": "Le fichier que vous lisez semble être différent de celui de {}", # User
+ "file-differences-notification": "Votre fichier diffère de la (des) manière(s) suivante(s): {}", # Differences
+ "room-file-differences": "Différences de fichiers: {}", # File differences (filename, size, and/or duration)
+ "file-difference-filename": "Nom",
+ "file-difference-filesize": "Taille",
+ "file-difference-duration": "durée",
+ "alone-in-the-room": "Vous êtes seul dans le salon",
+
+ "different-filesize-notification": "(leur taille de fichier est différente de la vôtre!)",
+ "userlist-playing-notification": "{} est en train de jouer:", # Username
+ "file-played-by-notification": "Fichier: {} est lu par:", # File
+ "no-file-played-notification": "{} ne lit pas de fichier", # Username
+ "notplaying-notification": "Les personnes qui ne lisent aucun fichier:",
+ "userlist-room-notification": "Dans la chambre '{}':", # Room
+ "userlist-file-notification": "Fichier",
+ "controller-userlist-userflag": "Opérateur",
+ "ready-userlist-userflag": "Prêt",
+
+ "update-check-failed-notification": "Impossible de vérifier automatiquement si Syncplay {} est à jour. Vous voulez visiter https://syncplay.pl/ pour vérifier manuellement les mises à jour?", # Syncplay version
+ "syncplay-uptodate-notification": "Syncplay est à jour",
+ "syncplay-updateavailable-notification": "Une nouvelle version de Syncplay est disponible. Voulez-vous visiter la page de publication?",
+
+ "mplayer-file-required-notification": "Syncplay à l'aide de mplayer nécessite que vous fournissiez un fichier au démarrage",
+ "mplayer-file-required-notification/example": "Exemple d'utilisation: syncplay [options] [url|chemin/]nom de fichier",
+ "mplayer2-required": "Syncplay est incompatible avec MPlayer 1.x, veuillez utiliser mplayer2 ou mpv",
+
+ "unrecognized-command-notification": "commande non reconnue",
+ "commandlist-notification": "Commandes disponibles:",
+ "commandlist-notification/room": "\tr [nom] - changer de chambre",
+ "commandlist-notification/list": "\tl - afficher la liste des utilisateurs",
+ "commandlist-notification/undo": "\tu - annuler la dernière recherche",
+ "commandlist-notification/pause": "\tp - basculer sur pause",
+ "commandlist-notification/seek": "\t[s][+-]temps - recherche la valeur de temps donnée, si + ou - n'est pas spécifié c'est le temps absolu en secondes ou min:sec",
+ "commandlist-notification/help": "\th - cette aide",
+ "commandlist-notification/toggle": "\tt - bascule si vous êtes prêt à regarder ou non",
+ "commandlist-notification/create": "\tc [nom] - crée une salle gérée en utilisant le nom de la salle actuelle",
+ "commandlist-notification/auth": "\tun [mot de passe] - s'authentifier en tant qu'opérateur de salle avec le mot de passe opérateur",
+ "commandlist-notification/chat": "\tch [message] - envoyer un message de chat dans une pièce",
+ "commandList-notification/queue": "\tqa [fichier/url] - ajoute un fichier ou une URL au bas de la liste de lecture",
+ "commandList-notification/playlist": "\tql - afficher la liste de lecture actuelle",
+ "commandList-notification/select": "\tqs [index] - sélectionnez l'entrée donnée dans la liste de lecture",
+ "commandList-notification/delete": "\tqd [index] - supprime l'entrée donnée de la liste de lecture",
+ "syncplay-version-notification": "Version de Syncplay: {}", # syncplay.version
+ "more-info-notification": "Plus d'informations disponibles sur: {}", # projectURL
+
+ "gui-data-cleared-notification": "Syncplay a effacé les données d'état de chemin et de fenêtre utilisées par l'interface graphique.",
+ "language-changed-msgbox-label": "La langue sera modifiée lorsque vous exécuterez Syncplay.",
+ "promptforupdate-label": "Est-ce que Syncplay peut vérifier automatiquement les mises à jour de temps en temps?",
+
+ "media-player-latency-warning": "Avertissement: Le lecteur multimédia a mis {}secondes à répondre. Si vous rencontrez des problèmes de synchronisation, fermez les applications pour libérer des ressources système, et si cela ne fonctionne pas, essayez un autre lecteur multimédia.", # Seconds to respond
+ "mpv-unresponsive-error": "mpv n'a pas répondu pendant {} secondes et semble donc avoir mal fonctionné. Veuillez redémarrer Syncplay.", # Seconds to respond
+
+ # Client prompts
+ "enter-to-exit-prompt": "Appuyez sur entrée pour quitter",
+
+ # Client errors
+ "missing-arguments-error": "Certains arguments nécessaires sont manquants, reportez-vous à --help",
+ "server-timeout-error": "La connexion avec le serveur a expiré",
+ "mpc-slave-error": "Impossible de démarrer MPC en mode esclave!",
+ "mpc-version-insufficient-error": "La version MPC n'est pas suffisante, veuillez utiliser `mpc-hc` >= `{}`",
+ "mpc-be-version-insufficient-error": "La version MPC n'est pas suffisante, veuillez utiliser `mpc-be` >= `{}`",
+ "mpv-version-error": "Syncplay n'est pas compatible avec cette version de mpv. Veuillez utiliser une version différente de mpv (par exemple Git HEAD).",
+ "mpv-failed-advice": "La raison pour laquelle mpv ne peut pas démarrer peut être due à l'utilisation d'arguments de ligne de commande non pris en charge ou à une version non prise en charge de mpv.",
+ "player-file-open-error": "Le lecteur n'a pas réussi à ouvrir le fichier",
+ "player-path-error": "Le chemin du lecteur n'est pas défini correctement. Les lecteurs pris en charge sont : mpv, mpv.net, VLC, MPC-HC, MPC-BE, mplayer2 et IINA",
+ "hostname-empty-error": "Le nom d'hôte ne peut pas être vide",
+ "empty-error": "{} ne peut pas être vide", # Configuration
+ "media-player-error": "Media player error: \"{}\"", # Error line
+ "unable-import-gui-error": "Impossible d'importer les bibliothèques GUI. Si vous n'avez pas installé PySide, vous devrez l'installer pour que l'interface graphique fonctionne.",
+ "unable-import-twisted-error": "Impossible d'importer Twisted. Veuillez installer Twisted v16.4.0 ou une version ultérieure.",
+
+ "arguments-missing-error": "Certains arguments nécessaires sont manquants, reportez-vous à --help",
+
+ "unable-to-start-client-error": "Impossible de démarrer le client",
+
+ "player-path-config-error": "Le chemin du lecteur n'est pas défini correctement. Les lecteurs pris en charge sont : mpv, mpv.net, VLC, MPC-HC, MPC-BE, mplayer2 et IINA.",
+ "no-file-path-config-error": "Le fichier doit être sélectionné avant de démarrer votre lecteur",
+ "no-hostname-config-error": "Le nom d'hôte ne peut pas être vide",
+ "invalid-port-config-error": "Le port doit être valide",
+ "empty-value-config-error": "{} ne peut pas être vide", # Config option
+
+ "not-json-error": "Pas une chaîne encodée en json",
+ "hello-arguments-error": "Pas assez d'arguments pour Hello", # DO NOT TRANSLATE
+ "version-mismatch-error": "Non-concordance entre les versions du client et du serveur",
+ "vlc-failed-connection": "Échec de la connexion à VLC. Si vous n'avez pas installé syncplay.lua et utilisez la dernière version de VLC, veuillez vous référer à https://syncplay.pl/LUA/ pour obtenir des instructions. Syncplay et VLC 4 ne sont actuellement pas compatibles, utilisez donc VLC 3 ou une alternative telle que mpv.",
+ "vlc-failed-noscript": "VLC a signalé que le script d'interface syncplay.lua n'a pas été installé. Veuillez vous référer à https://syncplay.pl/LUA/ pour obtenir des instructions.",
+ "vlc-failed-versioncheck": "Cette version de VLC n'est pas prise en charge par Syncplay.",
+ "vlc-initial-warning": "VLC ne fournit pas toujours des informations de position précises à Syncplay, en particulier pour les fichiers .mp4 et .avi. Si vous rencontrez des problèmes de recherche erronée, essayez un autre lecteur multimédia tel que mpv (ou mpv.net pour les utilisateurs de Windows).",
+
+ "feature-sharedPlaylists": "listes de lecture partagées", # used for not-supported-by-server-error
+ "feature-chat": "chat", # used for not-supported-by-server-error
+ "feature-readiness": "préparation", # used for not-supported-by-server-error
+ "feature-managedRooms": "salons gérés", # used for not-supported-by-server-error
+
+ "not-supported-by-server-error": "La fonctionnalité {} n'est pas prise en charge par ce serveur.", # feature
+ "shared-playlists-not-supported-by-server-error": "La fonctionnalité de listes de lecture partagées peut ne pas être prise en charge par le serveur. Pour s'assurer qu'il fonctionne correctement, il faut un serveur exécutant Syncplay {}+, mais le serveur exécute Syncplay {}.", # minVersion, serverVersion
+ "shared-playlists-disabled-by-server-error": "La fonctionnalité de liste de lecture partagée a été désactivée dans la configuration du serveur. Pour utiliser cette fonctionnalité, vous devrez vous connecter à un autre serveur.",
+
+ "invalid-seek-value": "Valeur de recherche non valide",
+ "invalid-offset-value": "Valeur de décalage non valide",
+
+ "switch-file-not-found-error": "Impossible de passer au fichier ''. Syncplay recherche dans les répertoires multimédias spécifiés.", # File not found
+ "folder-search-timeout-error": "La recherche de médias dans les répertoires de médias a été abandonnée car la recherche dans '{}' a pris trop de temps. Cela se produira si vous sélectionnez un dossier avec trop de sous-dossiers dans votre liste de dossiers multimédias à parcourir. Pour que le basculement automatique des fichiers fonctionne à nouveau, veuillez sélectionner Fichier->Définir les répertoires multimédias dans la barre de menu et supprimez ce répertoire ou remplacez-le par un sous-dossier approprié. Si le dossier est correct, vous pouvez le réactiver en sélectionnant Fichier->Définir les répertoires multimédias et en appuyant sur «OK».", # Folder
+ "folder-search-first-file-timeout-error": "La recherche de média dans '{}' a été abandonnée car elle a pris trop de temps pour accéder au répertoire. Cela peut arriver s'il s'agit d'un lecteur réseau ou si vous configurez votre lecteur pour qu'il ralentisse après une période d'inactivité. Pour que le basculement automatique des fichiers fonctionne à nouveau, accédez à Fichier-> Définir les répertoires multimédias et supprimez le répertoire ou résolvez le problème (par exemple en modifiant les paramètres d'économie d'énergie).", # Folder
+ "added-file-not-in-media-directory-error": "Vous avez chargé un fichier dans '{}' qui n'est pas un répertoire média connu. Vous pouvez l'ajouter en tant que répertoire multimédia en sélectionnant Fichier->Définir les répertoires multimédias dans la barre de menus.", # Folder
+ "no-media-directories-error": "Aucun répertoire multimédia n'a été défini. Pour que les fonctionnalités de liste de lecture partagée et de changement de fichier fonctionnent correctement, sélectionnez Fichier-> Définir les répertoires multimédias et spécifiez où Syncplay doit rechercher les fichiers multimédias.",
+ "cannot-find-directory-error": "Impossible de trouver le répertoire multimédia '{}'. Pour mettre à jour votre liste de répertoires multimédias, veuillez sélectionner Fichier->Définir les répertoires multimédias dans la barre de menu et spécifiez où Syncplay doit chercher pour trouver les fichiers multimédias.",
+
+ "failed-to-load-server-list-error": "Échec du chargement de la liste des serveurs publics. Veuillez visiter https://www.syncplay.pl/ dans votre navigateur.",
+
+ # Client arguments
+ "argument-description": "Solution pour synchroniser la lecture de plusieurs instances de lecteur multimédia sur le réseau.",
+ "argument-epilog": "Si aucune option n'est fournie, les valeurs _config seront utilisées",
+ "nogui-argument": "masquer l'interface graphique",
+ "host-argument": "adresse du serveur",
+ "name-argument": "nom d'utilisateur souhaité",
+ "debug-argument": "Mode débogage",
+ "force-gui-prompt-argument": "faire apparaître l'invite de configuration",
+ "no-store-argument": "ne pas stocker de valeurs dans .syncplay",
+ "room-argument": "salon par défaut",
+ "password-argument": "Mot de passe du serveur",
+ "player-path-argument": "chemin d'accès à l'exécutable de votre lecteur",
+ "file-argument": "fichier à lire",
+ "args-argument": 'player options, if you need to pass options starting with - prepend them with single \'--\' argument',
+ "clear-gui-data-argument": "réinitialise les données GUI du chemin et de l'état de la fenêtre stockées en tant que QSettings",
+ "language-argument": "langue pour les messages Syncplay (de/en/ru/it/es/pt_BR/pt_PT/tr/fr)",
+
+ "version-argument": "imprime votre version",
+ "version-message": "Vous utilisez Syncplay version {} ({})",
+
+ "load-playlist-from-file-argument": "charge la liste de lecture à partir d'un fichier texte (une entrée par ligne)",
+
+
+ # Client labels
+ "config-window-title": "configuration Syncplay",
+
+ "connection-group-title": "Paramètres de connexion",
+ "host-label": "Adresse du serveur:",
+ "name-label": "Nom d'utilisateur (facultatif):",
+ "password-label": "Mot de passe du serveur (le cas échéant):",
+ "room-label": "Salon par défaut:",
+ "roomlist-msgbox-label": "Modifier la liste des salons (une par ligne)",
+
+ "media-setting-title": "Paramètres du lecteur multimédia",
+ "executable-path-label": "Chemin d'accès au lecteur multimédia:",
+ "media-path-label": "Chemin d'accès à la vidéo (facultatif):",
+ "player-arguments-label": "Arguments du joueur (le cas échéant):",
+ "browse-label": "Parcourir",
+ "update-server-list-label": "Mettre à jour la liste",
+
+ "more-title": "Afficher plus de paramètres",
+ "never-rewind-value": "Jamais",
+ "seconds-suffix": "secs",
+ "privacy-sendraw-option": "Envoyer brut",
+ "privacy-sendhashed-option": "Envoyer haché",
+ "privacy-dontsend-option": "Ne pas envoyer",
+ "filename-privacy-label": "Informations sur le nom de fichier:",
+ "filesize-privacy-label": "Informations sur la taille du fichier:",
+ "checkforupdatesautomatically-label": "Rechercher automatiquement les mises à jour de Syncplay",
+ "autosavejoinstolist-label": "Ajouter les salons que vous rejoignez à la liste des salons",
+ "slowondesync-label": "Ralentissement en cas de désynchronisation mineure (non pris en charge sur MPC-HC/BE)",
+ "rewindondesync-label": "Retour en arrière en cas de désynchronisation majeure (recommandé)",
+ "fastforwardondesync-label": "Avance rapide en cas de retard (recommandé)",
+ "dontslowdownwithme-label": "Ne jamais ralentir ou rembobiner les autres (expérimental)",
+ "pausing-title": "Pause",
+ "pauseonleave-label": "Pause lorsque l'utilisateur quitte (par exemple s'il est déconnecté)",
+ "readiness-title": "État de préparation initial",
+ "readyatstart-label": "Définissez-moi comme «prêt à regarder» par défaut",
+ "forceguiprompt-label": "Ne pas toujours afficher la fenêtre de configuration Syncplay", # (Inverted)
+ "showosd-label": "Activer les Messages OSD",
+
+ "showosdwarnings-label": "Inclure des avertissements (par exemple, lorsque les fichiers sont différents, les utilisateurs ne sont pas prêts)",
+ "showsameroomosd-label": "Inclure des événements dans votre salon",
+ "shownoncontrollerosd-label": "Inclure les événements des non-opérateurs dans les salons gérés",
+ "showdifferentroomosd-label": "Inclure des événements dans d'autres salons",
+ "showslowdownosd-label": "Inclure les notifications de ralentissement/annulation",
+ "language-label": "Langue:",
+ "automatic-language": "Défaut ({})", # Default language
+ "showdurationnotification-label": "Avertir des incohérences de durée de média",
+ "basics-label": "Réglages de base",
+ "readiness-label": "Jouer pause",
+ "misc-label": "Divers",
+ "core-behaviour-title": "Comportement du salon principal",
+ "syncplay-internals-title": "procédures internes",
+ "syncplay-mediasearchdirectories-title": "Répertoires pour rechercher des médias",
+ "syncplay-mediasearchdirectories-label": "Répertoires pour rechercher des médias (un chemin par ligne)",
+ "sync-label": "Synchroniser",
+ "sync-otherslagging-title": "Si d'autres sont à la traîne...",
+ "sync-youlaggging-title": "Si vous êtes à la traîne...",
+ "messages-label": "Messages",
+ "messages-osd-title": "Paramètres d'affichage à l'écran",
+ "messages-other-title": "Autres paramètres d'affichage",
+ "chat-label": "Chat",
+ "privacy-label": "Sécurité données", # Currently unused, but will be brought back if more space is needed in Misc tab
+ "privacy-title": "Paramètres de confidentialité",
+ "unpause-title": "Si vous appuyez sur play, définissez comme prêt et:",
+ "unpause-ifalreadyready-option": "Annuler la pause si déjà défini comme prêt",
+ "unpause-ifothersready-option": "Reprendre la pause si déjà prêt ou si d'autres personnes dans la pièce sont prêtes (par défaut)",
+ "unpause-ifminusersready-option": "Annuler la pause si déjà prêt ou si tous les autres sont prêts et utilisateurs minimum prêts",
+ "unpause-always": "Toujours reprendre",
+ "syncplay-trusteddomains-title": "Domaines de confiance (pour les services de streaming et le contenu hébergé)",
+
+ "chat-title": "Saisie du message de discussion",
+ "chatinputenabled-label": "Activer la saisie de discussion via mpv",
+ "chatdirectinput-label": "Autoriser la saisie de discussion instantanée (éviter d'avoir à appuyer sur la touche Entrée pour discuter)",
+ "chatinputfont-label": "Police de caractères pour la saisie sur le Chat ",
+ "chatfont-label": "Définir la fonte",
+ "chatcolour-label": "Définir la couleur",
+ "chatinputposition-label": "Position de la zone de saisie des messages dans mpv",
+ "chat-top-option": "Haut",
+ "chat-middle-option": "Milieu",
+ "chat-bottom-option": "Bas",
+ "chatoutputheader-label": "Sortie du message de discussion",
+ "chatoutputfont-label": "Police de sortie du chat",
+ "chatoutputenabled-label": "Activer la sortie du chat dans le lecteur multimédia (mpv uniquement pour l'instant)",
+ "chatoutputposition-label": "Mode de sortie",
+ "chat-chatroom-option": "Style de salon de discussion",
+ "chat-scrolling-option": "Style de défilement",
+
+ "mpv-key-tab-hint": "[TAB] pour basculer l'accès aux raccourcis des touches de la ligne alphabétique.",
+ "mpv-key-hint": "[ENTER] pour envoyer un message. [ESC] pour quitter le mode chat.",
+ "alphakey-mode-warning-first-line": "Vous pouvez temporairement utiliser les anciennes liaisons mpv avec les touches az.",
+ "alphakey-mode-warning-second-line": "Appuyez sur [TAB] pour revenir au mode de discussion Syncplay.",
+
+ "help-label": "Aider",
+ "reset-label": "Réinitialiser",
+ "run-label": "Exécuter Syncplay",
+ "storeandrun-label": "Stocker la configuration et exécuter Syncplay",
+
+ "contact-label": "Feel free to e-mail dev@syncplay.pl, create an issue to report a bug/problem via GitHub, start a discussion to make a suggestion or ask a question via GitHub, like us on Facebook, follow us on Twitter, or visit https://syncplay.pl/. Do not use Syncplay to send sensitive information.",
+
+ "joinroom-label": "Rejoindre la salle",
+ "joinroom-menu-label": "Rejoindre la salle {}",
+ "seektime-menu-label": "Chercher le temps",
+ "undoseek-menu-label": "Annuler la recherche",
+ "play-menu-label": "Jouer",
+ "pause-menu-label": "Pause",
+ "playbackbuttons-menu-label": "Afficher les boutons de lecture",
+ "autoplay-menu-label": "Afficher le bouton de lecture automatique",
+ "autoplay-guipushbuttonlabel": "Jouer quand tout est prêt",
+ "autoplay-minimum-label": "Utilisateurs minimum:",
+
+ "sendmessage-label": "Envoyer",
+
+ "ready-guipushbuttonlabel": "Je suis prêt à regarder !",
+
+ "roomuser-heading-label": "Salon / Utilisateur",
+ "size-heading-label": "Taille",
+ "duration-heading-label": "Durée",
+ "filename-heading-label": "Nom de fichier",
+ "notifications-heading-label": "Notifications",
+ "userlist-heading-label": "Liste de qui joue quoi",
+
+ "browseformedia-label": "Parcourir les fichiers multimédias",
+
+ "file-menu-label": "&Fichier", # & precedes shortcut key
+ "openmedia-menu-label": "&Ouvrir le fichier multimédia",
+ "openstreamurl-menu-label": "Ouvrir l'URL du &flux multimédia",
+ "setmediadirectories-menu-label": "Définir les &répertoires multimédias",
+ "loadplaylistfromfile-menu-label": "&Charger la liste de lecture à partir du fichier",
+ "saveplaylisttofile-menu-label": "&Enregistrer la liste de lecture dans un fichier",
+ "exit-menu-label": "Sortir",
+ "advanced-menu-label": "&Avancée",
+ "window-menu-label": "&Fenêtre",
+ "setoffset-menu-label": "Définir &décalage",
+ "createcontrolledroom-menu-label": "&Créer une salon à gérer",
+ "identifyascontroller-menu-label": "&Identifier en tant qu'opérateur de salon",
+ "settrusteddomains-menu-label": "Définir des &domaines de confiance",
+ "addtrusteddomain-menu-label": "Ajouter {} comme domaine de confiance", # Domain
+
+ "edit-menu-label": "&Éditer",
+ "cut-menu-label": "Couper",
+ "copy-menu-label": "&Copier",
+ "paste-menu-label": "&Coller",
+ "selectall-menu-label": "&Tout sélectionner",
+
+ "playback-menu-label": "&Relecture",
+
+ "help-menu-label": "&Aide",
+ "userguide-menu-label": "Ouvrir le &guide de l'utilisateur",
+ "update-menu-label": "Rechercher et mettre à jour",
+
+ "startTLS-initiated": "Tentative de connexion sécurisée",
+ "startTLS-secure-connection-ok": "Connexion sécurisée établie ({})",
+ "startTLS-server-certificate-invalid": "Échec de la Connexion Sécurisée. Le serveur utilise un certificat de sécurité non valide. Cette communication pourrait être interceptée par un tiers. Pour plus de détails et de dépannage, voir ici .",
+ "startTLS-server-certificate-invalid-DNS-ID": "Syncplay ne fait pas confiance à ce serveur car il utilise un certificat qui n'est pas valide pour son nom d'hôte.",
+ "startTLS-not-supported-client": "Ce client ne prend pas en charge TLS",
+ "startTLS-not-supported-server": "Ce serveur ne prend pas en charge TLS",
+
+ # TLS certificate dialog
+ "tls-information-title": "Détails du certificat",
+ "tls-dialog-status-label": "Syncplay utilise une connexion cryptée à {}.",
+ "tls-dialog-desc-label": "Le cryptage avec un certificat numérique préserve la confidentialité des informations lorsqu'elles sont envoyées vers ou depuis le serveur {}.",
+ "tls-dialog-connection-label": "Informations chiffrées à l'aide de Transport Layer Security (TLS), version {} avec la suite de chiffrement: {}.",
+ "tls-dialog-certificate-label": "Certificat délivré par {} valable jusqu'au {}.",
+
+ # About dialog
+ "about-menu-label": "&À propos de la lecture synchronisée",
+ "about-dialog-title": "À propos de Syncplay",
+ "about-dialog-release": "Version {} release {}",
+ "about-dialog-license-text": "Sous licence Apache, version 2.0",
+ "about-dialog-license-button": "Licence",
+ "about-dialog-dependencies": "Dépendances",
+
+ "setoffset-msgbox-label": "Définir le décalage",
+ "offsetinfo-msgbox-label": "Offset (voir https://syncplay.pl/guide/ pour les instructions d'utilisation):",
+
+ "promptforstreamurl-msgbox-label": "Ouvrir l'URL du flux multimédia",
+ "promptforstreamurlinfo-msgbox-label": "URL de diffusion",
+
+ "addfolder-label": "Ajouter le dossier",
+
+ "adduris-msgbox-label": "Ajouter des URL à la liste de lecture (une par ligne)",
+ "editplaylist-msgbox-label": "Définir la liste de lecture (une par ligne)",
+ "trusteddomains-msgbox-label": "Domaines vers lesquels vous pouvez basculer automatiquement (un par ligne)",
+
+ "createcontrolledroom-msgbox-label": "Créer un salon à gérer",
+ "controlledroominfo-msgbox-label": "Enter name of managed room\r\n(see https://syncplay.pl/guide/ for usage instructions):",
+
+ "identifyascontroller-msgbox-label": "S'identifier en tant qu'opérateur de salon",
+ "identifyinfo-msgbox-label": "Enter operator password for this room\r\n(see https://syncplay.pl/guide/ for usage instructions):",
+
+ "public-server-msgbox-label": "Sélectionnez le serveur public pour cette session de visualisation",
+
+ "megabyte-suffix": "Mo",
+
+ # Tooltips
+
+ "host-tooltip": "Nom d'hôte ou IP auquel se connecter, incluant éventuellement le port (par exemple syncplay.pl:8999). Uniquement synchronisé avec des personnes sur le même serveur/port.",
+ "name-tooltip": "Surnom sous lequel vous serez connu. Pas d'inscription, donc peut facilement changer plus tard. Nom aléatoire généré si aucun n'est spécifié.",
+ "password-tooltip": "Les mots de passe ne sont nécessaires que pour se connecter à des serveurs privés.",
+ "room-tooltip": "Le salon à rejoindre lors de la connexion peut être presque n'importe quoi, mais vous ne serez synchronisé qu'avec des personnes dans le même salon.",
+
+ "edit-rooms-tooltip": "Modifier la liste des salons.",
+
+ "executable-path-tooltip": "Emplacement du lecteur multimédia pris en charge que vous avez choisi (mpv, mpv.net, VLC, MPC-HC/BE, mplayer2 ou IINA).",
+ "media-path-tooltip": "Emplacement de la vidéo ou du flux à ouvrir. Nécessaire pour mplayer2.",
+ "player-arguments-tooltip": "Arguments/commutateurs de ligne de commande supplémentaires à transmettre à ce lecteur multimédia.",
+ "mediasearcdirectories-arguments-tooltip": "Répertoires dans lesquels Syncplay recherchera les fichiers multimédias, par exemple lorsque vous utilisez la fonctionalité cliquer pour basculer. Syncplay recherchera récursivement dans les sous-dossiers.",
+
+ "more-tooltip": "Afficher les paramètres moins fréquemment utilisés.",
+ "filename-privacy-tooltip": "Mode de confidentialité pour l'envoi du nom de fichier en cours de lecture au serveur.",
+ "filesize-privacy-tooltip": "Mode de confidentialité pour l'envoi de la taille du fichier en cours de lecture au serveur.",
+ "privacy-sendraw-tooltip": "Envoyez ces informations sans brouillage. Il s'agit de l'option par défaut avec la plupart des fonctionnalités.",
+ "privacy-sendhashed-tooltip": "Envoyez une version hachée des informations, les rendant moins visibles pour les autres clients.",
+ "privacy-dontsend-tooltip": "N'envoyez pas ces informations au serveur. Cela garantit une confidentialité maximale.",
+ "checkforupdatesautomatically-tooltip": "Vérifiez régulièrement sur le site Web de Syncplay si une nouvelle version de Syncplay est disponible.",
+ "autosavejoinstolist-tooltip": "Lorsque vous rejoignez un salon sur un serveur, mémorisez automatiquement le nom de la salle dans la liste des salons à rejoindre.",
+ "slowondesync-tooltip": "Réduisez temporairement le taux de lecture si nécessaire pour vous synchroniser avec les autres téléspectateurs. Non pris en charge sur MPC-HC/BE.",
+ "dontslowdownwithme-tooltip": "Cela signifie que les autres ne sont pas ralentis ou rembobinés si votre lecture est en retard. Utile pour les opérateurs de salon.",
+ "pauseonleave-tooltip": "Mettez la lecture en pause si vous êtes déconnecté ou si quelqu'un quitte votre salon.",
+ "readyatstart-tooltip": "Définissez-vous comme «prêt» au début (sinon, vous êtes défini comme «pas prêt» jusqu'à ce que vous changiez votre état de préparation)",
+ "forceguiprompt-tooltip": "La boîte de dialogue de configuration ne s'affiche pas lors de l'ouverture d'un fichier avec Syncplay.", # (Inverted)
+ "nostore-tooltip": "Exécutez Syncplay avec la configuration donnée, mais ne stockez pas les modifications de manière permanente.", # (Inverted)
+ "rewindondesync-tooltip": "Revenez en arrière au besoin pour vous synchroniser. La désactivation de cette option peut entraîner des désynchronisations majeures!",
+ "fastforwardondesync-tooltip": "Avancez en cas de désynchronisation avec l'opérateur de la salle (ou votre position factice si l'option «Ne jamais ralentir ou rembobiner les autres» est activée).",
+ "showosd-tooltip": "Envoie des messages Syncplay à l'OSD du lecteur multimédia.",
+ "showosdwarnings-tooltip": "Afficher des avertissements en cas de lecture d'un fichier différent, seul dans la pièce, utilisateurs non prêts, etc.",
+ "showsameroomosd-tooltip": "Afficher les notifications OSD pour les événements liés à l'utilisateur du salon.",
+ "shownoncontrollerosd-tooltip": "Afficher les notifications OSD pour les événements relatifs aux non-opérateurs qui se trouvent dans les salles gérées.",
+ "showdifferentroomosd-tooltip": "Afficher les notifications OSD pour les événements liés à l'absence de l'utilisateur du salon.",
+ "showslowdownosd-tooltip": "Afficher les notifications de ralentissement / de retour au décalage temps.",
+ "showdurationnotification-tooltip": "Utile lorsqu'un segment dans un fichier en plusieurs parties est manquant, mais peut entraîner des faux positifs.",
+ "language-tooltip": "Langue à utiliser par Syncplay.",
+ "unpause-always-tooltip": "Si vous appuyez sur unpause, cela vous définit toujours comme prêt et non-pause, plutôt que de simplement vous définir comme prêt.",
+ "unpause-ifalreadyready-tooltip": "Si vous appuyez sur unpause lorsque vous n'êtes pas prêt, cela vous mettra comme prêt - appuyez à nouveau sur unpause pour reprendre la pause.",
+ "unpause-ifothersready-tooltip": "Si vous appuyez sur unpause lorsque vous n'êtes pas prêt, il ne reprendra la pause que si d'autres sont prêts.",
+ "unpause-ifminusersready-tooltip": "Si vous appuyez sur annuler la pause lorsqu'il n'est pas prêt, il ne s'arrêtera que si d'autres personnes sont prêtes et que le seuil minimal d'utilisateurs est atteint.",
+ "trusteddomains-arguments-tooltip": "Domaines vers lesquels Syncplay peut basculer automatiquement lorsque les listes de lecture partagées sont activées.",
+
+ "chatinputenabled-tooltip": "Activer la saisie du chat dans mpv (appuyez sur Entrée pour discuter, Entrée pour envoyer, Échap pour annuler)",
+ "chatdirectinput-tooltip": "Évitez d'avoir à appuyer sur «enter» pour passer en mode de saisie de discussion dans mpv. Appuyez sur TAB dans mpv pour désactiver temporairement cette fonctionnalité.",
+ "font-label-tooltip": "Police utilisée lors de la saisie de messages de discussion dans mpv. Côté client uniquement, n'affecte donc pas ce que les autres voient.",
+ "set-input-font-tooltip": "Famille de polices utilisée lors de la saisie de messages de discussion dans mpv. Côté client uniquement, n'affecte donc pas ce que les autres voient.",
+ "set-input-colour-tooltip": "Couleur de police utilisée lors de la saisie de messages de discussion dans mpv. Côté client uniquement, n'affecte donc pas ce que les autres voient.",
+ "chatinputposition-tooltip": "Emplacement dans mpv où le texte d'entrée de discussion apparaîtra lorsque vous appuyez sur Entrée et tapez.",
+ "chatinputposition-top-tooltip": "Placez l'entrée de discussion en haut de la fenêtre mpv.",
+ "chatinputposition-middle-tooltip": "Placez l'entrée de discussion au point mort de la fenêtre mpv.",
+ "chatinputposition-bottom-tooltip": "Placez l'entrée de discussion en bas de la fenêtre mpv.",
+ "chatoutputenabled-tooltip": "Afficher les messages de discussion dans l'OSD (si pris en charge par le lecteur multimédia).",
+ "font-output-label-tooltip": "Police de sortie du chat.",
+ "set-output-font-tooltip": "Police utilisée pour l'affichage des messages de discussion.",
+ "chatoutputmode-tooltip": "Comment les messages de chat sont affichés.",
+ "chatoutputmode-chatroom-tooltip": "Affichez les nouvelles lignes de discussion directement sous la ligne précédente.",
+ "chatoutputmode-scrolling-tooltip": "Faites défiler le texte du chat de droite à gauche.",
+
+ "help-tooltip": "Ouvre le guide de l'utilisateur de Syncplay.pl.",
+ "reset-tooltip": "Réinitialisez tous les paramètres à la configuration par défaut.",
+ "update-server-list-tooltip": "Connectez-vous à syncplay.pl pour mettre à jour la liste des serveurs publics.",
+
+ "sslconnection-tooltip": "Connecté en toute sécurité au serveur. Cliquez pour obtenir les détails du certificat.",
+
+ "joinroom-tooltip": "Quitter la salle actuelle et rejoindre le salon spécifié.",
+ "seektime-msgbox-label": "Aller au temps spécifié (en secondes / min:sec). Utilisez +/- pour la recherche relative.",
+ "ready-tooltip": "Indique si vous êtes prêt à regarder.",
+ "autoplay-tooltip": "Lecture automatique lorsque tous les utilisateurs qui ont un indicateur de disponibilité sont prêts et que le seuil d'utilisateur minimum est atteint.",
+ "switch-to-file-tooltip": "Double-cliquez pour passer à {}", # Filename
+ "sendmessage-tooltip": "Envoyer un message au salon",
+
+ # In-userlist notes (GUI)
+ "differentsize-note": "Différentes tailles!",
+ "differentsizeandduration-note": "Taille et durée différentes !",
+ "differentduration-note": "Durée différente !",
+ "nofile-note": "(Aucun fichier en cours de lecture)",
+
+ # Server messages to client
+ "new-syncplay-available-motd-message": "Vous utilisez Syncplay {} mais une version plus récente est disponible sur https://syncplay.pl", # ClientVersion
+
+ # Server notifications
+ "welcome-server-notification": "Bienvenue sur le serveur Syncplay, ver.", # version
+ "client-connected-room-server-notification": "({2}) connecté à la salle '{1}'", # username, host, room
+ "client-left-server-notification": "a quitté le serveur", # name
+ "no-salt-notification": "VEUILLEZ NOTER: Pour permettre aux mots de passe d'opérateur de salle générés par cette instance de serveur de fonctionner lorsque le serveur est redémarré, veuillez ajouter l'argument de ligne de commande suivant lors de l'exécution du serveur Syncplay à l'avenir: --salt {}", # Salt
+
+
+ # Server arguments
+ "server-argument-description": "Solution pour synchroniser la lecture de plusieurs instances de lecteur multimédia sur le réseau. Instance de serveur",
+ "server-argument-epilog": "Si aucune option n'est fournie, les valeurs _config seront utilisées",
+ "server-port-argument": "port TCP du serveur",
+ "server-password-argument": "Mot de passe du serveur",
+ "server-isolate-room-argument": "faut-il isoler les salons ?",
+ "server-salt-argument": "chaîne aléatoire utilisée pour générer les mots de passe des salons gérés",
+ "server-disable-ready-argument": "désactiver la fonction de préparation",
+ "server-motd-argument": "chemin vers le fichier à partir duquel motd sera récupéré",
+ "server-chat-argument": "Le chat doit-il être désactivé?",
+ "server-chat-maxchars-argument": "Nombre maximum de caractères dans un message de discussion (la valeur par défaut est {})", # Default number of characters
+ "server-maxusernamelength-argument": "Nombre maximum de caractères dans un nom d'utilisateur (la valeur par défaut est {})",
+ "server-stats-db-file-argument": "Activer les statistiques du serveur à l'aide du fichier db SQLite fourni",
+ "server-startTLS-argument": "Activer les connexions TLS à l'aide des fichiers de certificat dans le chemin fourni",
+ "server-messed-up-motd-unescaped-placeholders": "Le message du jour a des espaces réservés non échappés. Tous les signes $ doivent être doublés ($$).",
+ "server-messed-up-motd-too-long": "Le message du jour est trop long: {}caractères maximum, {} donnés.",
+
+ # Server errors
+ "unknown-command-server-error": "Commande inconnue {}", # message
+ "not-json-server-error": "Pas une chaîne encodée json {}", # message
+ "line-decode-server-error": "Pas une chaîne utf-8",
+ "not-known-server-error": "Vous devez être connu du serveur avant d'envoyer cette commande",
+ "client-drop-server-error": "Client drop: {} -- {}", # host, error
+ "password-required-server-error": "Mot de passe requis",
+ "wrong-password-server-error": "Mauvais mot de passe fourni",
+ "hello-server-error": "Pas assez d'arguments pour Hello", # DO NOT TRANSLATE
+
+ # Playlists
+ "playlist-selection-changed-notification": "{} a modifié la sélection de la liste de lecture", # Username
+ "playlist-contents-changed-notification": "{} a mis à jour la liste de lecture", # Username
+ "cannot-find-file-for-playlist-switch-error": "Impossible de trouver le fichier {} dans les répertoires multimédias pour le changement de liste de lecture!", # Filename
+ "cannot-add-duplicate-error": "Impossible d'ajouter la deuxième entrée pour '{}' à la liste de lecture car aucun doublon n'est autorisé.", # Filename
+ "cannot-add-unsafe-path-error": "Impossible de charger automatiquement {}, car il ne se trouve pas sur un domaine approuvé. Vous pouvez basculer manuellement vers l'URL en double-cliquant dessus dans la liste de lecture et ajouter des domaines de confiance via Fichier->Avancé->Définir les domaines de confiance. Si vous faites un clic droit sur une URL, vous pouvez ajouter son domaine en tant que domaine de confiance via le menu contextuel.", # Filename
+ "sharedplaylistenabled-label": "Activer les listes de lecture partagées",
+ "removefromplaylist-menu-label": "Supprimer de la liste de lecture",
+ "shuffleremainingplaylist-menu-label": "Mélanger la liste de lecture restante",
+ "shuffleentireplaylist-menu-label": "Mélanger toute la liste de lecture",
+ "undoplaylist-menu-label": "Annuler la dernière modification de la liste de lecture",
+ "addfilestoplaylist-menu-label": "Ajouter des fichiers au bas de la liste de lecture",
+ "addurlstoplaylist-menu-label": "Ajouter des URL au bas de la liste de lecture",
+ "editplaylist-menu-label": "Modifier la liste de lecture",
+
+ "open-containing-folder": "Ouvrir le dossier contenant ce fichier",
+ "addyourfiletoplaylist-menu-label": "Ajoutez votre fichier à la liste de lecture",
+ "addotherusersfiletoplaylist-menu-label": "Ajouter le fichier de {} à la liste de lecture", # [Username]
+ "addyourstreamstoplaylist-menu-label": "Ajoutez votre flux à la liste de lecture",
+ "addotherusersstreamstoplaylist-menu-label": "Ajouter {}' stream à la playlist", # [Username]
+ "openusersstream-menu-label": "Ouvrir le flux de {}", # [username]'s
+ "openusersfile-menu-label": "Ouvrir le fichier de {}", # [username]'s
+
+ "playlist-instruction-item-message": "Faites glisser le fichier ici pour l'ajouter à la liste de lecture partagée.",
+ "sharedplaylistenabled-tooltip": "Les opérateurs de salle peuvent ajouter des fichiers à une liste de lecture synchronisée pour permettre à tout le monde de regarder facilement la même chose. Configurez les répertoires multimédias sous «Divers».",
+
+ "playlist-empty-error": "La liste de lecture est actuellement vide.",
+ "playlist-invalid-index-error": "Index de liste de lecture non valide",
+}
diff --git a/syncplay/pyonfx/Untitled.ass b/syncplay/pyonfx/Untitled.ass
new file mode 100644
index 0000000..29d526b
--- /dev/null
+++ b/syncplay/pyonfx/Untitled.ass
@@ -0,0 +1,18 @@
+[Script Info]
+; Script generated by Aegisub 3.2.2
+; http://www.aegisub.org/
+Title: Default Aegisub file
+ScriptType: v4.00+
+WrapStyle: 0
+ScaledBorderAndShadow: yes
+YCbCr Matrix: None
+
+[Aegisub Project Garbage]
+
+[V4+ Styles]
+Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
+Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1
+
+[Events]
+Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
+Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,
\ No newline at end of file
diff --git a/syncplay/pyonfx/__init__.py b/syncplay/pyonfx/__init__.py
new file mode 100644
index 0000000..32c8e2f
--- /dev/null
+++ b/syncplay/pyonfx/__init__.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+
+from .font_utility import Font
+from .ass_core import Ass, Meta, Style, Line, Word, Syllable, Char
+from .convert import Convert, ColorModel
+from .shape import Shape
+from .utils import Utils, FrameUtility, ColorUtility
+
+__version__ = "0.9.10-reloaded"
diff --git a/syncplay/pyonfx/ass_core.py b/syncplay/pyonfx/ass_core.py
new file mode 100644
index 0000000..f7641bc
--- /dev/null
+++ b/syncplay/pyonfx/ass_core.py
@@ -0,0 +1,1427 @@
+# -*- coding: utf-8 -*-
+# PyonFX: An easy way to create KFX (Karaoke Effects) and complex typesetting using the ASS format (Advanced Substation Alpha).
+# Copyright (C) 2019 Antonio Strippoli (CoffeeStraw/YellowFlash)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyonFX is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see http://www.gnu.org/licenses/.
+
+from __future__ import annotations
+import re
+import os
+import sys
+import time
+import copy
+import subprocess
+from typing import List, Tuple, Union, Optional
+
+from .font_utility import Font
+from .convert import Convert
+
+
+class Meta:
+ """Meta object contains informations about the Ass.
+
+ More info about each of them can be found on http://docs.aegisub.org/manual/Styles
+
+ Attributes:
+ wrap_style (int): Determines how line breaking is applied to the subtitle line
+ scaled_border_and_shadow (bool): Determines if it has to be used script resolution (*True*) or video resolution (*False*) to scale border and shadow
+ play_res_x (int): Video Width
+ play_res_y (int): Video Height
+ audio (str): Loaded audio path (absolute)
+ video (str): Loaded video path (absolute)
+ """
+
+ wrap_style: int
+ scaled_border_and_shadow: bool
+ play_res_x: int
+ play_res_y: int
+ audio: str
+ video: str
+
+ def __repr__(self):
+ return pretty_print(self)
+
+
+class Style:
+ """Style object contains a set of typographic formatting rules that is applied to dialogue lines.
+
+ More info about styles can be found on http://docs.aegisub.org/3.2/ASS_Tags/.
+
+ Attributes:
+ fontname (str): Font name
+ fontsize (float): Font size in points
+ color1 (str): Primary color (fill)
+ alpha1 (str): Transparency of color1
+ color2 (str): Secondary color (secondary fill, for karaoke effect)
+ alpha2 (str): Transparency of color2
+ color3 (str): Outline (border) color
+ alpha3 (str): Transparency of color3
+ color4 (str): Shadow color
+ alpha4 (str): Transparency of color4
+ bold (bool): Font with bold
+ italic (bool): Font with italic
+ underline (bool): Font with underline
+ strikeout (bool): Font with strikeout
+ scale_x (float): Text stretching in the horizontal direction
+ scale_y (float): Text stretching in the vertical direction
+ spacing (float): Horizontal spacing between letters
+ angle (float): Rotation of the text
+ border_style (bool): *True* for opaque box, *False* for standard outline
+ outline (float): Border thickness value
+ shadow (float): How far downwards and to the right a shadow is drawn
+ alignment (int): Alignment of the text
+ margin_l (int): Distance from the left of the video frame
+ margin_r (int): Distance from the right of the video frame
+ margin_v (int): Distance from the bottom (or top if alignment >= 7) of the video frame
+ encoding (int): Codepage used to map codepoints to glyphs
+ """
+
+ fontname: str
+ fontsize: float
+ color1: str
+ alpha1: str
+ color2: str
+ alpha2: str
+ color3: str
+ alpha3: str
+ color4: str
+ alpha4: str
+ bold: bool
+ italic: bool
+ underline: bool
+ strikeout: bool
+ scale_x: float
+ scale_y: float
+ spacing: float
+ angle: float
+ border_style: bool
+ outline: float
+ shadow: float
+ alignment: int
+ margin_l: int
+ margin_r: int
+ margin_v: int
+ encoding: int
+
+ def __repr__(self):
+ return pretty_print(self)
+
+
+class Char:
+ """Char object contains informations about a single char of a line in the Ass.
+
+ A char is defined by some text between two karaoke tags (k, ko, kf).
+
+ Attributes:
+ i (int): Char index number
+ word_i (int): Char word index (e.g.: In line text ``Hello PyonFX users!``, letter "u" will have word_i=2).
+ syl_i (int): Char syl index (e.g.: In line text ``{\\k0}Hel{\\k0}lo {\\k0}Pyon{\\k0}FX {\\k0}users!``, letter "F" will have syl_i=3).
+ syl_char_i (int): Char invidual syl index (e.g.: In line text ``{\\k0}Hel{\\k0}lo {\\k0}Pyon{\\k0}FX {\\k0}users!``, letter "e" of "users" will have syl_char_i=2).
+ start_time (int): Char start time (in milliseconds).
+ end_time (int): Char end time (in milliseconds).
+ duration (int): Char duration (in milliseconds).
+ styleref (obj): Reference to the Style object of this object original line.
+ text (str): Char text.
+ inline_fx (str): Char inline effect (marked as \\-EFFECT in karaoke-time).
+ prespace (int): Char free space before text.
+ postspace (int): Char free space after text.
+ width (float): Char text width.
+ height (float): Char text height.
+ x (float): Char text position horizontal (depends on alignment).
+ y (float): Char text position vertical (depends on alignment).
+ left (float): Char text position left.
+ center (float): Char text position center.
+ right (float): Char text position right.
+ top (float): Char text position top.
+ middle (float): Char text position middle.
+ bottom (float): Char text position bottom.
+ """
+
+ i: int
+ word_i: int
+ syl_i: int
+ syl_char_i: int
+ start_time: int
+ end_time: int
+ duration: int
+ styleref: Style
+ text: str
+ inline_fx: str
+ prespace: int
+ postspace: int
+ width: float
+ height: float
+ x: float
+ y: float
+ left: float
+ center: float
+ right: float
+ top: float
+ middle: float
+ bottom: float
+
+ def __repr__(self):
+ return pretty_print(self)
+
+
+class Syllable:
+ """Syllable object contains informations about a single syl of a line in the Ass.
+
+ A syl can be defined as some text after a karaoke tag (k, ko, kf)
+ (e.g.: In ``{\\k0}Hel{\\k0}lo {\\k0}Pyon{\\k0}FX {\\k0}users!``, "Pyon" and "FX" are distinct syllables),
+
+ Attributes:
+ i (int): Syllable index number
+ word_i (int): Syllable word index (e.g.: In line text ``{\\k0}Hel{\\k0}lo {\\k0}Pyon{\\k0}FX {\\k0}users!``, syl "Pyon" will have word_i=1).
+ start_time (int): Syllable start time (in milliseconds).
+ end_time (int): Syllable end time (in milliseconds).
+ duration (int): Syllable duration (in milliseconds).
+ styleref (obj): Reference to the Style object of this object original line.
+ text (str): Syllable text.
+ tags (str): All the remaining tags before syl text apart \\k ones.
+ inline_fx (str): Syllable inline effect (marked as \\-EFFECT in karaoke-time).
+ prespace (int): Syllable free space before text.
+ postspace (int): Syllable free space after text.
+ width (float): Syllable text width.
+ height (float): Syllable text height.
+ x (float): Syllable text position horizontal (depends on alignment).
+ y (float): Syllable text position vertical (depends on alignment).
+ left (float): Syllable text position left.
+ center (float): Syllable text position center.
+ right (float): Syllable text position right.
+ top (float): Syllable text position top.
+ middle (float): Syllable text position middle.
+ bottom (float): Syllable text position bottom.
+ """
+
+ i: int
+ word_i: int
+ start_time: int
+ end_time: int
+ duration: int
+ styleref: Style
+ text: str
+ tags: str
+ inline_fx: str
+ prespace: int
+ postspace: int
+ width: float
+ height: float
+ x: float
+ y: float
+ left: float
+ center: float
+ right: float
+ top: float
+ middle: float
+ bottom: float
+
+ def __repr__(self):
+ return pretty_print(self)
+
+
+class Word:
+ """Word object contains informations about a single word of a line in the Ass.
+
+ A word can be defined as some text with some optional space before or after
+ (e.g.: In the string "What a beautiful world!", "beautiful" and "world" are both distinct words).
+
+ Attributes:
+ i (int): Word index number
+ start_time (int): Word start time (same as line start time) (in milliseconds).
+ end_time (int): Word end time (same as line end time) (in milliseconds).
+ duration (int): Word duration (same as line duration) (in milliseconds).
+ styleref (obj): Reference to the Style object of this object original line.
+ text (str): Word text.
+ prespace (int): Word free space before text.
+ postspace (int): Word free space after text.
+ width (float): Word text width.
+ height (float): Word text height.
+ x (float): Word text position horizontal (depends on alignment).
+ y (float): Word text position vertical (depends on alignment).
+ left (float): Word text position left.
+ center (float): Word text position center.
+ right (float): Word text position right.
+ top (float): Word text position top.
+ middle (float): Word text position middle.
+ bottom (float): Word text position bottom.
+ """
+
+ i: int
+ start_time: int
+ end_time: int
+ duration: int
+ styleref: Style
+ text: str
+ prespace: int
+ postspace: int
+ width: float
+ height: float
+ x: float
+ y: float
+ left: float
+ center: float
+ right: float
+ top: float
+ middle: float
+ bottom: float
+
+ def __repr__(self):
+ return pretty_print(self)
+
+
+class Line:
+ """Line object contains informations about a single line in the Ass.
+
+ Note:
+ (*) = This field is available only if :class:`extended` = True
+
+ Attributes:
+ i (int): Line index number
+ comment (bool): If *True*, this line will not be displayed on the screen.
+ layer (int): Layer for the line. Higher layer numbers are drawn on top of lower ones.
+ start_time (int): Line start time (in milliseconds).
+ end_time (int): Line end time (in milliseconds).
+ duration (int): Line duration (in milliseconds) (*).
+ leadin (float): Time between this line and the previous one (in milliseconds; first line = 1000.1) (*).
+ leadout (float): Time between this line and the next one (in milliseconds; first line = 1000.1) (*).
+ style (str): Style name used for this line.
+ styleref (obj): Reference to the Style object of this line (*).
+ actor (str): Actor field.
+ margin_l (int): Left margin for this line.
+ margin_r (int): Right margin for this line.
+ margin_v (int): Vertical margin for this line.
+ effect (str): Effect field.
+ raw_text (str): Line raw text.
+ text (str): Line stripped text (no tags).
+ width (float): Line text width (*).
+ height (float): Line text height (*).
+ ascent (float): Line font ascent (*).
+ descent (float): Line font descent (*).
+ internal_leading (float): Line font internal lead (*).
+ external_leading (float): Line font external lead (*).
+ x (float): Line text position horizontal (depends on alignment) (*).
+ y (float): Line text position vertical (depends on alignment) (*).
+ left (float): Line text position left (*).
+ center (float): Line text position center (*).
+ right (float): Line text position right (*).
+ top (float): Line text position top (*).
+ middle (float): Line text position middle (*).
+ bottom (float): Line text position bottom (*).
+ words (list): List containing objects :class:`Word` in this line (*).
+ syls (list): List containing objects :class:`Syllable` in this line (if available) (*).
+ chars (list): List containing objects :class:`Char` in this line (*).
+ """
+
+ i: int
+ comment: bool
+ layer: int
+ start_time: int
+ end_time: int
+ duration: int
+ leadin: float
+ leadout: float
+ style: str
+ styleref: Style
+ actor: str
+ margin_l: int
+ margin_r: int
+ margin_v: int
+ effect: str
+ raw_text: str
+ text: str
+ width: float
+ height: float
+ ascent: float
+ descent: float
+ internal_leading: float
+ external_leading: float
+ x: float
+ y: float
+ left: float
+ center: float
+ right: float
+ top: float
+ middle: float
+ bottom: float
+ words: List[Word]
+ syls: List[Syllable]
+ chars: List[Char]
+
+ def __repr__(self):
+ return pretty_print(self)
+
+ def copy(self) -> Line:
+ """
+ Returns:
+ A deep copy of this object (line)
+ """
+ return copy.deepcopy(self)
+
+
+class Ass:
+ """Contains all the informations about a file in the ASS format and the methods to work with it for both input and output.
+
+ | Usually you will create an Ass object and use it for input and output (see example_ section).
+ | PyonFX set automatically an absolute path for all the info in the output, so that wherever you will
+ put your generated file, it should always load correctly video and audio.
+
+ Args:
+ path_input (str): Path for the input file (either relative to your .py file or absolute).
+ path_output (str): Path for the output file (either relative to your .py file or absolute) (DEFAULT: "Output.ass").
+ keep_original (bool): If True, you will find all the lines of the input file commented before the new lines generated.
+ extended (bool): Calculate more informations from lines (usually you will not have to touch this).
+ vertical_kanji (bool): If True, line text with alignment 4, 5 or 6 will be positioned vertically.
+ Additionally, ``line`` fields will be re-calculated based on the re-positioned ``line.chars``.
+
+ Attributes:
+ path_input (str): Path for input file (absolute). If none create default from scratch like in aegisub Untitled.ass
+ path_output (str): Path for output file (absolute).
+ meta (:class:`Meta`): Contains informations about the ASS given.
+ styles (list of :class:`Style`): Contains all the styles in the ASS given.
+ lines (list of :class:`Line`): Contains all the lines (events) in the ASS given.
+
+ .. _example:
+ Example:
+ .. code-block:: python3
+
+ io = Ass ("in.ass")
+ meta, styles, lines = io.get_data()
+ """
+
+ def __init__( self,
+ path_input: str = "",
+ path_output: str = "Output.ass",
+ keep_original: bool = True,
+ extended: bool = True,
+ vertical_kanji: bool = False,):
+ # Starting to take process time
+ self.__saved = False
+ self.__plines = 0
+ self.__ptime = time.time()
+ self.meta, self.styles, self.lines = Meta(), {}, []
+
+ #if(path_input != ""):
+ # print("Warning path input is ignored, please use input() or load()")
+ #self.input(path_input)
+
+ self.__output = []
+ self.__output_extradata = []
+
+ self.parse_ass(path_input, path_output, keep_original, extended, vertical_kanji)
+
+ def set_input(self, path_input) :
+ """
+ Allow to set the input file
+ Args:
+ path_input (str): Path for the input file (either relative to your .py file or absolute).
+ """
+ section_pattern = re.compile(r"^\[Script Info\]")
+ if(path_input == ""):
+ #Use aesisub default template
+ path_input=os.path.join(os.path.dirname(os.path.abspath(__file__)),"Untitled.ass")
+ elif section_pattern.match(path_input):
+ #path input is an ass valid content
+ pass
+ else:
+ # Getting absolute sub file path
+ dirname = os.path.dirname(os.path.abspath(sys.argv[0]))
+
+ if(re.search("\.(ass|ssa|jass|jsos)$",path_input)):
+ if not os.path.isabs(path_input):
+ path_input = os.path.join(dirname, path_input)
+
+ # Checking sub file validity (does it exists?)
+ if not os.path.isfile(path_input):
+ raise FileNotFoundError(
+ "Invalid path for the Subtitle file: %s" % path_input
+ )
+ else:
+ raise FileNotFoundError(
+ "Invalid input for the Subtitle file"
+ )
+ self.path_input = path_input
+
+ def set_output(self, path_output) :
+ """
+ Allow to set the output file
+ Args:
+ path_output (str): Path for the output file (either relative to your .py file or absolute)
+ """
+ # Getting absolute sub file path
+ dirname = os.path.dirname(os.path.abspath(sys.argv[0]))
+ # Getting absolute output file path
+ if path_output == "Output.ass" or path_output == "Untitled.ass":
+ path_output = os.path.join(dirname, path_output)
+ elif not os.path.isabs(path_output):
+ path_output = os.path.join(dirname, path_output)
+
+ self.path_output = path_output
+
+ def parse_ass(
+ self,
+ path_input: str = "",
+ path_output: str = "Output.ass",
+ keep_original: bool = True,
+ extended: bool = True,
+ vertical_kanji: bool = False,
+ ):
+ """
+ Parse an input ASS file using its path
+ Args:
+ path_input (str): Path for the input file (either relative to your .py file or absolute).
+ path_output (str): Path for the output file (either relative to your .py file or absolute) (DEFAULT: "Output.ass").
+ keep_original (bool): If True, you will find all the lines of the input file commented before the new lines generated.
+ extended (bool): Calculate more informations from lines (usually you will not have to touch this).
+ vertical_kanji (bool): If True, line text with alignment 4, 5 or 6 will be positioned vertically.
+ Additionally, ``line`` fields will be re-calculated based on the re-positioned ``line.chars``.
+
+ Attributes:
+ path_input (str): Path for input file (absolute). If none create default from scratch like in aegisub Untitled.ass
+ path_output (str): Path for output file (absolute).
+ meta (:class:`Meta`): Contains informations about the ASS given.
+ styles (list of :class:`Style`): Contains all the styles in the ASS given.
+ lines (list of :class:`Line`): Contains all the lines (events) in the ASS given.
+
+ .. _example:
+ Example:
+ .. code-block:: python3
+
+ io = Ass ("in.ass")
+ meta, styles, lines = io.get_data()
+ """
+ # Starting to take process time
+ self.__saved = False
+ self.__plines = 0
+ self.__ptime = time.time()
+
+ self.meta, self.styles, self.lines = Meta(), {}, []
+
+ content=""
+ section_pattern = re.compile(r"^\[Script Info\]")
+ if(section_pattern.match(path_input)):
+ # input is a content
+ content = path_input
+ elif(path_input ==""):
+ dirname = os.path.dirname(__file__)
+ path_input = os.path.join(dirname, "Untitled.ass")
+ else:
+ # input is a path file
+ self.set_input(path_input)
+ path_input =self.path_input
+
+ self.set_output(path_output)
+
+ self.__output = []
+ self.__output_extradata = []
+
+ section = ""
+ li = 0
+
+ #Get the stream of content or file content
+ if(content):
+ from io import StringIO
+ stream = StringIO(content)
+ else:
+ try:
+ stream = open(path_input, "r", encoding="utf-8-sig")
+ except FileNotFoundError:
+ raise FileNotFoundError(
+ "Unsupported or broken subtitle file: '%s'" % path_input
+ )
+ #previous read set the cursor at the end, put it back at the start
+ stream.seek(0,0)
+ for line in stream:
+ # Getting section
+ section_pattern = re.compile(r"^\[([^\]]*)")
+ if section_pattern.match(line):
+ # Updating section
+ section = section_pattern.match(line)[1]
+ # Appending line to output
+ if section != "Aegisub Extradata":
+ self.__output.append(line)
+
+ # Parsing Meta data
+ elif section == "Script Info" or section == "Aegisub Project Garbage":
+ # Internal function that tries to get the absolute path for media files in meta
+ def get_media_abs_path(mediafile):
+ # If this is not a dummy video, let's try to get the absolute path for the video
+ if not mediafile.startswith("?dummy"):
+ tmp = mediafile
+ media_dir = os.path.dirname(self.path_input)
+
+ while mediafile.startswith("../"):
+ media_dir = os.path.dirname(media_dir)
+ mediafile = mediafile[3:]
+
+ mediafile = os.path.normpath(
+ "%s%s%s" % (media_dir, os.sep, mediafile)
+ )
+
+ if not os.path.isfile(mediafile):
+ mediafile = tmp
+
+ return mediafile
+
+ # Switch
+ if re.match(r"WrapStyle: *?(\d+)$", line):
+ self.meta.wrap_style = int(line[11:].strip())
+ elif re.match(r"ScaledBorderAndShadow: *?(.+)$", line):
+ self.meta.scaled_border_and_shadow = line[23:].strip() == "yes"
+ elif re.match(r"PlayResX: *?(\d+)$", line):
+ self.meta.play_res_x = int(line[10:].strip())
+ elif re.match(r"PlayResY: *?(\d+)$", line):
+ self.meta.play_res_y = int(line[10:].strip())
+ elif re.match(r"Audio File: *?(.*)$", line):
+ self.meta.audio = get_media_abs_path(line[11:].strip())
+ line = "Audio File: %s\n" % self.meta.audio
+ elif re.match(r"Video File: *?(.*)$", line):
+ self.meta.video = get_media_abs_path(line[11:].strip())
+ line = "Video File: %s\n" % self.meta.video
+
+ # Appending line to output
+ self.__output.append(line)
+
+ # Parsing Styles
+ elif section == "V4+ Styles":
+ # Appending line to output
+ self.__output.append(line)
+ style = re.match(r"Style: (.+?)$", line)
+
+ if style:
+ # Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour,
+ # Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle,
+ # BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
+ style = [el for el in style[1].split(",")]
+ tmp = Style()
+
+ tmp.fontname = style[1]
+ tmp.fontsize = float(style[2])
+
+ tmp.color1, tmp.alpha1 = f"&H{style[3][4:]}&", f"{style[3][:4]}&"
+ tmp.color2, tmp.alpha2 = f"&H{style[4][4:]}&", f"{style[4][:4]}&"
+ tmp.color3, tmp.alpha3 = f"&H{style[5][4:]}&", f"{style[5][:4]}&"
+ tmp.color4, tmp.alpha4 = f"&H{style[6][4:]}&", f"{style[6][:4]}&"
+
+ tmp.bold = style[7] == "-1"
+ tmp.italic = style[8] == "-1"
+ tmp.underline = style[9] == "-1"
+ tmp.strikeout = style[10] == "-1"
+
+ tmp.scale_x = float(style[11])
+ tmp.scale_y = float(style[12])
+
+ tmp.spacing = float(style[13])
+ tmp.angle = float(style[14])
+
+ tmp.border_style = style[15] == "3"
+ tmp.outline = float(style[16])
+ tmp.shadow = float(style[17])
+
+ tmp.alignment = int(style[18])
+ tmp.margin_l = int(style[19])
+ tmp.margin_r = int(style[20])
+ tmp.margin_v = int(style[21])
+
+ tmp.encoding = int(style[22])
+
+ self.styles[style[0]] = tmp
+
+ # Parsing Dialogues
+ elif section == "Events":
+ # Appending line to output (commented) if keep_original is True
+ if keep_original:
+ self.__output.append(
+ re.sub(r"^(Dialogue|Comment):", "Comment:", line, count=1)
+ )
+ elif line.startswith("Format"):
+ self.__output.append(line.strip())
+
+ # Analyzing line
+ line = re.match(r"(Dialogue|Comment): (.+?)$", line)
+
+ if line:
+ # Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
+ tmp = Line()
+
+ tmp.i = li
+ li += 1
+
+ tmp.comment = line[1] == "Comment"
+ line = [el for el in line[2].split(",")]
+
+ tmp.layer = int(line[0])
+
+ tmp.start_time = Convert.time(line[1])
+ tmp.end_time = Convert.time(line[2])
+
+ tmp.style = line[3]
+ tmp.actor = line[4]
+
+ tmp.margin_l = int(line[5])
+ tmp.margin_r = int(line[6])
+ tmp.margin_v = int(line[7])
+
+ tmp.effect = line[8]
+
+ tmp.raw_text = ",".join(line[9:])
+
+ self.lines.append(tmp)
+
+ elif section == "Aegisub Extradata":
+ self.__output_extradata.append(line)
+
+ else:
+ raise ValueError(f"Unexpected section in the input file: [{section}]")
+
+ # Adding informations to lines and meta?
+ if not extended:
+ return None
+ else:
+ return self.add_pyonfx_extension()
+
+ def add_pyonfx_extension(self):
+ """
+ Calculate more informations from lines ( this affects the lines only if play_res_x and play_res_y are provided in the Script info section).
+ Args: None
+ return None
+ """
+ #security check if no video provided, abort calculations
+ if (not hasattr(self.meta,"play_res_x") or not hasattr(self.meta,"play_res_y")):
+ return None
+ lines_by_styles = {}
+ # Let the fun begin (Pyon!)
+ for li, line in enumerate(self.lines):
+ try:
+ line.styleref = self.styles[line.style]
+ except KeyError:
+ line.styleref = None
+
+ # Append dialog to styles (for leadin and leadout later)
+ if line.style not in lines_by_styles:
+ lines_by_styles[line.style] = []
+ lines_by_styles[line.style].append(line)
+
+ line.duration = line.end_time - line.start_time
+ line.text = re.sub(r"\{.*?\}", "", line.raw_text)
+
+ # Add dialog text sizes and positions (if possible)
+ if line.styleref:
+ # Creating a Font object and saving return values of font.get_metrics() for the future
+ font = Font(line.styleref)
+ font_metrics = font.get_metrics()
+
+ line.width, line.height = font.get_text_extents(line.text)
+ (
+ line.ascent,
+ line.descent,
+ line.internal_leading,
+ line.external_leading,
+ ) = font_metrics
+
+ if self.meta.play_res_x > 0 and self.meta.play_res_y > 0:
+ # Horizontal position
+ tmp_margin_l = (
+ line.margin_l if line.margin_l != 0 else line.styleref.margin_l
+ )
+ tmp_margin_r = (
+ line.margin_r if line.margin_r != 0 else line.styleref.margin_r
+ )
+
+ if (line.styleref.alignment - 1) % 3 == 0:
+ line.left = tmp_margin_l
+ line.center = line.left + line.width / 2
+ line.right = line.left + line.width
+ line.x = line.left
+ elif (line.styleref.alignment - 2) % 3 == 0:
+ line.left = (
+ self.meta.play_res_x / 2
+ - line.width / 2
+ + tmp_margin_l / 2
+ - tmp_margin_r / 2
+ )
+ line.center = line.left + line.width / 2
+ line.right = line.left + line.width
+ line.x = line.center
+ else:
+ line.left = self.meta.play_res_x - tmp_margin_r - line.width
+ line.center = line.left + line.width / 2
+ line.right = line.left + line.width
+ line.x = line.right
+
+ # Vertical position
+ if line.styleref.alignment > 6:
+ line.top = (
+ line.margin_v
+ if line.margin_v != 0
+ else line.styleref.margin_v
+ )
+ line.middle = line.top + line.height / 2
+ line.bottom = line.top + line.height
+ line.y = line.top
+ elif line.styleref.alignment > 3:
+ line.top = self.meta.play_res_y / 2 - line.height / 2
+ line.middle = line.top + line.height / 2
+ line.bottom = line.top + line.height
+ line.y = line.middle
+ else:
+ line.top = (
+ self.meta.play_res_y
+ - (
+ line.margin_v
+ if line.margin_v != 0
+ else line.styleref.margin_v
+ )
+ - line.height
+ )
+ line.middle = line.top + line.height / 2
+ line.bottom = line.top + line.height
+ line.y = line.bottom
+
+ # Calculating space width and saving spacing
+ space_width = font.get_text_extents(" ")[0]
+ style_spacing = line.styleref.spacing
+
+ # Adding words
+ line.words = []
+
+ wi = 0
+ for prespace, word_text, postspace in re.findall(
+ r"(\s*)([^\s]+)(\s*)", line.text
+ ):
+ word = Word()
+
+ word.i = wi
+ wi += 1
+
+ word.start_time = line.start_time
+ word.end_time = line.end_time
+ word.duration = line.duration
+
+ word.styleref = line.styleref
+ word.text = word_text
+
+ word.prespace = len(prespace)
+ word.postspace = len(postspace)
+
+ word.width, word.height = font.get_text_extents(word.text)
+ (
+ word.ascent,
+ word.descent,
+ word.internal_leading,
+ word.external_leading,
+ ) = font_metrics
+
+ line.words.append(word)
+
+ # Calculate word positions with all words data already available
+ if line.words and self.meta.play_res_x > 0 and self.meta.play_res_y > 0:
+ if line.styleref.alignment > 6 or line.styleref.alignment < 4:
+ cur_x = line.left
+ for word in line.words:
+ # Horizontal position
+ cur_x = cur_x + word.prespace * (
+ space_width + style_spacing
+ )
+
+ word.left = cur_x
+ word.center = word.left + word.width / 2
+ word.right = word.left + word.width
+
+ if (line.styleref.alignment - 1) % 3 == 0:
+ word.x = word.left
+ elif (line.styleref.alignment - 2) % 3 == 0:
+ word.x = word.center
+ else:
+ word.x = word.right
+
+ # Vertical position
+ word.top = line.top
+ word.middle = line.middle
+ word.bottom = line.bottom
+ word.y = line.y
+
+ # Updating cur_x
+ cur_x = (
+ cur_x
+ + word.width
+ + word.postspace * (space_width + style_spacing)
+ + style_spacing
+ )
+ else:
+ max_width, sum_height = 0, 0
+ for word in line.words:
+ max_width = max(max_width, word.width)
+ sum_height = sum_height + word.height
+
+ cur_y = x_fix = self.meta.play_res_y / 2 - sum_height / 2
+ for word in line.words:
+ # Horizontal position
+ x_fix = (max_width - word.width) / 2
+
+ if line.styleref.alignment == 4:
+ word.left = line.left + x_fix
+ word.center = word.left + word.width / 2
+ word.right = word.left + word.width
+ word.x = word.left
+ elif line.styleref.alignment == 5:
+ word.left = self.meta.play_res_x / 2 - word.width / 2
+ word.center = word.left + word.width / 2
+ word.right = word.left + word.width
+ word.x = word.center
+ else:
+ word.left = line.right - word.width - x_fix
+ word.center = word.left + word.width / 2
+ word.right = word.left + word.width
+ word.x = word.right
+
+ # Vertical position
+ word.top = cur_y
+ word.middle = word.top + word.height / 2
+ word.bottom = word.top + word.height
+ word.y = word.middle
+ cur_y = cur_y + word.height
+
+ # Search for dialog's text chunks, to later create syllables
+ # A text chunk is a text with one or more {tags} preceding it
+ # Tags can be some text or empty string
+ text_chunks = []
+ tag_pattern = re.compile(r"(\{.*?\})+")
+ tag = tag_pattern.search(line.raw_text)
+ word_i = 0
+
+ if not tag:
+ # No tags found
+ text_chunks.append({"tags": "", "text": line.raw_text})
+ else:
+ # First chunk without tags?
+ if tag.start() != 0:
+ text_chunks.append(
+ {"tags": "", "text": line.raw_text[0 : tag.start()]}
+ )
+
+ # Searching for other tags
+ while True:
+ next_tag = tag_pattern.search(line.raw_text, tag.end())
+ tmp = {
+ # Note that we're removing possibles '}{' caused by consecutive tags
+ "tags": line.raw_text[
+ tag.start() + 1 : tag.end() - 1
+ ].replace("}{", ""),
+ "text": line.raw_text[
+ tag.end() : (next_tag.start() if next_tag else None)
+ ],
+ "word_i": word_i,
+ }
+ text_chunks.append(tmp)
+
+ # If there are some spaces after text, then we're at the end of the current word
+ if re.match(r"(.*?)(\s+)$", tmp["text"]):
+ word_i = word_i + 1
+
+ if not next_tag:
+ break
+ tag = next_tag
+
+ # Adding syls
+ si = 0
+ last_time = 0
+ inline_fx = ""
+ syl_tags_pattern = re.compile(r"(.*?)\\[kK][of]?(\d+)(.*)")
+
+ line.syls = []
+ for tc in text_chunks:
+ # If we don't have at least one \k tag, everything is invalid
+ if not syl_tags_pattern.match(tc["tags"]):
+ line.syls.clear()
+ break
+
+ posttags = tc["tags"]
+ syls_in_text_chunk = []
+ while True:
+ # Are there \k in posttags?
+ tags_syl = syl_tags_pattern.match(posttags)
+
+ if not tags_syl:
+ # Append all the temporary syls, except last one
+ for syl in syls_in_text_chunk[:-1]:
+ curr_inline_fx = re.search(r"\\\-([^\\]+)", syl.tags)
+ if curr_inline_fx:
+ inline_fx = curr_inline_fx[1]
+ syl.inline_fx = inline_fx
+
+ # Hidden syls are treated like empty syls
+ syl.prespace, syl.text, syl.postspace = 0, "", 0
+
+ syl.width, syl.height = font.get_text_extents("")
+ (
+ syl.ascent,
+ syl.descent,
+ syl.internal_leading,
+ syl.external_leading,
+ ) = font_metrics
+
+ line.syls.append(syl)
+
+ # Append last syl
+ syl = syls_in_text_chunk[-1]
+ syl.tags += posttags
+
+ curr_inline_fx = re.search(r"\\\-([^\\]+)", syl.tags)
+ if curr_inline_fx:
+ inline_fx = curr_inline_fx[1]
+ syl.inline_fx = inline_fx
+
+ if tc["text"].isspace():
+ syl.prespace, syl.text, syl.postspace = 0, tc["text"], 0
+ else:
+ syl.prespace, syl.text, syl.postspace = re.match(
+ r"(\s*)(.*?)(\s*)$", tc["text"]
+ ).groups()
+ syl.prespace, syl.postspace = (
+ len(syl.prespace),
+ len(syl.postspace),
+ )
+
+ syl.width, syl.height = font.get_text_extents(syl.text)
+ (
+ syl.ascent,
+ syl.descent,
+ syl.internal_leading,
+ syl.external_leading,
+ ) = font_metrics
+
+ line.syls.append(syl)
+ break
+
+ pretags, kdur, posttags = tags_syl.groups()
+
+ # Create a Syllable object
+ syl = Syllable()
+
+ syl.start_time = last_time
+ syl.end_time = last_time + int(kdur) * 10
+ syl.duration = int(kdur) * 10
+
+ syl.styleref = line.styleref
+ syl.tags = pretags
+
+ syl.i = si
+ syl.word_i = tc["word_i"]
+
+ syls_in_text_chunk.append(syl)
+
+ # Update working variable
+ si += 1
+ last_time = syl.end_time
+
+ # Calculate syllables positions with all syllables data already available
+ if line.syls and self.meta.play_res_x > 0 and self.meta.play_res_y > 0:
+ if (
+ line.styleref.alignment > 6
+ or line.styleref.alignment < 4
+ or not vertical_kanji
+ ):
+ cur_x = line.left
+ for syl in line.syls:
+ cur_x = cur_x + syl.prespace * (space_width + style_spacing)
+ # Horizontal position
+ syl.left = cur_x
+ syl.center = syl.left + syl.width / 2
+ syl.right = syl.left + syl.width
+
+ if (line.styleref.alignment - 1) % 3 == 0:
+ syl.x = syl.left
+ elif (line.styleref.alignment - 2) % 3 == 0:
+ syl.x = syl.center
+ else:
+ syl.x = syl.right
+
+ cur_x = (
+ cur_x
+ + syl.width
+ + syl.postspace * (space_width + style_spacing)
+ + style_spacing
+ )
+
+ # Vertical position
+ syl.top = line.top
+ syl.middle = line.middle
+ syl.bottom = line.bottom
+ syl.y = line.y
+
+ else: # Kanji vertical position
+ max_width, sum_height = 0, 0
+ for syl in line.syls:
+ max_width = max(max_width, syl.width)
+ sum_height = sum_height + syl.height
+
+ cur_y = self.meta.play_res_y / 2 - sum_height / 2
+
+ for syl in line.syls:
+ # Horizontal position
+ x_fix = (max_width - syl.width) / 2
+ if line.styleref.alignment == 4:
+ syl.left = line.left + x_fix
+ syl.center = syl.left + syl.width / 2
+ syl.right = syl.left + syl.width
+ syl.x = syl.left
+ elif line.styleref.alignment == 5:
+ syl.left = line.center - syl.width / 2
+ syl.center = syl.left + syl.width / 2
+ syl.right = syl.left + syl.width
+ syl.x = syl.center
+ else:
+ syl.left = line.right - syl.width - x_fix
+ syl.center = syl.left + syl.width / 2
+ syl.right = syl.left + syl.width
+ syl.x = syl.right
+
+ # Vertical position
+ syl.top = cur_y
+ syl.middle = syl.top + syl.height / 2
+ syl.bottom = syl.top + syl.height
+ syl.y = syl.middle
+ cur_y = cur_y + syl.height
+
+ # Adding chars
+ line.chars = []
+
+ # If we have syls in line, we prefert to work with them to provide more informations
+ if line.syls:
+ words_or_syls = line.syls
+ else:
+ words_or_syls = line.words
+
+ # Getting chars
+ char_index = 0
+ for el in words_or_syls:
+ el_text = "{}{}{}".format(
+ " " * el.prespace, el.text, " " * el.postspace
+ )
+ for ci, char_text in enumerate(list(el_text)):
+ char = Char()
+ char.i = ci
+
+ # If we're working with syls, we can add some indexes
+ char.i = char_index
+ char_index += 1
+ if line.syls:
+ char.word_i = el.word_i
+ char.syl_i = el.i
+ char.syl_char_i = ci
+ else:
+ char.word_i = el.i
+
+ # Adding last fields based on the existance of syls or not
+ char.start_time = el.start_time
+ char.end_time = el.end_time
+ char.duration = el.duration
+
+ char.styleref = line.styleref
+ char.text = char_text
+
+ char.width, char.height = font.get_text_extents(char.text)
+ (
+ char.ascent,
+ char.descent,
+ char.internal_leading,
+ char.external_leading,
+ ) = font_metrics
+
+ line.chars.append(char)
+
+ # Calculate character positions with all characters data already available
+ if line.chars and self.meta.play_res_x > 0 and self.meta.play_res_y > 0:
+ if (
+ line.styleref.alignment > 6
+ or line.styleref.alignment < 4
+ or not vertical_kanji
+ ):
+ cur_x = line.left
+ for char in line.chars:
+ # Horizontal position
+ char.left = cur_x
+ char.center = char.left + char.width / 2
+ char.right = char.left + char.width
+
+ if (line.styleref.alignment - 1) % 3 == 0:
+ char.x = char.left
+ elif (line.styleref.alignment - 2) % 3 == 0:
+ char.x = char.center
+ else:
+ char.x = char.right
+
+ cur_x = cur_x + char.width + style_spacing
+
+ # Vertical position
+ char.top = line.top
+ char.middle = line.middle
+ char.bottom = line.bottom
+ char.y = line.y
+ else:
+ max_width, sum_height = 0, 0
+ for char in line.chars:
+ max_width = max(max_width, char.width)
+ sum_height = sum_height + char.height
+
+ cur_y = x_fix = self.meta.play_res_y / 2 - sum_height / 2
+
+ # Fixing line positions
+ line.top = cur_y
+ line.middle = self.meta.play_res_y / 2
+ line.bottom = line.top + sum_height
+ line.width = max_width
+ line.height = sum_height
+ if line.styleref.alignment == 4:
+ line.center = line.left + max_width / 2
+ line.right = line.left + max_width
+ elif line.styleref.alignment == 5:
+ line.left = line.center - max_width / 2
+ line.right = line.left + max_width
+ else:
+ line.left = line.right - max_width
+ line.center = line.left + max_width / 2
+
+ for char in line.chars:
+ # Horizontal position
+ x_fix = (max_width - char.width) / 2
+ if line.styleref.alignment == 4:
+ char.left = line.left + x_fix
+ char.center = char.left + char.width / 2
+ char.right = char.left + char.width
+ char.x = char.left
+ elif line.styleref.alignment == 5:
+ char.left = self.meta.play_res_x / 2 - char.width / 2
+ char.center = char.left + char.width / 2
+ char.right = char.left + char.width
+ char.x = char.center
+ else:
+ char.left = line.right - char.width - x_fix
+ char.center = char.left + char.width / 2
+ char.right = char.left + char.width
+ char.x = char.right
+
+ # Vertical position
+ char.top = cur_y
+ char.middle = char.top + char.height / 2
+ char.bottom = char.top + char.height
+ char.y = char.middle
+ cur_y = cur_y + char.height
+
+ # Add durations between dialogs
+ for style in lines_by_styles:
+ lines_by_styles[style].sort(key=lambda x: x.start_time)
+ for li, line in enumerate(lines_by_styles[style]):
+ line.leadin = (
+ 1000.1
+ if li == 0
+ else line.start_time - lines_by_styles[style][li - 1].end_time
+ )
+ line.leadout = (
+ 1000.1
+ if li == len(lines_by_styles[style]) - 1
+ else lines_by_styles[style][li + 1].start_time - line.end_time
+ )
+
+ def get_data(self) -> Tuple[Meta, Style, List[Line]]:
+ """Utility function to retrieve easily meta styles and lines.
+
+ Returns:
+ :attr:`meta`, :attr:`styles` and :attr:`lines`
+ """
+ return self.meta, self.styles, self.lines
+
+ def del_line(self,no):
+ """Delete a line of the output list (which is private) """
+ nb=-1
+
+ # Retrieve the index of the first line, this is ugly having to do so
+ #as is if we could'nt rectify self.lines instead and generate self.__output on save()
+ # in lua this is what has been done when you get the aegisub object
+ for li, line in enumerate(self.__output):
+ if re.match(r"\n?(Dialogue|Comment): (.+?)$", line):
+ nb=li-1
+ break;
+
+ if (nb >=0) and isinstance(self.__output[no+nb], str):
+ del self.__output[no+nb]
+ self.__plines -= 1
+ else:
+ raise TypeError("No Line %d exists" % no)
+
+ def write_line(self, line: Line) -> Optional[TypeError]:
+ """Appends a line to the output list (which is private) that later on will be written to the output file when calling save().
+
+ Use it whenever you've prepared a line, it will not impact performance since you
+ will not actually write anything until :func:`save` will be called.
+
+ Parameters:
+ line (:class:`Line`): A line object. If not valid, TypeError is raised.
+ """
+ if isinstance(line, Line):
+ self.__output.append(
+ "\n%s: %d,%s,%s,%s,%s,%04d,%04d,%04d,%s,%s"
+ % (
+ "Comment" if line.comment else "Dialogue",
+ line.layer,
+ Convert.time(max(0, int(line.start_time))),
+ Convert.time(max(0, int(line.end_time))),
+ line.style,
+ line.actor,
+ line.margin_l,
+ line.margin_r,
+ line.margin_v,
+ line.effect,
+ line.text,
+ )
+ )
+ self.__plines += 1
+ else:
+ raise TypeError("Expected Line object, got %s." % type(line))
+
+ def save(self, quiet: bool = False) -> None:
+ """Write everything inside the private output list to a file.
+
+ Parameters:
+ quiet (bool): If True, you will not get printed any message.
+ """
+
+ # Writing to file
+ with open(self.path_output, "w", encoding="utf-8-sig") as f:
+ f.writelines(self.__output + ["\n"])
+ if self.__output_extradata:
+ f.write("\n[Aegisub Extradata]\n")
+ f.writelines(self.__output_extradata)
+
+ self.__saved = True
+
+ if not quiet:
+ print(
+ "Produced lines: %d\nProcess duration (in seconds): %.3f"
+ % (self.__plines, time.time() - self.__ptime)
+ )
+
+ def open_aegisub(self) -> int:
+ """Open the output (specified in self.path_output) with Aegisub.
+
+ This can be usefull if you don't have MPV installed or you want to look at your output in detailed.
+
+ Returns:
+ 0 if success, -1 if the output couldn't be opened.
+ """
+
+ # Check if it was saved
+ if not self.__saved:
+ print(
+ "[WARNING] You've tried to open the output with Aegisub before having saved. Check your code."
+ )
+ return -1
+
+ if sys.platform == "win32":
+ os.startfile(self.path_output)
+ else:
+ try:
+ subprocess.call(["aegisub", os.path.abspath(self.path_output)])
+ except FileNotFoundError:
+ print("[WARNING] Aegisub not found.")
+ return -1
+
+ return 0
+
+ def open_mpv(
+ self, video_path: str = "", video_start: str = "", full_screen: bool = False
+ ) -> int:
+ """Open the output (specified in self.path_output) in softsub with the MPV player.
+ To utilize this function, MPV player is required. Additionally if you're on Windows, MPV must be in the PATH (check https://pyonfx.readthedocs.io/en/latest/quick%20start.html#installation-extra-step).
+
+ This is one of the fastest way to reproduce your output in a comfortable way.
+
+ Parameters:
+ video_path (string): The video file path (absolute) to reproduce. If not specified, **meta.video** is automatically taken.
+ video_start (string): The start time for the video (more info: https://mpv.io/manual/master/#options-start). If not specified, 0 is automatically taken.
+ full_screen (bool): If True, it will reproduce the output in full screen. If not specified, False is automatically taken.
+ """
+
+ # Check if it was saved
+ if not self.__saved:
+ print(
+ "[ERROR] You've tried to open the output with MPV before having saved. Check your code."
+ )
+ return -1
+
+ # Check if mpv is usable
+ if self.meta.video.startswith("?dummy") and not video_path:
+ print(
+ "[WARNING] Cannot use MPV (if you have it in your PATH) for file preview, since your .ass contains a dummy video.\n"
+ "You can specify a new video source using video_path parameter, check the documentation of the function."
+ )
+ return -1
+
+ # Setting up the command to execute
+ cmd = ["mpv"]
+
+ if not video_path:
+ cmd.append(self.meta.video)
+ else:
+ cmd.append(video_path)
+ if video_start:
+ cmd.append("--start=" + video_start)
+ if full_screen:
+ cmd.append("--fs")
+
+ cmd.append("--sub-file=" + self.path_output)
+
+ try:
+ subprocess.call(cmd)
+ except FileNotFoundError:
+ print(
+ "[WARNING] MPV not found in your environment variables.\n"
+ "Please refer to the documentation's \"Quick Start\" section if you don't know how to solve it."
+ )
+ return -1
+
+ return 0
+
+
+def pretty_print(
+ obj: Union[Meta, Style, Line, Word, Syllable, Char], indent: int = 0, name: str = ""
+) -> str:
+ # Utility function to print object Meta, Style, Line, Word, Syllable and Char (this is a dirty solution probably)
+ if type(obj) == Line:
+ out = " " * indent + f"lines[{obj.i}] ({type(obj).__name__}):\n"
+ elif type(obj) == Word:
+ out = " " * indent + f"words[{obj.i}] ({type(obj).__name__}):\n"
+ elif type(obj) == Syllable:
+ out = " " * indent + f"syls[{obj.i}] ({type(obj).__name__}):\n"
+ elif type(obj) == Char:
+ out = " " * indent + f"chars[{obj.i}] ({type(obj).__name__}):\n"
+ else:
+ out = " " * indent + f"{name}({type(obj).__name__}):\n"
+
+ # Let's print all this object fields
+ indent += 4
+ for k, v in obj.__dict__.items():
+ if "__dict__" in dir(v):
+ # Work recursively to print another object
+ out += pretty_print(v, indent, k + " ")
+ elif type(v) == list:
+ for i, el in enumerate(v):
+ # Work recursively to print other objects inside a list
+ out += pretty_print(el, indent, f"{k}[{i}] ")
+ else:
+ # Just print a field of this object
+ out += " " * indent + f"{k}: {str(v)}\n"
+
+ return out
diff --git a/syncplay/pyonfx/convert.py b/syncplay/pyonfx/convert.py
new file mode 100644
index 0000000..8ce3f76
--- /dev/null
+++ b/syncplay/pyonfx/convert.py
@@ -0,0 +1,765 @@
+# -*- coding: utf-8 -*-
+# PyonFX: An easy way to create KFX (Karaoke Effects) and complex typesetting using the ASS format (Advanced Substation Alpha).
+# Copyright (C) 2019 Antonio Strippoli (CoffeeStraw/YellowFlash)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyonFX is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see http://www.gnu.org/licenses/.
+
+from __future__ import annotations
+import re
+import math
+import colorsys
+from enum import Enum
+from typing import List, NamedTuple, Tuple, Union, TYPE_CHECKING
+
+from .font_utility import Font
+
+if TYPE_CHECKING:
+ from .ass_core import Line, Word, Syllable, Char
+ from .shape import Shape
+
+# A simple NamedTuple to represent pixels
+Pixel = NamedTuple("Pixel", [("x", float), ("y", float), ("alpha", int)])
+
+
+class ColorModel(Enum):
+ ASS = "&HBBGGRR&"
+ ASS_STYLE = "&HAABBGGRR"
+ RGB = "(r, g, b)"
+ RGB_STR = "#RRGGBB"
+ RGBA = "(r, g, b, a)"
+ RGBA_STR = "#RRGGBBAA"
+ HSV = "(h, s, v)"
+
+
+class Convert:
+ """
+ This class is a collection of static methods that will help
+ the user to convert everything needed to the ASS format.
+ """
+
+ @staticmethod
+ def time(ass_ms: Union[int, str]) -> Union[str, int, ValueError]:
+ """Converts between milliseconds and ASS timestamp.
+
+ You can probably ignore that function, you will not make use of it for KFX or typesetting generation.
+
+ Parameters:
+ ass_ms (int or str): If int, than milliseconds are expected, else ASS timestamp as str is expected.
+
+ Returns:
+ If milliseconds -> ASS timestamp, else if ASS timestamp -> milliseconds, else ValueError will be raised.
+ """
+ # Milliseconds?
+ if type(ass_ms) is int and ass_ms >= 0:
+ return "{:d}:{:02d}:{:02d}.{:02d}".format(
+ math.floor(ass_ms / 3600000) % 10,
+ math.floor(ass_ms % 3600000 / 60000),
+ math.floor(ass_ms % 60000 / 1000),
+ math.floor(ass_ms % 1000 / 10),
+ )
+ # ASS timestamp?
+ elif type(ass_ms) is str and re.match(r"^\d:\d+:\d+\.\d+$", ass_ms):
+ return (
+ int(ass_ms[0]) * 3600000
+ + int(ass_ms[2:4]) * 60000
+ + int(ass_ms[5:7]) * 1000
+ + int(ass_ms[8:10]) * 10
+ )
+ else:
+ raise ValueError("Milliseconds or ASS timestamp expected")
+
+ @staticmethod
+ def alpha_ass_to_dec(alpha_ass: str) -> int:
+ """Converts from ASS alpha string to corresponding decimal value.
+
+ Parameters:
+ alpha_ass (str): A string in the format '&HXX&'.
+
+ Returns:
+ A decimal in [0, 255] representing ``alpha_ass`` converted.
+
+ Examples:
+ .. code-block:: python3
+
+ print(Convert.alpha_ass_to_dec("&HFF&"))
+
+ >>> 255
+ """
+ try:
+ match = re.fullmatch(r"&H([0-9A-F]{2})&", alpha_ass)
+ return int(match.group(1), 16)
+ except TypeError as e:
+ raise TypeError(
+ f"Provided ASS alpha was expected of type 'str', but you provided a '{type(alpha_ass)}'."
+ ) from e
+ except AttributeError as e:
+ raise ValueError(
+ f"Provided ASS alpha string '{alpha_ass}' is not in the expected format '&HXX&'."
+ ) from e
+
+ @staticmethod
+ def alpha_dec_to_ass(alpha_dec: Union[int, float]) -> str:
+ """Converts from decimal value to corresponding ASS alpha string.
+
+ Parameters:
+ alpha_dec (int or float): Decimal in [0, 255] representing an alpha value.
+
+ Returns:
+ A string in the format '&HXX&' representing ``alpha_dec`` converted.
+
+ Examples:
+ .. code-block:: python3
+
+ print(Convert.alpha_dec_to_ass(255))
+ print(Convert.alpha_dec_to_ass(255.0))
+
+ >>> "&HFF&"
+ >>> "&HFF&"
+ """
+ try:
+ if not 0 <= alpha_dec <= 255:
+ raise ValueError(
+ f"Provided alpha decimal '{alpha_dec}' is out of the range [0, 255]."
+ )
+ except TypeError as e:
+ raise TypeError(
+ f"Provided alpha decimal was expected of type 'int' or 'float', but you provided a '{type(alpha_dec)}'."
+ ) from e
+ return f"&H{round(alpha_dec):02X}&"
+
+ @staticmethod
+ def color(
+ c: Union[
+ str,
+ Union[
+ Tuple[
+ Union[int, float],
+ Union[int, float],
+ Union[int, float],
+ ],
+ Tuple[
+ Union[int, float],
+ Union[int, float],
+ Union[int, float],
+ Union[int, float],
+ ],
+ ],
+ ],
+ input_format: ColorModel,
+ output_format: ColorModel,
+ round_output: bool = True,
+ ) -> Union[
+ str,
+ Tuple[int, int, int],
+ Tuple[int, int, int, int],
+ Tuple[float, float, float],
+ Tuple[float, float, float, float],
+ ]:
+ """Converts a provided color from a color model to another.
+
+ Parameters:
+ c (str or tuple of int or tuple of float): A color in the format ``input_format``.
+ input_format (ColorModel): The color format of ``c``.
+ output_format (ColorModel): The color format for the output.
+ round_output (bool): A boolean to determine whether the output should be rounded or not.
+
+ Returns:
+ A color in the format ``output_format``.
+
+ Examples:
+ .. code-block:: python3
+
+ print(Convert.color("&H0000FF&", ColorModel.ASS, ColorModel.RGB))
+
+ >>> (255, 0, 0)
+ """
+ try:
+ # Text for exception if input is out of ranges
+ input_range_e = f"Provided input '{c}' has value(s) out of the range "
+
+ # Parse input, obtaining its corresponding (r,g,b,a) values
+ if input_format == ColorModel.ASS:
+ match = re.fullmatch(r"&H([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})&", c)
+ (b, g, r), a = map(lambda x: int(x, 16), match.groups()), 255
+ elif input_format == ColorModel.ASS_STYLE:
+ match = re.fullmatch(
+ r"&H([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})", c
+ )
+ a, b, g, r = map(lambda x: int(x, 16), match.groups())
+ elif input_format == ColorModel.RGB:
+ if not all(0 <= n <= 255 for n in c):
+ raise ValueError(input_range_e + "[0, 255].")
+ (r, g, b), a = c, 255
+ elif input_format == ColorModel.RGB_STR:
+ match = re.fullmatch(r"#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})", c)
+ (r, g, b), a = map(lambda x: int(x, 16), match.groups()), 255
+ elif input_format == ColorModel.RGBA:
+ if not all(0 <= n <= 255 for n in c):
+ raise ValueError(input_range_e + "[0, 255].")
+ r, g, b, a = c
+ elif input_format == ColorModel.RGBA_STR:
+ match = re.fullmatch(
+ r"#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})", c
+ )
+ r, g, b, a = map(lambda x: int(x, 16), match.groups())
+ elif input_format == ColorModel.HSV:
+ if not (0 <= c[0] < 360 and 0 <= c[1] <= 100 and 0 <= c[2] <= 100):
+ raise ValueError(
+ input_range_e + "( [0, 360), [0, 100], [0, 100] )."
+ )
+ h, s, v = c[0] / 360, c[1] / 100, c[2] / 100
+ (r, g, b), a = map(lambda x: 255 * x, colorsys.hsv_to_rgb(h, s, v)), 255
+ except (AttributeError, ValueError, TypeError) as e:
+ # AttributeError -> re.fullmatch failed
+ # ValueError -> too many values to unpack
+ # TypeError -> in case the provided tuple is not a list of numbers
+ raise ValueError(
+ f"Provided input '{c}' is not in the format '{input_format}'."
+ ) from e
+
+ # Convert (r,g,b,a) to the desired output_format
+ try:
+ if output_format == ColorModel.ASS:
+ return f"&H{round(b):02X}{round(g):02X}{round(r):02X}&"
+ elif output_format == ColorModel.ASS_STYLE:
+ return f"&H{round(a):02X}{round(b):02X}{round(g):02X}{round(r):02X}"
+ elif output_format == ColorModel.RGB:
+ method = round if round_output else float
+ return tuple(map(method, (r, g, b)))
+ elif output_format == ColorModel.RGB_STR:
+ return f"#{round(r):02X}{round(g):02X}{round(b):02X}"
+ elif output_format == ColorModel.RGBA:
+ method = round if round_output else float
+ return tuple(map(method, (r, g, b, a)))
+ elif output_format == ColorModel.RGBA_STR:
+ return f"#{round(r):02X}{round(g):02X}{round(b):02X}{round(a):02X}"
+ elif output_format == ColorModel.HSV:
+ method = round if round_output else float
+ h, s, v = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255)
+ return method(h * 360) % 360, method(s * 100), method(v * 100)
+ else:
+ raise ValueError(f"Unsupported output_format ('{output_format}').")
+ except NameError as e:
+ raise ValueError(f"Unsupported input_format ('{input_format}').") from e
+
+ @staticmethod
+ def color_ass_to_rgb(
+ color_ass: str, as_str: bool = False
+ ) -> Union[str, Tuple[int, int, int]]:
+ """Converts from ASS color string to corresponding RGB color.
+
+ Parameters:
+ color_ass (str): A string in the format '&HBBGGRR&'.
+ as_str (bool): A boolean to determine the output type format.
+
+ Returns:
+ The output represents ``color_ass`` converted. If ``as_str`` = False, the output is a tuple of integers in range *[0, 255]*.
+ Else, the output is a string in the format '#RRGGBB'.
+
+ Examples:
+ .. code-block:: python3
+
+ print(Convert.color_ass_to_rgb("&HABCDEF&"))
+ print(Convert.color_ass_to_rgb("&HABCDEF&", as_str=True))
+
+ >>> (239, 205, 171)
+ >>> "#EFCDAB"
+ """
+ return Convert.color(
+ color_ass, ColorModel.ASS, ColorModel.RGB_STR if as_str else ColorModel.RGB
+ )
+
+ @staticmethod
+ def color_ass_to_hsv(
+ color_ass: str, round_output: bool = True
+ ) -> Union[Tuple[int, int, int], Tuple[float, float, float]]:
+ """Converts from ASS color string to corresponding HSV color.
+
+ Parameters:
+ color_ass (str): A string in the format '&HBBGGRR&'.
+ round_output (bool): A boolean to determine whether the output should be rounded or not.
+
+ Returns:
+ The output represents ``color_ass`` converted. If ``round_output`` = True, the output is a tuple of integers in range *( [0, 360), [0, 100], [0, 100] )*.
+ Else, the output is a tuple of floats in range *( [0, 360), [0, 100], [0, 100] )*.
+
+ Examples:
+ .. code-block:: python3
+
+ print(Convert.color_ass_to_hsv("&HABCDEF&"))
+ print(Convert.color_ass_to_hsv("&HABCDEF&", round_output=False))
+
+ >>> (30, 28, 94)
+ >>> (30.000000000000014, 28.451882845188294, 93.72549019607843)
+ """
+ return Convert.color(color_ass, ColorModel.ASS, ColorModel.HSV, round_output)
+
+ @staticmethod
+ def color_rgb_to_ass(
+ color_rgb: Union[
+ str, Tuple[Union[int, float], Union[int, float], Union[int, float]]
+ ]
+ ) -> str:
+ """Converts from RGB color to corresponding ASS color.
+
+ Parameters:
+ color_rgb (str or tuple of int or tuple of float): Either a string in the format '#RRGGBB' or a tuple of three integers (or floats) in the range *[0, 255]*.
+
+ Returns:
+ A string in the format '&HBBGGRR&' representing ``color_rgb`` converted.
+
+ Examples:
+ .. code-block:: python3
+
+ print(Convert.color_rgb_to_ass("#ABCDEF"))
+
+ >>> "&HEFCDAB&"
+ """
+ return Convert.color(
+ color_rgb,
+ ColorModel.RGB_STR if type(color_rgb) is str else ColorModel.RGB,
+ ColorModel.ASS,
+ )
+
+ @staticmethod
+ def color_rgb_to_hsv(
+ color_rgb: Union[
+ str, Tuple[Union[int, float], Union[int, float], Union[int, float]]
+ ],
+ round_output: bool = True,
+ ) -> Union[Tuple[int, int, int], Tuple[float, float, float]]:
+ """Converts from RGB color to corresponding HSV color.
+
+ Parameters:
+ color_rgb (str or tuple of int or tuple of float): Either a string in the format '#RRGGBB' or a tuple of three integers (or floats) in the range *[0, 255]*.
+ round_output (bool): A boolean to determine whether the output should be rounded or not.
+
+ Returns:
+ The output represents ``color_rgb`` converted. If ``round_output`` = True, the output is a tuple of integers in range *( [0, 360), [0, 100], [0, 100] )*.
+ Else, the output is a tuple of floats in range *( [0, 360), [0, 100], [0, 100] )*.
+
+ Examples:
+ .. code-block:: python3
+
+ print(Convert.color_rgb_to_hsv("#ABCDEF"))
+ print(Convert.color_rgb_to_hsv("#ABCDEF"), round_output=False)
+
+ >>> (210, 28, 94)
+ >>> (210.0, 28.451882845188294, 93.72549019607843)
+ """
+ return Convert.color(
+ color_rgb,
+ ColorModel.RGB_STR if type(color_rgb) is str else ColorModel.RGB,
+ ColorModel.HSV,
+ round_output,
+ )
+
+ @staticmethod
+ def color_hsv_to_ass(
+ color_hsv: Tuple[Union[int, float], Union[int, float], Union[int, float]]
+ ) -> str:
+ """Converts from HSV color string to corresponding ASS color.
+
+ Parameters:
+ color_hsv (tuple of int/float): A tuple of three integers (or floats) in the range *( [0, 360), [0, 100], [0, 100] )*.
+
+ Returns:
+ A string in the format '&HBBGGRR&' representing ``color_hsv`` converted.
+
+ Examples:
+ .. code-block:: python3
+
+ print(Convert.color_hsv_to_ass((100, 100, 100)))
+
+ >>> "&H00FF55&"
+ """
+ return Convert.color(color_hsv, ColorModel.HSV, ColorModel.ASS)
+
+ @staticmethod
+ def color_hsv_to_rgb(
+ color_hsv: Tuple[Union[int, float], Union[int, float], Union[int, float]],
+ as_str: bool = False,
+ round_output: bool = True,
+ ) -> str:
+ """Converts from HSV color string to corresponding RGB color.
+
+ Parameters:
+ color_hsv (tuple of int/float): A tuple of three integers (or floats) in the range *( [0, 360), [0, 100], [0, 100] )*.
+ as_str (bool): A boolean to determine the output type format.
+ round_output (bool): A boolean to determine whether the output should be rounded or not.
+
+ Returns:
+ The output represents ``color_hsv`` converted. If ``as_str`` = False, the output is a tuple
+ ( also, if ``round_output`` = True, the output is a tuple of integers in range *( [0, 360), [0, 100], [0, 100] )*, else a tuple of float in range *( [0, 360), [0, 100], [0, 100] ) )*.
+ Else, the output is a string in the format '#RRGGBB'.
+
+ Examples:
+ .. code-block:: python3
+
+ print(Convert.color_hsv_to_rgb((100, 100, 100)))
+ print(Convert.color_hsv_to_rgb((100, 100, 100), as_str=True))
+ print(Convert.color_hsv_to_rgb((100, 100, 100), round_output=False))
+
+ >>> (85, 255, 0)
+ >>> "#55FF00"
+ >>> (84.99999999999999, 255.0, 0.0)
+ """
+ return Convert.color(
+ color_hsv,
+ ColorModel.HSV,
+ ColorModel.RGB_STR if as_str else ColorModel.RGB,
+ round_output,
+ )
+
+ @staticmethod
+ def text_to_shape(
+ obj: Union[Line, Word, Syllable, Char], fscx: float = None, fscy: float = None
+ ) -> Shape:
+ """Converts text with given style information to an ASS shape.
+
+ **Tips:** *You can easily create impressive deforming effects.*
+
+ Parameters:
+ obj (Line, Word, Syllable or Char): An object of class Line, Word, Syllable or Char.
+ fscx (float, optional): The scale_x value for the shape.
+ fscy (float, optional): The scale_y value for the shape.
+
+ Returns:
+ A Shape object, representing the text with the style format values of the object.
+
+ Examples:
+ .. code-block:: python3
+
+ line = Line.copy(lines[1])
+ line.text = "{\\\\an7\\\\pos(%.3f,%.3f)\\\\p1}%s" % (line.left, line.top, Convert.text_to_shape(line))
+ io.write_line(line)
+ """
+ # Obtaining information and editing values of style if requested
+ original_scale_x = obj.styleref.scale_x
+ original_scale_y = obj.styleref.scale_y
+
+ # Editing temporary the style to properly get the shape
+ if fscx is not None:
+ obj.styleref.scale_x = fscx
+ if fscy is not None:
+ obj.styleref.scale_y = fscy
+
+ # Obtaining font information from style and obtaining shape
+ font = Font(obj.styleref)
+ shape = font.text_to_shape(obj.text)
+ # Clearing resources to not let overflow errors take over
+ del font
+
+ # Restoring values of style and returning the shape converted
+ if fscx is not None:
+ obj.styleref.scale_x = original_scale_x
+ if fscy is not None:
+ obj.styleref.scale_y = original_scale_y
+ return shape
+
+ @staticmethod
+ def text_to_clip(
+ obj: Union[Line, Word, Syllable, Char],
+ an: int = 5,
+ fscx: float = None,
+ fscy: float = None,
+ ) -> Shape:
+ """Converts text with given style information to an ASS shape, applying some translation/scaling to it since
+ it is not possible to position a shape with \\pos() once it is in a clip.
+
+ This is an high level function since it does some additional operations, check text_to_shape for further infromations.
+
+ **Tips:** *You can easily create text masks even for growing/shrinking text without too much effort.*
+
+ Parameters:
+ obj (Line, Word, Syllable or Char): An object of class Line, Word, Syllable or Char.
+ an (integer, optional): The alignment wanted for the shape.
+ fscx (float, optional): The scale_x value for the shape.
+ fscy (float, optional): The scale_y value for the shape.
+
+ Returns:
+ A Shape object, representing the text with the style format values of the object.
+
+ Examples:
+ .. code-block:: python3
+
+ line = Line.copy(lines[1])
+ line.text = "{\\\\an5\\\\pos(%.3f,%.3f)\\\\clip(%s)}%s" % (line.center, line.middle, Convert.text_to_clip(line), line.text)
+ io.write_line(line)
+ """
+ # Checking for errors
+ if an < 1 or an > 9:
+ raise ValueError("Alignment value must be an integer between 1 and 9")
+
+ # Setting default values
+ if fscx is None:
+ fscx = obj.styleref.scale_x
+ if fscy is None:
+ fscy = obj.styleref.scale_y
+
+ # Obtaining text converted to shape
+ shape = Convert.text_to_shape(obj, fscx, fscy)
+
+ # Setting mult_x based on alignment
+ if an % 3 == 1: # an=1 or an=4 or an=7
+ mult_x = 0
+ elif an % 3 == 2: # an=2 or an=5 or an=8
+ mult_x = 1 / 2
+ else:
+ mult_x = 1
+
+ # Setting mult_y based on alignment
+ if an < 4:
+ mult_y = 1
+ elif an < 7:
+ mult_y = 1 / 2
+ else:
+ mult_y = 0
+
+ # Calculating offsets
+ cx = (
+ obj.left
+ - obj.width * mult_x * (fscx - obj.styleref.scale_x) / obj.styleref.scale_x
+ )
+ cy = (
+ obj.top
+ - obj.height * mult_y * (fscy - obj.styleref.scale_y) / obj.styleref.scale_y
+ )
+
+ return shape.move(cx, cy)
+
+ @staticmethod
+ def text_to_pixels(
+ obj: Union[Line, Word, Syllable, Char], supersampling: int = 8
+ ) -> List[Pixel]:
+ """| Converts text with given style information to a list of pixel data.
+ | A pixel data is a dictionary containing 'x' (horizontal position), 'y' (vertical position) and 'alpha' (alpha/transparency).
+
+ It is highly suggested to create a dedicated style for pixels,
+ because you will write less tags for line in your pixels, which means less size for your .ass file.
+
+ | The style suggested is:
+ | - **an=7 (very important!);**
+ | - bord=0;
+ | - shad=0;
+ | - For Font informations leave whatever the default is;
+
+ **Tips:** *It allows easy creation of text decaying or light effects.*
+
+ Parameters:
+ obj (Line, Word, Syllable or Char): An object of class Line, Word, Syllable or Char.
+ supersampling (int): Value used for supersampling. Higher value means smoother and more precise anti-aliasing (and more computational time for generation).
+
+ Returns:
+ A list of dictionaries representing each individual pixel of the input text styled.
+
+ Examples:
+ .. code-block:: python3
+
+ line = lines[2].copy()
+ line.style = "p"
+ p_sh = Shape.rectangle()
+ for pixel in Convert.text_to_pixels(line):
+ x, y = math.floor(line.left) + pixel['x'], math.floor(line.top) + pixel['y']
+ alpha = "\\alpha" + Convert.color_alpha_to_ass(pixel['alpha']) if pixel['alpha'] != 255 else ""
+
+ line.text = "{\\p1\\pos(%d,%d)%s}%s" % (x, y, alpha, p_sh)
+ io.write_line(line)
+ """
+ shape = Convert.text_to_shape(obj).move(obj.left % 1, obj.top % 1)
+ return Convert.shape_to_pixels(shape, supersampling)
+
+ @staticmethod
+ def shape_to_pixels(shape: Shape, supersampling: int = 8) -> List[Pixel]:
+ """| Converts a Shape object to a list of pixel data.
+ | A pixel data is a dictionary containing 'x' (horizontal position), 'y' (vertical position) and 'alpha' (alpha/transparency).
+
+ It is highly suggested to create a dedicated style for pixels,
+ because you will write less tags for line in your pixels, which means less size for your .ass file.
+
+ | The style suggested is:
+ | - **an=7 (very important!);**
+ | - bord=0;
+ | - shad=0;
+ | - For Font informations leave whatever the default is;
+
+ **Tips:** *As for text, even shapes can decay!*
+
+ Parameters:
+ shape (Shape): An object of class Shape.
+ supersampling (int): Value used for supersampling. Higher value means smoother and more precise anti-aliasing (and more computational time for generation).
+
+ Returns:
+ A list of dictionaries representing each individual pixel of the input shape.
+
+ Examples:
+ .. code-block:: python3
+
+ line = lines[2].copy()
+ line.style = "p"
+ p_sh = Shape.rectangle()
+ for pixel in Convert.shape_to_pixels(Shape.heart(100)):
+ # Random circle to pixel effect just to show
+ x, y = math.floor(line.left) + pixel.x, math.floor(line.top) + pixel.y
+ alpha = "\\alpha" + Convert.color_alpha_to_ass(pixel.alpha) if pixel.alpha != 255 else ""
+
+ line.text = "{\\p1\\pos(%d,%d)%s\\fad(0,%d)}%s" % (x, y, alpha, l.dur/4, p_sh)
+ io.write_line(line)
+ """
+ # Scale values for supersampled rendering
+ upscale = supersampling
+ downscale = 1 / upscale
+
+ # Upscale shape for later downsampling
+ shape.map(lambda x, y: (x * upscale, y * upscale))
+
+ # Bring shape near origin in positive room
+ x1, y1, x2, y2 = shape.bounding()
+ shift_x, shift_y = -1 * (x1 - x1 % upscale), -1 * (y1 - y1 % upscale)
+ shape.move(shift_x, shift_y)
+
+ # Create image
+ width, height = (
+ math.ceil((x2 + shift_x) * downscale) * upscale,
+ math.ceil((y2 + shift_y) * downscale) * upscale,
+ )
+ image = [False for i in range(width * height)]
+
+ # Renderer (on binary image with aliasing)
+ lines, last_point, last_move = [], {}, {}
+
+ def collect_lines(x, y, typ):
+ # Collect lines (points + vectors)
+ nonlocal lines, last_point, last_move
+ x, y = int(round(x)), int(round(y)) # Use integers to avoid rounding errors
+
+ # Move
+ if typ == "m":
+ # Close figure with non-horizontal line in image
+ if (
+ last_move
+ and last_move["y"] != last_point["y"]
+ and not (last_point["y"] < 0 and last_move["y"] < 0)
+ and not (last_point["y"] > height and last_move["y"] > height)
+ ):
+ lines.append(
+ [
+ last_point["x"],
+ last_point["y"],
+ last_move["x"] - last_point["x"],
+ last_move["y"] - last_point["y"],
+ ]
+ )
+
+ last_move = {"x": x, "y": y}
+ # Non-horizontal line in image
+ elif (
+ last_point
+ and last_point["y"] != y
+ and not (last_point["y"] < 0 and y < 0)
+ and not (last_point["y"] > height and y > height)
+ ):
+ lines.append(
+ [
+ last_point["x"],
+ last_point["y"],
+ x - last_point["x"],
+ y - last_point["y"],
+ ]
+ )
+
+ # Remember last point
+ last_point = {"x": x, "y": y}
+
+ shape.flatten().map(collect_lines)
+
+ # Close last figure with non-horizontal line in image
+ if (
+ last_move
+ and last_move["y"] != last_point["y"]
+ and not (last_point["y"] < 0 and last_move["y"] < 0)
+ and not (last_point["y"] > height and last_move["y"] > height)
+ ):
+ lines.append(
+ [
+ last_point["x"],
+ last_point["y"],
+ last_move["x"] - last_point["x"],
+ last_move["y"] - last_point["y"],
+ ]
+ )
+
+ # Calculates line x horizontal line intersection
+ def line_x_hline(x, y, vx, vy, y2):
+ if vy != 0:
+ s = (y2 - y) / vy
+ if s >= 0 and s <= 1:
+ return x + s * vx
+ return None
+
+ # Scan image rows in shape
+ _, y1, _, y2 = shape.bounding()
+ for y in range(max(math.floor(y1), 0), min(math.ceil(y2), height)):
+ # Collect row intersections with lines
+ row_stops = []
+ for line in lines:
+ cx = line_x_hline(line[0], line[1], line[2], line[3], y + 0.5)
+ if cx is not None:
+ row_stops.append(
+ [max(0, min(cx, width)), 1 if line[3] > 0 else -1]
+ ) # image trimmed stop position & line vertical direction
+
+ # Enough intersections / something to render?
+ if len(row_stops) > 1:
+ # Sort row stops by horizontal position
+ row_stops.sort(key=lambda x: x[0])
+ # Render!
+ status, row_index = 0, y * width
+ for i in range(0, len(row_stops) - 1):
+ status = status + row_stops[i][1]
+ if status != 0:
+ for x in range(
+ math.ceil(row_stops[i][0] - 0.5),
+ math.floor(row_stops[i + 1][0] + 0.5),
+ ):
+ image[row_index + x] = True
+
+ # Extract pixels from image
+ pixels = []
+ for y in range(0, height, upscale):
+ for x in range(0, width, upscale):
+ opacity = 0
+ for yy in range(0, upscale):
+ for xx in range(0, upscale):
+ if image[(y + yy) * width + (x + xx)]:
+ opacity = opacity + 255
+
+ if opacity > 0:
+ pixels.append(
+ Pixel(
+ x=(x - shift_x) * downscale,
+ y=(y - shift_y) * downscale,
+ alpha=round(opacity * downscale ** 2),
+ )
+ )
+
+ return pixels
+
+ @staticmethod
+ def image_to_ass(image):
+ pass
+
+ @staticmethod
+ def image_to_pixels(image):
+ pass
diff --git a/syncplay/pyonfx/font_utility.py b/syncplay/pyonfx/font_utility.py
new file mode 100644
index 0000000..2328c23
--- /dev/null
+++ b/syncplay/pyonfx/font_utility.py
@@ -0,0 +1,353 @@
+# -*- coding: utf-8 -*-
+# PyonFX: An easy way to create KFX (Karaoke Effects) and complex typesetting using the ASS format (Advanced Substation Alpha).
+# Copyright (C) 2019 Antonio Strippoli (CoffeeStraw/YellowFlash)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyonFX is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see http://www.gnu.org/licenses/.
+"""
+This module contains the Font class definition, which has some functions
+to help getting informations from a specific font
+"""
+from __future__ import annotations
+import sys
+from typing import Tuple, TYPE_CHECKING
+
+from .shape import Shape
+
+if sys.platform == "win32":
+ import win32gui # pylint: disable=import-error
+ import win32ui # pylint: disable=import-error
+ import win32con # pylint: disable=import-error
+elif sys.platform in ["linux", "darwin"] and not "sphinx" in sys.modules:
+ import cairo # pylint: disable=import-error
+ import gi # pylint: disable=import-error
+
+ gi.require_version("Pango", "1.0")
+ gi.require_version("PangoCairo", "1.0")
+
+ from gi.repository import Pango, PangoCairo # pylint: disable=import-error
+ import html
+
+if TYPE_CHECKING:
+ from .ass_core import Style
+
+# CONFIGURATION
+FONT_PRECISION = 64 # Font scale for better precision output from native font system
+LIBASS_FONTHACK = True # Scale font data to fontsize? (no effect on windows)
+PANGO_SCALE = 1024 # The PANGO_SCALE macro represents the scale between dimensions used for Pango distances and device units.
+
+
+class Font:
+ """
+ Font class definition
+ """
+
+ def __init__(self, style: Style):
+ self.family = style.fontname
+ self.bold = style.bold
+ self.italic = style.italic
+ self.underline = style.underline
+ self.strikeout = style.strikeout
+ self.size = style.fontsize
+ self.xscale = style.scale_x / 100
+ self.yscale = style.scale_y / 100
+ self.hspace = style.spacing
+ self.upscale = FONT_PRECISION
+ self.downscale = 1 / FONT_PRECISION
+
+ if sys.platform == "win32":
+ # Create device context
+ self.dc = win32gui.CreateCompatibleDC(None)
+ # Set context coordinates mapping mode
+ win32gui.SetMapMode(self.dc, win32con.MM_TEXT)
+ # Set context backgrounds to transparent
+ win32gui.SetBkMode(self.dc, win32con.TRANSPARENT)
+ # Create font handle
+ font_spec = {
+ "height": int(self.size * self.upscale),
+ "width": 0,
+ "escapement": 0,
+ "orientation": 0,
+ "weight": win32con.FW_BOLD if self.bold else win32con.FW_NORMAL,
+ "italic": int(self.italic),
+ "underline": int(self.underline),
+ "strike out": int(self.strikeout),
+ "charset": win32con.DEFAULT_CHARSET,
+ "out precision": win32con.OUT_TT_PRECIS,
+ "clip precision": win32con.CLIP_DEFAULT_PRECIS,
+ "quality": win32con.ANTIALIASED_QUALITY,
+ "pitch and family": win32con.DEFAULT_PITCH + win32con.FF_DONTCARE,
+ "name": self.family,
+ }
+ self.pycfont = win32ui.CreateFont(font_spec)
+ win32gui.SelectObject(self.dc, self.pycfont.GetSafeHandle())
+ # Calculate metrics
+ self.metrics = win32gui.GetTextMetrics(self.dc)
+ elif sys.platform == "linux" or sys.platform == "darwin":
+ surface = cairo.ImageSurface(cairo.Format.A8, 1, 1)
+
+ self.context = cairo.Context(surface)
+ self.layout = PangoCairo.create_layout(self.context)
+
+ font_description = Pango.FontDescription()
+ font_description.set_family(self.family)
+ font_description.set_absolute_size(self.size * self.upscale * PANGO_SCALE)
+ font_description.set_weight(
+ Pango.Weight.BOLD if self.bold else Pango.Weight.NORMAL
+ )
+ font_description.set_style(
+ Pango.Style.ITALIC if self.italic else Pango.Style.NORMAL
+ )
+
+ self.layout.set_font_description(font_description)
+ self.metrics = Pango.Context.get_metrics(
+ self.layout.get_context(), self.layout.get_font_description()
+ )
+
+ if LIBASS_FONTHACK:
+ self.fonthack_scale = self.size / (
+ (self.metrics.get_ascent() + self.metrics.get_descent())
+ / PANGO_SCALE
+ * self.downscale
+ )
+ else:
+ self.fonthack_scale = 1
+ else:
+ raise NotImplementedError
+
+ def __del__(self):
+ if sys.platform == "win32":
+ win32gui.DeleteObject(self.pycfont.GetSafeHandle())
+ win32gui.DeleteDC(self.dc)
+
+ def get_metrics(self) -> Tuple[float, float, float, float]:
+ if sys.platform == "win32":
+ const = self.downscale * self.yscale
+ return (
+ # 'height': self.metrics['Height'] * const,
+ self.metrics["Ascent"] * const,
+ self.metrics["Descent"] * const,
+ self.metrics["InternalLeading"] * const,
+ self.metrics["ExternalLeading"] * const,
+ )
+ elif sys.platform == "linux" or sys.platform == "darwin":
+ const = self.downscale * self.yscale * self.fonthack_scale / PANGO_SCALE
+ return (
+ # 'height': (self.metrics.get_ascent() + self.metrics.get_descent()) * const,
+ self.metrics.get_ascent() * const,
+ self.metrics.get_descent() * const,
+ 0.0,
+ self.layout.get_spacing() * const,
+ )
+ else:
+ raise NotImplementedError
+
+ def get_text_extents(self, text: str) -> Tuple[float, float]:
+ if sys.platform == "win32":
+ cx, cy = win32gui.GetTextExtentPoint32(self.dc, text)
+
+ return (
+ (cx * self.downscale + self.hspace * (len(text) - 1)) * self.xscale,
+ cy * self.downscale * self.yscale,
+ )
+ elif sys.platform == "linux" or sys.platform == "darwin":
+ if not text:
+ return 0.0, 0.0
+
+ def get_rect(new_text):
+ self.layout.set_markup(
+ f""
+ f"{html.escape(new_text)}"
+ f"",
+ -1,
+ )
+ return self.layout.get_pixel_extents()[1]
+
+ width = 0
+ for char in text:
+ width += get_rect(char).width
+
+ return (
+ (
+ width * self.downscale * self.fonthack_scale
+ + self.hspace * (len(text) - 1)
+ )
+ * self.xscale,
+ get_rect(text).height
+ * self.downscale
+ * self.yscale
+ * self.fonthack_scale,
+ )
+ else:
+ raise NotImplementedError
+
+ def text_to_shape(self, text: str) -> Shape:
+ if sys.platform == "win32":
+ # TODO: Calcultating distance between origins of character cells (just in case of spacing)
+
+ # Add path to device context
+ win32gui.BeginPath(self.dc)
+ win32gui.ExtTextOut(self.dc, 0, 0, 0x0, None, text)
+ win32gui.EndPath(self.dc)
+ # Getting Path produced by Microsoft API
+ points, type_points = win32gui.GetPath(self.dc)
+
+ # Checking for errors
+ if len(points) == 0 or len(points) != len(type_points):
+ raise RuntimeError(
+ "This should never happen: function win32gui.GetPath has returned something unexpected.\nPlease report this to the developer"
+ )
+
+ # Defining variables
+ shape, last_type = [], None
+ mult_x, mult_y = self.downscale * self.xscale, self.downscale * self.yscale
+
+ # Convert points to shape
+ i = 0
+ while i < len(points):
+ cur_point, cur_type = points[i], type_points[i]
+
+ if cur_type == win32con.PT_MOVETO:
+ if last_type != win32con.PT_MOVETO:
+ # Avoid repetition of command tags
+ shape.append("m")
+ last_type = cur_type
+ shape.extend(
+ [
+ Shape.format_value(cur_point[0] * mult_x),
+ Shape.format_value(cur_point[1] * mult_y),
+ ]
+ )
+ i += 1
+ elif cur_type == win32con.PT_LINETO or cur_type == (
+ win32con.PT_LINETO | win32con.PT_CLOSEFIGURE
+ ):
+ if last_type != win32con.PT_LINETO:
+ # Avoid repetition of command tags
+ shape.append("l")
+ last_type = cur_type
+ shape.extend(
+ [
+ Shape.format_value(cur_point[0] * mult_x),
+ Shape.format_value(cur_point[1] * mult_y),
+ ]
+ )
+ i += 1
+ elif cur_type == win32con.PT_BEZIERTO or cur_type == (
+ win32con.PT_BEZIERTO | win32con.PT_CLOSEFIGURE
+ ):
+ if last_type != win32con.PT_BEZIERTO:
+ # Avoid repetition of command tags
+ shape.append("b")
+ last_type = cur_type
+ shape.extend(
+ [
+ Shape.format_value(cur_point[0] * mult_x),
+ Shape.format_value(cur_point[1] * mult_y),
+ Shape.format_value(points[i + 1][0] * mult_x),
+ Shape.format_value(points[i + 1][1] * mult_y),
+ Shape.format_value(points[i + 2][0] * mult_x),
+ Shape.format_value(points[i + 2][1] * mult_y),
+ ]
+ )
+ i += 3
+ else: # If there is an invalid type -> skip, for safeness
+ i += 1
+
+ # Clear device context path
+ win32gui.AbortPath(self.dc)
+
+ return Shape(" ".join(shape))
+ elif sys.platform == "linux" or sys.platform == "darwin":
+ # Defining variables
+ shape, last_type = [], None
+
+ def shape_from_text(new_text, x_add):
+ nonlocal shape, last_type
+
+ self.layout.set_markup(
+ f""
+ f"{html.escape(new_text)}"
+ f"",
+ -1,
+ )
+
+ self.context.save()
+ self.context.scale(
+ self.downscale * self.xscale * self.fonthack_scale,
+ self.downscale * self.yscale * self.fonthack_scale,
+ )
+ PangoCairo.layout_path(self.context, self.layout)
+ self.context.restore()
+ path = self.context.copy_path()
+
+ # Convert points to shape
+ for current_entry in path:
+ current_type = current_entry[0]
+ current_path = current_entry[1]
+
+ if current_type == 0: # MOVE_TO
+ if last_type != current_type:
+ # Avoid repetition of command tags
+ shape.append("m")
+ last_type = current_type
+ shape.extend(
+ [
+ Shape.format_value(current_path[0] + x_add),
+ Shape.format_value(current_path[1]),
+ ]
+ )
+ elif current_type == 1: # LINE_TO
+ if last_type != current_type:
+ # Avoid repetition of command tags
+ shape.append("l")
+ last_type = current_type
+ shape.extend(
+ [
+ Shape.format_value(current_path[0] + x_add),
+ Shape.format_value(current_path[1]),
+ ]
+ )
+ elif current_type == 2: # CURVE_TO
+ if last_type != current_type:
+ # Avoid repetition of command tags
+ shape.append("b")
+ last_type = current_type
+ shape.extend(
+ [
+ Shape.format_value(current_path[0] + x_add),
+ Shape.format_value(current_path[1]),
+ Shape.format_value(current_path[2] + x_add),
+ Shape.format_value(current_path[3]),
+ Shape.format_value(current_path[4] + x_add),
+ Shape.format_value(current_path[5]),
+ ]
+ )
+
+ self.context.new_path()
+
+ curr_width = 0
+
+ for i, char in enumerate(text):
+ shape_from_text(char, curr_width + self.hspace * self.xscale * i)
+ curr_width += self.get_text_extents(char)[0]
+
+ return Shape(" ".join(shape))
+ else:
+ raise NotImplementedError
diff --git a/syncplay/pyonfx/shape.py b/syncplay/pyonfx/shape.py
new file mode 100644
index 0000000..cff9019
--- /dev/null
+++ b/syncplay/pyonfx/shape.py
@@ -0,0 +1,994 @@
+# -*- coding: utf-8 -*-
+# PyonFX: An easy way to create KFX (Karaoke Effects) and complex typesetting using the ASS format (Advanced Substation Alpha).
+# Copyright (C) 2019 Antonio Strippoli (CoffeeStraw/YellowFlash)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyonFX is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see http://www.gnu.org/licenses/.
+
+from __future__ import annotations
+import math
+from typing import Callable, Optional, List, Tuple, Union
+from pyquaternion import Quaternion
+from inspect import signature
+
+
+class Shape:
+ """
+ This class can be used to define a Shape object (by passing its drawing commands)
+ and then apply functions to it in order to accomplish some tasks, like analyzing its bounding box, apply transformations, splitting curves into segments...
+
+ Args:
+ drawing_cmds (str): The shape's drawing commands in ASS format as a string.
+ """
+
+ def __init__(self, drawing_cmds: str):
+ # Assure that drawing_cmds is a string
+ if not isinstance(drawing_cmds, str):
+ raise TypeError(
+ f"A string containing the shape's drawing commands is expected, but you passed a {type(drawing_cmds)}"
+ )
+ self.drawing_cmds = drawing_cmds
+
+ def __repr__(self):
+ # We return drawing commands as a string rapresentation of the object
+ return self.drawing_cmds
+
+ def __eq__(self, other: Shape):
+ # Method used to compare two shapes
+ return type(other) is type(self) and self.drawing_cmds == other.drawing_cmds
+
+ @staticmethod
+ def format_value(x: float, prec: int = 3) -> str:
+ # Utility function to properly format values for shapes also returning them as a string
+ return f"{x:.{prec}f}".rstrip("0").rstrip(".")
+
+ def has_error(self) -> Union[bool, str]:
+ """Utility function that checks if the shape is valid.
+
+ Returns:
+ False if no error has been found, else a string with the first error encountered.
+ """
+ # Obtain commands and points
+ cad = self.drawing_cmds.split()
+ n = len(cad)
+ mode = ""
+
+ # Prepare usefull lists
+ two_args_cmds = ["m", "n", "l", "p"]
+ six_args_cmds = ["b", "s"]
+
+ # Iterate over commands and points
+ i = 0
+ while i < n:
+ if cad[i] in two_args_cmds:
+ # Check if we have an unexpected and of the shape
+ if n - i < 3:
+ return (
+ f"Unexpected end of shape ('{cad[i]}' expect at least two args)"
+ )
+
+ # Check if we have two numeric values after command
+ try:
+ float(cad[i + 1])
+ float(cad[i + 2])
+ except ValueError:
+ return (
+ f"Expected numeric value at: '{cad[i]} {cad[i+1]} {cad[i+2]}'"
+ )
+
+ # Valid, go on
+ mode = cad[i]
+ i += 3
+ elif cad[i] in six_args_cmds:
+ # Check if we have an unexpected and of the shape
+ if n - i < 7:
+ return (
+ f"Unexpected end of shape ('{cad[i]}' expect at least six args)"
+ )
+
+ # Check if we have six numeric values after command
+ try:
+ for x in range(1, 7):
+ float(cad[i + x])
+ except ValueError:
+ return f"Expected numeric value at: '{cad[i]} {' '.join(cad[i+1:i+7])}'"
+
+ # Valid, go on
+ mode = cad[i]
+ i += 7
+ elif cad[i] == "c":
+ # 'c' expect no arguments, skip
+ mode = ""
+ i += 1
+ elif mode in two_args_cmds:
+ # Check if we have an unexpected and of the shape
+ if n - i < 2:
+ return (
+ f"Unexpected end of shape ('{cad[i]}' expect at least two args)"
+ )
+
+ # Check if we have two numeric values
+ try:
+ float(cad[i])
+ float(cad[i + 1])
+ except ValueError:
+ return (
+ f"Expected numeric value at: '{mode} ... {cad[i]} {cad[i+1]}'"
+ )
+
+ # Valid, go on
+ i += 2
+ elif mode in six_args_cmds:
+ # Check if we have an unexpected and of the shape
+ if n - i < 6:
+ return (
+ f"Unexpected end of shape ('{cad[i]}' expect at least six args)"
+ )
+
+ # Check if we have six numeric values
+ try:
+ for x in range(6):
+ float(cad[i + x])
+ except ValueError:
+ return f"Expected numeric value at: '{mode} ... {' '.join(cad[i:i+6])}'"
+
+ # Valid, go on
+ i += 6
+ else:
+ # Wtf is this?
+ return f"Unexpected command '{cad[i]}'"
+
+ return False
+
+ def map(
+ self, fun: Callable[[float, float, Optional[str]], Tuple[float, float]]
+ ) -> Shape:
+ """Sends every point of a shape through given transformation function to change them.
+
+ **Tips:** *Working with outline points can be used to deform the whole shape and make f.e. a wobble effect.*
+
+ Parameters:
+ fun (function): A function with two (or optionally three) parameters. It will define how each coordinate will be changed. The first two parameters represent the x and y coordinates of each point. The third optional it represents the type of each point (move, line, bezier...).
+
+ Returns:
+ A pointer to the current object.
+
+ Examples:
+ .. code-block:: python3
+
+ original = Shape("m 0 0 l 20 0 20 10 0 10")
+ print ( original.map(lambda x, y: (x+10, y+5) ) )
+
+ >>> m 10 5 l 30 5 30 15 10 15
+ """
+ if not callable(fun):
+ raise TypeError("(Lambda) function expected")
+
+ # Getting all points and commands in a list
+ cmds_and_points = self.drawing_cmds.split()
+ i = 0
+ n = len(cmds_and_points)
+
+ # Checking whether the function take the typ parameter or not
+ if len(signature(fun).parameters) == 2:
+ while i < n:
+ try:
+ # Applying transformation
+ x, y = fun(float(cmds_and_points[i]), float(cmds_and_points[i + 1]))
+ except TypeError:
+ # Values weren't returned, so we don't need to modify them
+ i += 2
+ continue
+ except ValueError:
+ # We have found a string, let's skip this
+ i += 1
+ continue
+ except IndexError:
+ raise ValueError("Unexpected end of the shape")
+
+ # Convert back to string the results for later
+ cmds_and_points[i : i + 2] = (
+ Shape.format_value(x),
+ Shape.format_value(y),
+ )
+ i += 2
+ else:
+ typ = ""
+ while i < n:
+ try:
+ # Applying transformation
+ x, y = fun(
+ float(cmds_and_points[i]), float(cmds_and_points[i + 1]), typ
+ )
+ except TypeError:
+ # Values weren't returned, so we don't need to modify them
+ i += 2
+ continue
+ except ValueError:
+ # We have found a string, let's skip this
+ typ = cmds_and_points[i]
+ i += 1
+ continue
+ except IndexError:
+ raise ValueError("Unexpected end of the shape")
+
+ # Convert back to string the results for later
+ cmds_and_points[i : i + 2] = (
+ Shape.format_value(x),
+ Shape.format_value(y),
+ )
+ i += 2
+
+ # Sew up everything back and update shape
+ self.drawing_cmds = " ".join(cmds_and_points)
+ return self
+
+ def bounding(self) -> Tuple[float, float, float, float]:
+ """Calculates shape bounding box.
+
+ **Tips:** *Using this you can get more precise information about a shape (width, height, position).*
+
+ Returns:
+ A tuple (x0, y0, x1, y1) containing coordinates of the bounding box.
+
+ Examples:
+ .. code-block:: python3
+
+ print("Left-top: %d %d\\nRight-bottom: %d %d" % ( Shape("m 10 5 l 25 5 25 42 10 42").bounding() ) )
+
+ >>> Left-top: 10 5
+ >>> Right-bottom: 25 42
+ """
+
+ # Bounding data
+ x0: float = None
+ y0: float = None
+ x1: float = None
+ y1: float = None
+
+ # Calculate minimal and maximal coordinates
+ def compute_edges(x, y):
+ nonlocal x0, y0, x1, y1
+ if x0 is not None:
+ x0, y0, x1, y1 = min(x0, x), min(y0, y), max(x1, x), max(y1, y)
+ else:
+ x0, y0, x1, y1 = x, y, x, y
+ return x, y
+
+ self.map(compute_edges)
+ return x0, y0, x1, y1
+
+ def move(self, x: float = None, y: float = None) -> Shape:
+ """Moves shape coordinates in given direction.
+
+ | If neither x and y are passed, it will automatically center the shape to the origin (0,0).
+ | This function is an high level function, it just uses Shape.map, which is more advanced. Additionally, it is an easy way to center a shape.
+
+ Parameters:
+ x (int or float): Displacement along the x-axis.
+ y (int or float): Displacement along the y-axis.
+
+ Returns:
+ A pointer to the current object.
+
+ Examples:
+ .. code-block:: python3
+
+ print( Shape("m 0 0 l 30 0 30 20 0 20").move(-5, 10) )
+
+ >>> m -5 10 l 25 10 25 30 -5 30
+ """
+ if x is None and y is None:
+ x, y = [-1 * el for el in self.bounding()[0:2]]
+ elif x is None:
+ x = 0
+ elif y is None:
+ y = 0
+
+ # Update shape
+ self.map(lambda cx, cy: (cx + x, cy + y))
+ return self
+
+ def flatten(self, tolerance: float = 1.0) -> Shape:
+ """Splits shape's bezier curves into lines.
+
+ | This is a low level function. Instead, you should use :func:`split` which already calls this function.
+
+ Parameters:
+ tolerance (float): Angle in degree to define a curve as flat (increasing it will boost performance during reproduction, but lower accuracy)
+
+ Returns:
+ A pointer to the current object.
+
+ Returns:
+ The shape as a string, with bezier curves converted to lines.
+ """
+ # TO DO: Make this function iterative, recursion is bad.
+ if tolerance < 0:
+ raise ValueError("Tolerance must be a positive number")
+
+ # Inner functions definitions
+ # 4th degree curve subdivider (De Casteljau)
+ def curve4_subdivide(
+ x0,
+ y0,
+ x1,
+ y1,
+ x2,
+ y2,
+ x3,
+ y3,
+ pct,
+ ):
+ # Calculate points on curve vectors
+ x01, y01, x12, y12, x23, y23 = (
+ (x0 + x1) * pct,
+ (y0 + y1) * pct,
+ (x1 + x2) * pct,
+ (y1 + y2) * pct,
+ (x2 + x3) * pct,
+ (y2 + y3) * pct,
+ )
+ x012, y012, x123, y123 = (
+ (x01 + x12) * pct,
+ (y01 + y12) * pct,
+ (x12 + x23) * pct,
+ (y12 + y23) * pct,
+ )
+ x0123, y0123 = (x012 + x123) * pct, (y012 + y123) * pct
+ # Return new 2 curves
+ return (
+ x0,
+ y0,
+ x01,
+ y01,
+ x012,
+ y012,
+ x0123,
+ y0123,
+ x0123,
+ y0123,
+ x123,
+ y123,
+ x23,
+ y23,
+ x3,
+ y3,
+ )
+
+ # Check flatness of 4th degree curve with angles
+ def curve4_is_flat(
+ x0,
+ y0,
+ x1,
+ y1,
+ x2,
+ y2,
+ x3,
+ y3,
+ ):
+ # Pack curve vectors (only ones non zero)
+ vecs = [[x1 - x0, y1 - y0], [x2 - x1, y2 - y1], [x3 - x2, y3 - y2]]
+ vecs = [el for el in vecs if not (el[0] == 0 and el[1] == 0)]
+
+ # Inner functions to calculate degrees between two 2d vectors
+ def dotproduct(v1, v2):
+ return sum((a * b) for a, b in zip(v1, v2))
+
+ def length(v):
+ return math.sqrt(dotproduct(v, v))
+
+ def get_angle(v1, v2):
+ calc = max(
+ min(dotproduct(v1, v2) / (length(v1) * length(v2)), 1), -1
+ ) # Clamping value to prevent errors
+ angle = math.degrees(math.acos(calc))
+ if (v1[0] * v2[1] - v1[1] * v2[0]) < 0:
+ return -angle
+ return angle
+
+ # Check flatness on vectors
+ for i in range(1, len(vecs)):
+ if abs(get_angle(vecs[i - 1], vecs[i])) > tolerance:
+ return False
+ return True
+
+ # Inner function to convert 4th degree curve to line points
+ def curve4_to_lines(
+ x0,
+ y0,
+ x1,
+ y1,
+ x2,
+ y2,
+ x3,
+ y3,
+ ):
+ # Line points buffer
+ pts = ""
+
+ # Conversion in recursive processing
+ def convert_recursive(x0, y0, x1, y1, x2, y2, x3, y3):
+ if curve4_is_flat(x0, y0, x1, y1, x2, y2, x3, y3):
+ nonlocal pts
+ x3, y3 = Shape.format_value(x3), Shape.format_value(y3)
+ pts += f"{x3} {y3} "
+ return
+
+ (
+ x10,
+ y10,
+ x11,
+ y11,
+ x12,
+ y12,
+ x13,
+ y13,
+ x20,
+ y20,
+ x21,
+ y21,
+ x22,
+ y22,
+ x23,
+ y23,
+ ) = curve4_subdivide(x0, y0, x1, y1, x2, y2, x3, y3, 0.5)
+ convert_recursive(x10, y10, x11, y11, x12, y12, x13, y13)
+ convert_recursive(x20, y20, x21, y21, x22, y22, x23, y23)
+
+ # Splitting curve recursively until we're not satisfied (angle <= tolerance)
+ convert_recursive(x0, y0, x1, y1, x2, y2, x3, y3)
+ # Return resulting points
+ return " ".join(
+ pts[:-1].split(" ")[:-2]
+ ) # Delete last space and last two float values
+
+ # Getting all points and commands in a list
+ cmds_and_points = self.drawing_cmds.split()
+ i = 0
+ n = len(cmds_and_points)
+
+ # Scanning all commands and points (improvable)
+ while i < n:
+ if (
+ cmds_and_points[i] == "b"
+ ): # We've found a curve, let's split it into lines
+ try:
+ # Getting all the points: if we don't have exactly 8 points, shape is not valid
+ x0, y0 = (
+ float(cmds_and_points[i - 2]),
+ float(cmds_and_points[i - 1]),
+ )
+ x1, y1 = (
+ float(cmds_and_points[i + 1]),
+ float(cmds_and_points[i + 2]),
+ )
+ x2, y2 = (
+ float(cmds_and_points[i + 3]),
+ float(cmds_and_points[i + 4]),
+ )
+ x3, y3 = (
+ float(cmds_and_points[i + 5]),
+ float(cmds_and_points[i + 6]),
+ )
+ except IndexError:
+ raise ValueError(
+ "Shape providen is not valid (not enough points for a curve)"
+ )
+
+ # Obtaining the converted curve and saving it for later
+ cmds_and_points[i] = "l"
+ cmds_and_points[i + 1] = curve4_to_lines(x0, y0, x1, y1, x2, y2, x3, y3)
+
+ i += 2
+ n -= 3
+
+ # Deleting the remaining points
+ for _ in range(3):
+ del cmds_and_points[i]
+
+ # Going to the next point
+ i += 2
+
+ # Check if we're at the end of the shape
+ if i < n:
+ # Check for implicit bezier curve
+ try:
+ float(cmds_and_points[i]) # Next number is a float?
+ cmds_and_points.insert(i, "b")
+ n += 1
+ except ValueError:
+ pass
+ elif cmds_and_points[i] == "c": # Deleting c tag?
+ del cmds_and_points[i]
+ n -= 1
+ else:
+ i += 1
+
+ # Update shape
+ self.drawing_cmds = " ".join(cmds_and_points)
+ return self
+
+ def split(self, max_len: float = 16, tolerance: float = 1.0) -> Shape:
+ """Splits shape bezier curves into lines and splits lines into shorter segments with maximum given length.
+
+ **Tips:** *You can call this before using :func:`map` to work with more outline points for smoother deforming.*
+
+ Parameters:
+ max_len (int or float): The max length that you want all the lines to be
+ tolerance (float): Angle in degree to define a bezier curve as flat (increasing it will boost performance during reproduction, but lower accuracy)
+
+ Returns:
+ A pointer to the current object.
+
+ Examples:
+ .. code-block:: python3
+
+ print( Shape("m -100.5 0 l 100 0 b 100 100 -100 100 -100.5 0 c").split() )
+
+ >>> m -100.5 0 l -100 0 -90 0 -80 0 -70 0 -60 0 -50 0 -40 0 -30 0 -20 0 -10 0 0 0 10 0 20 0 30 0 40 0 50 0 60 0 70 0 80 0 90 0 100 0 l 99.964 2.325 99.855 4.614 99.676 6.866 99.426 9.082 99.108 11.261 98.723 13.403 98.271 15.509 97.754 17.578 97.173 19.611 96.528 21.606 95.822 23.566 95.056 25.488 94.23 27.374 93.345 29.224 92.403 31.036 91.405 32.812 90.352 34.552 89.246 36.255 88.086 37.921 86.876 39.551 85.614 41.144 84.304 42.7 82.945 44.22 81.54 45.703 80.088 47.15 78.592 48.56 77.053 49.933 75.471 51.27 73.848 52.57 72.184 53.833 70.482 55.06 68.742 56.25 66.965 57.404 65.153 58.521 63.307 59.601 61.427 60.645 59.515 61.652 57.572 62.622 55.599 63.556 53.598 64.453 51.569 65.314 49.514 66.138 47.433 66.925 45.329 67.676 43.201 68.39 41.052 69.067 38.882 69.708 36.692 70.312 34.484 70.88 32.259 71.411 27.762 72.363 23.209 73.169 18.61 73.828 13.975 74.341 9.311 74.707 4.629 74.927 -0.062 75 -4.755 74.927 -9.438 74.707 -14.103 74.341 -18.741 73.828 -23.343 73.169 -27.9 72.363 -32.402 71.411 -34.63 70.88 -36.841 70.312 -39.033 69.708 -41.207 69.067 -43.359 68.39 -45.49 67.676 -47.599 66.925 -49.683 66.138 -51.743 65.314 -53.776 64.453 -55.782 63.556 -57.759 62.622 -59.707 61.652 -61.624 60.645 -63.509 59.601 -65.361 58.521 -67.178 57.404 -68.961 56.25 -70.707 55.06 -72.415 53.833 -74.085 52.57 -75.714 51.27 -77.303 49.933 -78.85 48.56 -80.353 47.15 -81.811 45.703 -83.224 44.22 -84.59 42.7 -85.909 41.144 -87.178 39.551 -88.397 37.921 -89.564 36.255 -90.68 34.552 -91.741 32.812 -92.748 31.036 -93.699 29.224 -94.593 27.374 -95.428 25.488 -96.205 23.566 -96.92 21.606 -97.575 19.611 -98.166 17.578 -98.693 15.509 -99.156 13.403 -99.552 11.261 -99.881 9.082 -100.141 6.866 -100.332 4.614 -100.452 2.325 -100.5 0
+ """
+ if max_len <= 0:
+ raise ValueError(
+ "The length of segments must be a positive and non-zero value"
+ )
+
+ # Internal function to help splitting a line
+ def line_split(x0: float, y0: float, x1: float, y1: float):
+ x0, y0, x1, y1 = float(x0), float(y0), float(x1), float(y1)
+ # Line direction & length
+ rel_x, rel_y = x1 - x0, y1 - y0
+ distance = math.sqrt(rel_x * rel_x + rel_y * rel_y)
+ # If the line is too long -> split
+ if distance > max_len:
+ lines: list[str] = []
+ distance_rest = distance % max_len
+ cur_distance = distance_rest if distance_rest > 0 else max_len
+
+ while cur_distance <= distance:
+ pct = cur_distance / distance
+ x, y = (
+ Shape.format_value(x0 + rel_x * pct),
+ Shape.format_value(y0 + rel_y * pct),
+ )
+
+ lines.append(f"{x} {y}")
+ cur_distance += max_len
+
+ return " ".join(lines), lines[-1].split()
+ else: # No line split
+ x1, y1 = Shape.format_value(x1), Shape.format_value(y1)
+ return f"{x1} {y1}", [x1, y1]
+
+ # Getting all points and commands in a list
+ cmds_and_points = self.flatten().drawing_cmds.split()
+ i = 0
+ n = len(cmds_and_points)
+
+ # Utility variables
+ is_line = False
+ previous_two = None
+ last_move = None
+
+ # Splitting everything splittable, probably improvable
+ while i < n:
+ current = cmds_and_points[i]
+ if current == "l":
+ # Activate line mode, save previous two points
+ is_line = True
+ if (
+ not previous_two
+ ): # If we're not running into contiguous line, we need to save the previous two
+ previous_two = [cmds_and_points[i - 2], cmds_and_points[i - 1]]
+ i += 1
+ elif (
+ current == "m"
+ or current == "n"
+ or current == "b"
+ or current == "s"
+ or current == "p"
+ or current == "c"
+ ):
+ if current == "m":
+ if (
+ last_move
+ ): # If we had a previous move, we need to close the previous figure before proceding
+ x0, y0 = None, None
+ if (
+ previous_two
+ ): # If I don't have previous point, I can read them on cmds_and_points, else I wil take 'em
+ x0, y0 = previous_two[0], previous_two[1]
+ else:
+ x0, y0 = cmds_and_points[i - 2], cmds_and_points[i - 1]
+
+ if not (
+ x0 == last_move[0] and y0 == last_move[1]
+ ): # Closing last figure
+ cmds_and_points[i] = (
+ line_split(x0, y0, last_move[0], last_move[1])[0] + " m"
+ )
+ last_move = [cmds_and_points[i + 1], cmds_and_points[i + 2]]
+
+ # Disabling line mode, removing previous two points
+ is_line = False
+ previous_two = None
+ i += 1
+ elif is_line:
+ # Do the work with the two points found and the previous two
+ cmds_and_points[i], previous_two = line_split(
+ previous_two[0],
+ previous_two[1],
+ cmds_and_points[i],
+ cmds_and_points[i + 1],
+ )
+ del cmds_and_points[i + 1]
+ # Let's go to the next two points or tag
+ i += 1
+ n -= 1
+ else: # We're working with points that are not lines points, let's go forward
+ i += 2
+
+ # Close last figure of new shape, taking two last points and two last points of move
+ i = n
+ if not previous_two:
+ while i >= 0:
+ current = cmds_and_points[i]
+ current_prev = cmds_and_points[i - 1]
+ if (
+ current != "m"
+ and current != "n"
+ and current != "b"
+ and current != "s"
+ and current != "p"
+ and current != "c"
+ and current_prev != "m"
+ and current_prev != "n"
+ and current_prev != "b"
+ and current_prev != "s"
+ and current_prev != "p"
+ and current_prev != "c"
+ ):
+ previous_two = [current, current_prev]
+ break
+ i -= 1
+ if not (
+ previous_two[0] == last_move[0] and previous_two[1] == last_move[1]
+ ): # Split!
+ cmds_and_points.append(
+ "l "
+ + line_split(
+ previous_two[0], previous_two[1], last_move[0], last_move[1]
+ )[0]
+ )
+
+ # Sew up everything back and update shape
+ self.drawing_cmds = " ".join(cmds_and_points)
+ return self
+
+ def __to_outline(
+ self, bord_xy: float, bord_y: float = None, mode: str = "round"
+ ) -> Shape:
+ """Converts shape command for filling to a shape command for stroking.
+
+ **Tips:** *You could use this for border textures.*
+
+ Parameters:
+ shape (str): The shape in ASS format as a string.
+
+ Returns:
+ A pointer to the current object.
+
+ Returns:
+ A new shape as string, representing the border of the input.
+ """
+ raise NotImplementedError
+
+ @staticmethod
+ def ring(out_r: float, in_r: float) -> Shape:
+ """Returns a shape object of a ring with given inner and outer radius, centered around (0,0).
+
+ **Tips:** *A ring with increasing inner radius, starting from 0, can look like an outfading point.*
+
+ Parameters:
+ out_r (int or float): The outer radius for the ring.
+ in_r (int or float): The inner radius for the ring.
+
+ Returns:
+ A shape object representing a ring.
+ """
+ try:
+ out_r2, in_r2 = out_r * 2, in_r * 2
+ off = out_r - in_r
+ off_in_r = off + in_r
+ off_in_r2 = off + in_r2
+ except TypeError:
+ raise TypeError("Number(s) expected")
+
+ if in_r >= out_r:
+ raise ValueError(
+ "Valid number expected. Inner radius must be less than outer radius"
+ )
+
+ f = Shape.format_value
+ return Shape(
+ "m 0 %s "
+ "b 0 %s 0 0 %s 0 "
+ "%s 0 %s 0 %s %s "
+ "%s %s %s %s %s %s "
+ "%s %s 0 %s 0 %s "
+ "m %s %s "
+ "b %s %s %s %s %s %s "
+ "%s %s %s %s %s %s "
+ "%s %s %s %s %s %s "
+ "%s %s %s %s %s %s"
+ % (
+ f(out_r), # outer move
+ f(out_r),
+ f(out_r), # outer curve 1
+ f(out_r),
+ f(out_r2),
+ f(out_r2),
+ f(out_r), # outer curve 2
+ f(out_r2),
+ f(out_r),
+ f(out_r2),
+ f(out_r2),
+ f(out_r),
+ f(out_r2), # outer curve 3
+ f(out_r),
+ f(out_r2),
+ f(out_r2),
+ f(out_r), # outer curve 4
+ f(off),
+ f(off_in_r), # inner move
+ f(off),
+ f(off_in_r),
+ f(off),
+ f(off_in_r2),
+ f(off_in_r),
+ f(off_in_r2), # inner curve 1
+ f(off_in_r),
+ f(off_in_r2),
+ f(off_in_r2),
+ f(off_in_r2),
+ f(off_in_r2),
+ f(off_in_r), # inner curve 2
+ f(off_in_r2),
+ f(off_in_r),
+ f(off_in_r2),
+ f(off),
+ f(off_in_r),
+ f(off), # inner curve 3
+ f(off_in_r),
+ f(off),
+ f(off),
+ f(off),
+ f(off),
+ f(off_in_r), # inner curve 4
+ )
+ )
+
+ @staticmethod
+ def ellipse(w: float, h: float) -> Shape:
+ """Returns a shape object of an ellipse with given width and height, centered around (0,0).
+
+ **Tips:** *You could use that to create rounded stribes or arcs in combination with blurring for light effects.*
+
+ Parameters:
+ w (int or float): The width for the ellipse.
+ h (int or float): The height for the ellipse.
+
+ Returns:
+ A shape object representing an ellipse.
+ """
+ try:
+ w2, h2 = w / 2, h / 2
+ except TypeError:
+ raise TypeError("Number(s) expected")
+
+ f = Shape.format_value
+
+ return Shape(
+ "m 0 %s "
+ "b 0 %s 0 0 %s 0 "
+ "%s 0 %s 0 %s %s "
+ "%s %s %s %s %s %s "
+ "%s %s 0 %s 0 %s"
+ % (
+ f(h2), # move
+ f(h2),
+ f(w2), # curve 1
+ f(w2),
+ f(w),
+ f(w),
+ f(h2), # curve 2
+ f(w),
+ f(h2),
+ f(w),
+ f(h),
+ f(w2),
+ f(h), # curve 3
+ f(w2),
+ f(h),
+ f(h),
+ f(h2), # curve 4
+ )
+ )
+
+ @staticmethod
+ def heart(size: float, offset: float = 0) -> Shape:
+ """Returns a shape object of a heart object with given size (width&height) and vertical offset of center point, centered around (0,0).
+
+ **Tips:** *An offset=size*(2/3) results in a splitted heart.*
+
+ Parameters:
+ size (int or float): The width&height for the heart.
+ offset (int or float): The vertical offset of center point.
+
+ Returns:
+ A shape object representing an heart.
+ """
+ try:
+ mult = size / 30
+ except TypeError:
+ raise TypeError("Size parameter must be a number")
+ # Build shape from template
+ shape = Shape(
+ "m 15 30 b 27 22 30 18 30 14 30 8 22 0 15 10 8 0 0 8 0 14 0 18 3 22 15 30"
+ ).map(lambda x, y: (x * mult, y * mult))
+
+ # Shift mid point of heart vertically
+ count = 0
+
+ def shift_mid_point(x, y):
+ nonlocal count
+ count += 1
+
+ if count == 7:
+ try:
+ return x, y + offset
+ except TypeError:
+ raise TypeError("Offset parameter must be a number")
+ return x, y
+
+ # Return result
+ return shape.map(shift_mid_point)
+
+ @staticmethod
+ def __glance_or_star(
+ edges: int, inner_size: float, outer_size: float, g_or_s: str
+ ) -> Shape:
+ """
+ General function to create a shape object representing star or glance.
+ """
+ # Alias for utility functions
+ f = Shape.format_value
+
+ def rotate_on_axis_z(point, theta):
+ # Internal function to rotate a point around z axis by a given angle.
+ theta = math.radians(theta)
+ return Quaternion(axis=[0, 0, 1], angle=theta).rotate(point)
+
+ # Building shape
+ shape = ["m 0 %s %s" % (-outer_size, g_or_s)]
+ inner_p, outer_p = 0, 0
+
+ for i in range(1, edges + 1):
+ # Inner edge
+ inner_p = rotate_on_axis_z([0, -inner_size, 0], ((i - 0.5) / edges) * 360)
+ # Outer edge
+ outer_p = rotate_on_axis_z([0, -outer_size, 0], (i / edges) * 360)
+ # Add curve / line
+ if g_or_s == "l":
+ shape.append(
+ "%s %s %s %s"
+ % (f(inner_p[0]), f(inner_p[1]), f(outer_p[0]), f(outer_p[1]))
+ )
+ else:
+ shape.append(
+ "%s %s %s %s %s %s"
+ % (
+ f(inner_p[0]),
+ f(inner_p[1]),
+ f(inner_p[0]),
+ f(inner_p[1]),
+ f(outer_p[0]),
+ f(outer_p[1]),
+ )
+ )
+
+ shape = Shape(" ".join(shape))
+
+ # Return result centered
+ return shape.move()
+
+ @staticmethod
+ def star(edges: int, inner_size: float, outer_size: float) -> Shape:
+ """Returns a shape object of a star object with given number of outer edges and sizes, centered around (0,0).
+
+ **Tips:** *Different numbers of edges and edge distances allow individual n-angles.*
+
+ Parameters:
+ edges (int): The number of edges of the star.
+ inner_size (int or float): The inner edges distance from center.
+ outer_size (int or float): The outer edges distance from center.
+
+ Returns:
+ A shape object as a string representing a star.
+ """
+ return Shape.__glance_or_star(edges, inner_size, outer_size, "l")
+
+ @staticmethod
+ def glance(edges: int, inner_size: float, outer_size: float) -> Shape:
+ """Returns a shape object of a glance object with given number of outer edges and sizes, centered around (0,0).
+
+ **Tips:** *Glance is similar to Star, but with curves instead of inner edges between the outer edges.*
+
+ Parameters:
+ edges (int): The number of edges of the star.
+ inner_size (int or float): The inner edges distance from center.
+ outer_size (int or float): The control points for bezier curves between edges distance from center.
+
+ Returns:
+ A shape object as a string representing a glance.
+ """
+ return Shape.__glance_or_star(edges, inner_size, outer_size, "b")
+
+ @staticmethod
+ def rectangle(w: float = 1.0, h: float = 1.0) -> Shape:
+ """Returns a shape object of a rectangle with given width and height, centered around (0,0).
+
+ **Tips:** *A rectangle with width=1 and height=1 is a pixel.*
+
+ Parameters:
+ w (int or float): The width for the rectangle.
+ h (int or float): The height for the rectangle.
+
+ Returns:
+ A shape object representing an rectangle.
+ """
+ try:
+ f = Shape.format_value
+ return Shape("m 0 0 l %s 0 %s %s 0 %s 0 0" % (f(w), f(w), f(h), f(h)))
+ except TypeError:
+ raise TypeError("Number(s) expected")
+
+ @staticmethod
+ def triangle(size: float) -> Shape:
+ """Returns a shape object of an equilateral triangle with given side length, centered around (0,0).
+
+ Parameters:
+ size (int or float): The side length for the triangle.
+
+ Returns:
+ A shape object representing an triangle.
+ """
+ try:
+ h = math.sqrt(3) * size / 2
+ base = -h / 6
+ except TypeError:
+ raise TypeError("Number expected")
+
+ f = Shape.format_value
+ return Shape(
+ "m %s %s l %s %s 0 %s %s %s"
+ % (
+ f(size / 2),
+ f(base),
+ f(size),
+ f(base + h),
+ f(base + h),
+ f(size / 2),
+ f(base),
+ )
+ )
diff --git a/syncplay/pyonfx/utils.py b/syncplay/pyonfx/utils.py
new file mode 100644
index 0000000..eb1bf3f
--- /dev/null
+++ b/syncplay/pyonfx/utils.py
@@ -0,0 +1,608 @@
+# -*- coding: utf-8 -*-
+# PyonFX: An easy way to create KFX (Karaoke Effects) and complex typesetting using the ASS format (Advanced Substation Alpha).
+# Copyright (C) 2019 Antonio Strippoli (CoffeeStraw/YellowFlash)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyonFX is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see http://www.gnu.org/licenses/.
+
+from __future__ import annotations
+import math
+import re
+from typing import List, Union, TYPE_CHECKING
+
+from .convert import Convert, ColorModel
+
+if TYPE_CHECKING:
+ from .ass_core import Line, Word, Syllable, Char
+
+
+class Utils:
+ """
+ This class is a collection of static methods that will help the user in some tasks.
+ """
+
+ @staticmethod
+ def all_non_empty(
+ lines_chars_syls_or_words: List[Union[Line, Word, Syllable, Char]]
+ ) -> List[Union[Line, Word, Syllable, Char]]:
+ """
+ Helps to not check everytime for text containing only spaces or object's duration equals to zero.
+
+ Parameters:
+ lines_chars_syls_or_words (list of :class:`Line`, :class:`Char`, :class:`Syllable` or :class:`Word`)
+
+ Returns:
+ A list containing lines_chars_syls_or_words without objects with duration equals to zero or blank text (no text or only spaces).
+ """
+ out = []
+ for obj in lines_chars_syls_or_words:
+ if obj.text.strip() and obj.duration > 0:
+ out.append(obj)
+ return out
+
+ @staticmethod
+ def clean_tags(text: str) -> str:
+ # TODO: Cleans up ASS subtitle lines of badly-formed override. Returns a cleaned up text.
+ pass
+
+ @staticmethod
+ def accelerate(pct: float, accelerator: float) -> float:
+ # Modifies pct according to the acceleration provided.
+ # TODO: Implement acceleration based on bezier's curve
+ return pct ** accelerator
+
+ @staticmethod
+ def interpolate(
+ pct: float,
+ val1: Union[float, str],
+ val2: Union[float, str],
+ acc: float = 1.0,
+ ) -> Union[str, float]:
+ """
+ | Interpolates 2 given values (ASS colors, ASS alpha channels or numbers) by percent value as decimal number.
+ | You can also provide a http://cubic-bezier.com to accelerate based on bezier curves. (TO DO)
+ |
+ | You could use that for the calculation of color/alpha gradients.
+
+ Parameters:
+ pct (float): Percent value of the interpolation.
+ val1 (int, float or str): First value to interpolate (either string or number).
+ val2 (int, float or str): Second value to interpolate (either string or number).
+ acc (float, optional): Optional acceleration that influences final percent value.
+
+ Returns:
+ Interpolated value of given 2 values (so either a string or a number).
+
+ Examples:
+ .. code-block:: python3
+
+ print( Utils.interpolate(0.5, 10, 20) )
+ print( Utils.interpolate(0.9, "&HFFFFFF&", "&H000000&") )
+
+ >>> 15
+ >>> &HE5E5E5&
+ """
+ if pct > 1.0 or pct < 0:
+ raise ValueError(
+ f"Percent value must be a float between 0.0 and 1.0, but yours was {pct}"
+ )
+
+ # Calculating acceleration (if requested)
+ pct = Utils.accelerate(pct, acc) if acc != 1.0 else pct
+
+ def interpolate_numbers(val1, val2):
+ nonlocal pct
+ return val1 + (val2 - val1) * pct
+
+ # Interpolating
+ if type(val1) is str and type(val2) is str:
+ if len(val1) != len(val2):
+ raise ValueError(
+ "ASS values must have the same type (either two alphas, two colors or two colors+alpha)."
+ )
+ if len(val1) == len("&HXX&"):
+ val1 = Convert.alpha_ass_to_dec(val1)
+ val2 = Convert.alpha_ass_to_dec(val2)
+ a = interpolate_numbers(val1, val2)
+ return Convert.alpha_dec_to_ass(a)
+ elif len(val1) == len("&HBBGGRR&"):
+ val1 = Convert.color_ass_to_rgb(val1)
+ val2 = Convert.color_ass_to_rgb(val2)
+ rgb = tuple(map(interpolate_numbers, val1, val2))
+ return Convert.color_rgb_to_ass(rgb)
+ elif len(val1) == len("&HAABBGGRR"):
+ val1 = Convert.color(val1, ColorModel.ASS, ColorModel.RGBA)
+ val2 = Convert.color(val2, ColorModel.ASS, ColorModel.RGBA)
+ rgba = tuple(map(interpolate_numbers, val1, val2))
+ return Convert.color(rgba, ColorModel.RGBA, ColorModel.ASS)
+ else:
+ raise ValueError(
+ f"Provided inputs '{val1}' and '{val2}' are not valid ASS strings."
+ )
+ elif type(val1) in [int, float] and type(val2) in [int, float]:
+ return interpolate_numbers(val1, val2)
+ else:
+ raise TypeError(
+ "Invalid input(s) type, either pass two strings or two numbers."
+ )
+
+
+class FrameUtility:
+ """
+ This class helps in the stressful calculation of frames per frame.
+
+ Parameters:
+ start_time (positive float): Initial time
+ end_time (positive float): Final time
+ fr (positive float, optional): Frame Duration
+
+ Returns:
+ Returns a Generator containing start_time, end_time, index and total number of frames for each step.
+
+ Examples:
+ .. code-block:: python3
+ :emphasize-lines: 1
+
+ FU = FrameUtility(0, 100)
+ for s, e, i, n in FU:
+ print(f"Frame {i}/{n}: {s} - {e}")
+
+ >>> Frame 1/3: 0 - 41.71
+ >>> Frame 2/3: 41.71 - 83.42
+ >>> Frame 3/3: 83.42 - 100
+
+ """
+
+ def __init__(self, start_time: float, end_time: float, fr: float = 41.71):
+ # Checking for invalid values
+ if start_time < 0 or end_time < 0 or fr <= 0 or end_time < start_time:
+ raise ValueError("Positive values and/or end_time > start_time expected.")
+
+ # Calculating number of frames
+ self.n = math.ceil((end_time - start_time) / fr)
+
+ # Defining fields
+ self.start_time = start_time
+ self.end_time = end_time
+ self.current_time = fr
+ self.fr = fr
+
+ def __iter__(self):
+ # For loop for the first n-1 frames
+ for i in range(1, self.n):
+ yield (
+ round(self.start_time, 2),
+ round(self.start_time + self.fr, 2),
+ i,
+ self.n,
+ )
+ self.start_time += self.fr
+ self.current_time += self.fr
+
+ # Last frame, with end value clamped at end_time
+ yield (round(self.start_time, 2), round(self.end_time, 2), self.n, self.n)
+
+ # Resetting to make this object usable again
+ self.start_time = self.start_time - self.fr * max(self.n - 1, 0)
+ self.current_time = self.fr
+
+ def add(
+ self,
+ start_time: int,
+ end_time: int,
+ end_value: float,
+ accelerator: float = 1.0,
+ ) -> float:
+ """
+ This function makes a lot easier the calculation of tags value.
+ You can see this as a \"\\t\" tag usable in frame per frame operations.
+ Use it in a for loop which iterates a FrameUtility object, as you can see in the example.
+
+ Parameters:
+ start_time (int): Initial time
+ end_time (int): Final time
+ end_value (int or float): Value reached at end_time
+ accelerator (float): Accelerator value
+
+ Examples:
+ .. code-block:: python3
+ :emphasize-lines: 4,5
+
+ FU = FrameUtility(0, 105, 40)
+ for s, e, i, n in FU:
+ fsc = 100
+ fsc += FU.add(0, 50, 50)
+ fsc += FU.add(50, 100, -50)
+ print(f"Frame {i}/{n}: {s} - {e}; fsc: {fsc}")
+
+ >>> Frame 1/3: 0 - 40; fsc: 140.0
+ >>> Frame 2/3: 40 - 80; fsc: 120.0
+ >>> Frame 3/3: 80 - 105; fsc: 100
+ """
+
+ if self.current_time < start_time:
+ return 0
+ elif self.current_time > end_time:
+ return end_value
+
+ pstart = self.current_time - start_time
+ pend = end_time - start_time
+ return Utils.interpolate(pstart / pend, 0, end_value, accelerator)
+
+
+class ColorUtility:
+ """
+ This class helps to obtain all the color transformations written in a list of lines
+ (usually all the lines of your input .ass)
+ to later retrieve all of those transformations that fit between the start_time and end_time of a line passed,
+ without having to worry about interpolating times or other stressfull tasks.
+
+ It is highly suggested to create this object just one time in your script, for performance reasons.
+
+ Note:
+ A few notes about the color transformations in your lines:
+
+ * Every color-tag has to be in the format of ``c&Hxxxxxx&``, do not forget the last &;
+ * You can put color changes without using transformations, like ``{\\1c&HFFFFFF&\\3c&H000000&}Test``, but those will be interpreted as ``{\\t(0,0,\\1c&HFFFFFF&\\3c&H000000&)}Test``;
+ * For an example of how color changes should be put in your lines, check `this `_.
+
+ Also, it is important to remember that **color changes in your lines are treated as if they were continuous**.
+
+ For example, let's assume we have two lines:
+
+ #. ``{\\1c&HFFFFFF&\\t(100,150,\\1c&H000000&)}Line1``, starting at 0ms, ending at 100ms;
+ #. ``{}Line2``, starting at 100ms, ending at 200ms.
+
+ Even if the second line **doesn't have any color changes** and you would expect to have the style's colors,
+ **it will be treated as it has** ``\\1c&H000000&``. That could seem strange at first,
+ but thinking about your generated lines, **the majority** will have **start_time and end_time different** from the ones of your original file.
+
+ Treating transformations as if they were continous, **ColorUtility will always know the right colors** to pick for you.
+ Also, remember that even if you can't always see them directly on Aegisub, you can use transformations
+ with negative times or with times that exceed line total duration.
+
+ Parameters:
+ lines (list of Line): List of lines to be parsed
+ offset (integer, optional): Milliseconds you may want to shift all the color changes
+
+ Returns:
+ Returns a ColorUtility object.
+
+ Examples:
+ .. code-block:: python3
+ :emphasize-lines: 2, 4
+
+ # Parsing all the lines in the file
+ CU = ColorUtility(lines)
+ # Parsing just a single line (the first in this case) in the file
+ CU = ColorUtility([ line[0] ])
+ """
+
+ def __init__(self, lines: List[Line], offset: int = 0):
+ self.color_changes = []
+ self.c1_req = False
+ self.c3_req = False
+ self.c4_req = False
+
+ # Compiling regex
+ tag_all = re.compile(r"{.*?}")
+ tag_t = re.compile(r"\\t\( *?(-?\d+?) *?, *?(-?\d+?) *?, *(.+?) *?\)")
+ tag_c1 = re.compile(r"\\1c(&H.{6}&)")
+ tag_c3 = re.compile(r"\\3c(&H.{6}&)")
+ tag_c4 = re.compile(r"\\4c(&H.{6}&)")
+
+ for line in lines:
+ # Obtaining all tags enclosured in curly brackets
+ tags = tag_all.findall(line.raw_text)
+
+ # Let's search all color changes in the tags
+ for tag in tags:
+ # Get everything beside \t to see if there are some colors there
+ other_tags = tag_t.sub("", tag)
+
+ # Searching for colors in the other tags
+ c1, c3, c4 = (
+ tag_c1.search(other_tags),
+ tag_c3.search(other_tags),
+ tag_c4.search(other_tags),
+ )
+
+ # If we found something, add to the list as a color change
+ if c1 or c3 or c4:
+ if c1:
+ c1 = c1.group(0)
+ self.c1_req = True
+ if c3:
+ c3 = c3.group(0)
+ self.c3_req = True
+ if c4:
+ c4 = c4.group(0)
+ self.c4_req = True
+
+ self.color_changes.append(
+ {
+ "start": line.start_time + offset,
+ "end": line.start_time + offset,
+ "acc": 1,
+ "c1": c1,
+ "c3": c3,
+ "c4": c4,
+ }
+ )
+
+ # Find all transformation in tag
+ ts = tag_t.findall(tag)
+
+ # Working with each transformation
+ for t in ts:
+ # Parsing start, end, optional acceleration and colors
+ start, end, acc_colors = int(t[0]), int(t[1]), t[2].split(",")
+ acc, c1, c3, c4 = 1, None, None, None
+
+ # Do we have also acceleration?
+ if len(acc_colors) == 1:
+ c1, c3, c4 = (
+ tag_c1.search(acc_colors[0]),
+ tag_c3.search(acc_colors[0]),
+ tag_c4.search(acc_colors[0]),
+ )
+ elif len(acc_colors) == 2:
+ acc = float(acc_colors[0])
+ c1, c3, c4 = (
+ tag_c1.search(acc_colors[1]),
+ tag_c3.search(acc_colors[1]),
+ tag_c4.search(acc_colors[1]),
+ )
+ else:
+ # This transformation is malformed (too many ','), let's skip this
+ continue
+
+ # If found, extract from groups
+ if c1:
+ c1 = c1.group(0)
+ self.c1_req = True
+ if c3:
+ c3 = c3.group(0)
+ self.c3_req = True
+ if c4:
+ c4 = c4.group(0)
+ self.c4_req = True
+
+ # Saving in the list
+ self.color_changes.append(
+ {
+ "start": line.start_time + start + offset,
+ "end": line.start_time + end + offset,
+ "acc": acc,
+ "c1": c1,
+ "c3": c3,
+ "c4": c4,
+ }
+ )
+
+ def get_color_change(
+ self, line: Line, c1: bool = None, c3: bool = None, c4: bool = None
+ ) -> str:
+ """Returns all the color_changes in the object that fit (in terms of time) between line.start_time and line.end_time.
+
+ Parameters:
+ line (Line object): The line of which you want to get the color changes
+ c1 (bool, optional): If False, you will not get color values containing primary color
+ c3 (bool, optional): If False, you will not get color values containing border color
+ c4 (bool, optional): If False, you will not get color values containing shadow color
+
+ Returns:
+ A string containing color changes interpolated.
+
+ Note:
+ If c1, c3 or c4 is/are None, the script will automatically recognize what you used in the color changes in the lines and put only the ones considered essential.
+
+ Examples:
+ .. code-block:: python3
+ :emphasize-lines: 6
+
+ # Assume that we have l as a copy of line and we're iterating over all the syl in the current line
+ # All the fun stuff of the effect creation...
+ l.start_time = line.start_time + syl.start_time
+ l.end_time = line.start_time + syl.end_time
+
+ l.text = "{\\\\an5\\\\pos(%.3f,%.3f)\\\\fscx120\\\\fscy120%s}%s" % (syl.center, syl.middle, CU.get_color_change(l), syl.text)
+ """
+ transform = ""
+
+ # If we don't have user's settings, we set c values
+ # to the ones that we previously saved
+ if c1 is None:
+ c1 = self.c1_req
+ if c3 is None:
+ c3 = self.c3_req
+ if c4 is None:
+ c4 = self.c4_req
+
+ # Reading default colors
+ base_c1 = "\\1c" + line.styleref.color1
+ base_c3 = "\\3c" + line.styleref.color3
+ base_c4 = "\\4c" + line.styleref.color4
+
+ for color_change in self.color_changes:
+ if color_change["end"] <= line.start_time:
+ # Get base colors from this color change, since it is before my current line
+ # Last color change written in .ass wins
+ if color_change["c1"]:
+ base_c1 = color_change["c1"]
+ if color_change["c3"]:
+ base_c3 = color_change["c3"]
+ if color_change["c4"]:
+ base_c4 = color_change["c4"]
+ elif color_change["start"] <= line.end_time:
+ # We have found a valid color change, append it to the transform
+ start_time = color_change["start"] - line.start_time
+ end_time = color_change["end"] - line.start_time
+
+ # We don't want to have times = 0
+ start_time = 1 if start_time == 0 else start_time
+ end_time = 1 if end_time == 0 else end_time
+
+ transform += "\\t(%d,%d," % (start_time, end_time)
+
+ if color_change["acc"] != 1:
+ transform += str(color_change["acc"])
+
+ if c1 and color_change["c1"]:
+ transform += color_change["c1"]
+ if c3 and color_change["c3"]:
+ transform += color_change["c3"]
+ if c4 and color_change["c4"]:
+ transform += color_change["c4"]
+
+ transform += ")"
+
+ # Appending default color found, if requested
+ if c4:
+ transform = base_c4 + transform
+ if c3:
+ transform = base_c3 + transform
+ if c1:
+ transform = base_c1 + transform
+
+ return transform
+
+ def get_fr_color_change(
+ self, line: Line, c1: bool = None, c3: bool = None, c4: bool = None
+ ) -> str:
+ """Returns the single color(s) in the color_changes that fit the current frame (line.start_time) in your frame loop.
+
+ Note:
+ If you get errors, try either modifying your \\\\t values or set your **fr parameter** in FU object to **10**.
+
+ Parameters:
+ line (Line object): The line of which you want to get the color changes
+ c1 (bool, optional): If False, you will not get color values containing primary color.
+ c3 (bool, optional): If False, you will not get color values containing border color.
+ c4 (bool, optional): If False, you will not get color values containing shadow color.
+
+ Returns:
+ A string containing color changes interpolated.
+
+ Examples:
+ .. code-block:: python3
+ :emphasize-lines: 5
+
+ # Assume that we have l as a copy of line and we're iterating over all the syl in the current line and we're iterating over the frames
+ l.start_time = s
+ l.end_time = e
+
+ l.text = "{\\\\an5\\\\pos(%.3f,%.3f)\\\\fscx120\\\\fscy120%s}%s" % (syl.center, syl.middle, CU.get_fr_color_change(l), syl.text)
+ """
+ # If we don't have user's settings, we set c values
+ # to the ones that we previously saved
+ if c1 is None:
+ c1 = self.c1_req
+ if c3 is None:
+ c3 = self.c3_req
+ if c4 is None:
+ c4 = self.c4_req
+
+ # Reading default colors
+ base_c1 = "\\1c" + line.styleref.color1
+ base_c3 = "\\3c" + line.styleref.color3
+ base_c4 = "\\4c" + line.styleref.color4
+
+ # Searching valid color_change
+ current_time = line.start_time
+ latest_index = -1
+
+ for i, color_change in enumerate(self.color_changes):
+ if current_time >= color_change["start"]:
+ latest_index = i
+
+ # If no color change is found, take default from style
+ if latest_index == -1:
+ colors = ""
+ if c1:
+ colors += base_c1
+ if c3:
+ colors += base_c3
+ if c4:
+ colors += base_c4
+ return colors
+
+ # If we have passed the end of the lastest color change available, then take the final values of it
+ if current_time >= self.color_changes[latest_index]["end"]:
+ colors = ""
+ if c1 and self.color_changes[latest_index]["c1"]:
+ colors += self.color_changes[latest_index]["c1"]
+ if c3 and self.color_changes[latest_index]["c3"]:
+ colors += self.color_changes[latest_index]["c3"]
+ if c4 and self.color_changes[latest_index]["c4"]:
+ colors += self.color_changes[latest_index]["c4"]
+ return colors
+
+ # Else, interpolate the latest color change
+ start = current_time - self.color_changes[latest_index]["start"]
+ end = (
+ self.color_changes[latest_index]["end"]
+ - self.color_changes[latest_index]["start"]
+ )
+ pct = start / end
+
+ # If we're in the first color_change, interpolate with base colors
+ if latest_index == 0:
+ colors = ""
+ if c1 and self.color_changes[latest_index]["c1"]:
+ colors += "\\1c" + Utils.interpolate(
+ pct,
+ base_c1[3:],
+ self.color_changes[latest_index]["c1"][3:],
+ self.color_changes[latest_index]["acc"],
+ )
+ if c3 and self.color_changes[latest_index]["c3"]:
+ colors += "\\3c" + Utils.interpolate(
+ pct,
+ base_c3[3:],
+ self.color_changes[latest_index]["c3"][3:],
+ self.color_changes[latest_index]["acc"],
+ )
+ if c4 and self.color_changes[latest_index]["c4"]:
+ colors += "\\4c" + Utils.interpolate(
+ pct,
+ base_c4[3:],
+ self.color_changes[latest_index]["c4"][3:],
+ self.color_changes[latest_index]["acc"],
+ )
+ return colors
+
+ # Else, we interpolate between current color change and previous
+ colors = ""
+ if c1:
+ colors += "\\1c" + Utils.interpolate(
+ pct,
+ self.color_changes[latest_index - 1]["c1"][3:],
+ self.color_changes[latest_index]["c1"][3:],
+ self.color_changes[latest_index]["acc"],
+ )
+ if c3:
+ colors += "\\3c" + Utils.interpolate(
+ pct,
+ self.color_changes[latest_index - 1]["c3"][3:],
+ self.color_changes[latest_index]["c3"][3:],
+ self.color_changes[latest_index]["acc"],
+ )
+ if c4:
+ colors += "\\4c" + Utils.interpolate(
+ pct,
+ self.color_changes[latest_index - 1]["c4"][3:],
+ self.color_changes[latest_index]["c4"][3:],
+ self.color_changes[latest_index]["acc"],
+ )
+ return colors