2 VLSub Extension for VLC media player 1.1 and 2.0
3 Copyright 2013 Guillaume Le Maout
5 Authors: Guillaume Le Maout
7 http://addons.videolan.org/messages/?action=newmessage&username=exebetche
8 Bug report: http://addons.videolan.org/content/show.php/?content=148752
10 This program is free software; you can redistribute it and/or modify
11 it under the terms of the GNU General Public License as published by
12 the Free Software Foundation; either version 2 of the License, or
13 (at your option) any later version.
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
28 -- You can set here your default language by replacing nil with
29 -- your language code (see below).Example:
37 downloadBehaviour
= 'save',
40 showMediaInformation
= true,
43 translations_avail
= {
51 pob
= 'Brazilian Portuguese',
52 por
= 'Portuguese (Portugal)',
62 int_descr
= 'Download subtitles from OpenSubtitles.org',
63 int_research
= 'Research',
64 int_config
= 'Config',
65 int_configuration
= 'Configuration',
67 int_search_hash
= 'Search by hash',
68 int_search_name
= 'Search by name',
70 int_season
= 'Season (series)',
71 int_episode
= 'Episode (series)',
72 int_show_help
= 'Show help',
73 int_show_conf
= 'Show config',
74 int_dowload_sel
= 'Download selection',
78 int_cancel
= 'Cancel',
79 int_bool_true
= 'Yes',
80 int_bool_false
= 'No',
81 int_search_transl
= 'Search translations',
82 int_searching_transl
= 'Searching translations ...',
83 int_int_lang
= 'Interface language',
84 int_default_lang
= 'Subtitles language',
85 int_dowload_behav
= 'What to do with subtitles',
86 int_dowload_save
= 'Load and save',
87 int_dowload_load
= 'Load only',
88 int_dowload_manual
= 'Manual download',
89 int_display_code
= 'Display language code in file name',
90 int_remove_tag
= 'Remove tags',
91 int_vlsub_work_dir
= 'VLSub working directory',
92 int_os_username
= 'Username',
93 int_os_password
= 'Password',
95 Download subtitles from
96 <a href='http://www.opensubtitles.org/'>
98 </a> and display them while watching a video.<br>
100 <b><u>Usage:</u></b><br>
102 Start your video. If you use Vlsub witout playing a video
103 you will get a link to download the subtitles in your browser
104 but the subtitles won't be saved and loaded automatically.<br>
106 Choose the language for your subtitles and click on the
107 button corresponding to one of the two research methods
108 provided by VLSub:<br>
110 <b>Method 1: Search by hash</b><br>
111 It is recommended to try this method first, because it
112 performs a research based on the video file print, so you
113 can find subtitles synchronized with your video.<br>
115 <b>Method 2: Search by name</b><br>
116 If you have no luck with the first method, just check the
117 title is correct before clicking. If you search subtitles
118 for a series, you can also provide a season and episode
121 <b>Downloading Subtitles</b><br>
122 Select one subtitle in the list and click on 'Download'.<br>
123 It will be put in the same directory that your video, with
124 the same name (different extension)
125 so VLC will load them automatically the next time you'll
128 <b>/!\\ Beware :</b> Existing subtitles are overwritten
129 without asking confirmation, so put them elsewhere if
130 they're important.<br>
132 Find more VLC extensions at
133 <a href='http://addons.videolan.org'>addons.videolan.org</a>.
135 int_no_support_mess
= [[
136 <strong>VLSub is not working with VLC 2.1.x on
137 any platform</strong>
138 because the lua "net" module needed to interact
139 with opensubtitles has been
140 removed in this release for the extensions.
142 <strong>Works with VLC 2.2 on mac and linux.</strong>
144 <strong>On windows you have to install an older version
145 of VLC (2.0.8 for example)</strong>
148 <a target="_blank" rel="nofollow"
149 href="http://download.videolan.org/pub/videolan/vlc/2.0.8/">
150 http://download.videolan.org/pub/videolan/vlc/2.0.8/</a><br>
153 action_login
= 'Logging in',
154 action_logout
= 'Logging out',
155 action_noop
= 'Checking session',
156 action_search
= 'Searching subtitles',
157 action_hash
= 'Calculating movie hash',
159 mess_success
= 'Success',
160 mess_error
= 'Error',
161 mess_warn
= 'Warning',
162 mess_no_response
= 'Server not responding',
163 mess_unauthorized
= 'Request unauthorized',
164 mess_expired
= 'Session expired, retrying',
165 mess_overloaded
= 'Server overloaded, please retry later',
166 mess_no_input
= 'Please use this method during playing',
167 mess_not_local
= 'This method works with local file only (for now)',
168 mess_not_found
= 'File not found',
169 mess_not_found2
= 'File not found (illegal character?)',
170 mess_no_selection
= 'No subtitles selected',
171 mess_save_fail
= 'Unable to save subtitles',
172 mess_save_warn
= 'Unable to save subtitles in file folder, using config folder',
173 mess_click_link
= 'Click here to open the file',
174 mess_complete
= 'Research complete',
175 mess_no_res
= 'No result',
176 mess_res
= 'result(s)',
177 mess_loaded
= 'Subtitles loaded',
178 mess_not_load
= 'Unable to load subtitles',
179 mess_downloading
= 'Downloading subtitle',
180 mess_dowload_link
= 'Download link',
181 mess_err_conf_access
='Can\'t find a suitable path to save'..
182 'config, please set it manually',
183 mess_err_wrong_path
='the path contains illegal character, '..
185 mess_err_hash
= 'Failed to generate hash'
197 {'bul', 'Bulgarian'},
206 {'epo', 'Esperanto'},
216 {'hun', 'Hungarian'},
217 {'ice', 'Icelandic'},
218 {'ind', 'Indonesian'},
225 {'lit', 'Lithuanian'},
226 {'ltz', 'Luxembourgish'},
227 {'mac', 'Macedonian'},
229 {'mal', 'Malayalam'},
230 {'mon', 'Mongolian'},
231 {'nor', 'Norwegian'},
235 {'por', 'Portuguese'},
236 {'pob', 'Brazilian Portuguese'},
240 {'sin', 'Sinhalese'},
242 {'slv', 'Slovenian'},
251 {'ukr', 'Ukrainian'},
253 {'vie', 'Vietnamese'}
256 -- Languages code conversion table: iso-639-1 to iso-639-3
257 -- See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
258 local lang_os_to_iso
= {
325 local input_table
= {} -- General widget id reference
326 local select_conf
= {} -- Drop down widget / option table association
328 --[[ VLC extension stuff ]]--
330 function descriptor()
332 title
= "VLsub 0.10.0",
334 author
= "exebetche",
335 url
= 'http://www.opensubtitles.org/',
337 description
= options
.translation
.int_descr
,
338 capabilities
= {"menu", "input-listener" }
343 vlc
.msg
.dbg("[VLsub] Welcome")
345 if not check_config() then
346 vlc
.msg
.err("[VLsub] Unsupported VLC version")
350 if vlc
.input
.item() then
351 openSub
.getFileInfo()
352 openSub
.getMovieInfo()
362 function deactivate()
363 vlc
.msg
.dbg("[VLsub] Bye bye!")
368 if openSub
.session
.token
and openSub
.session
.token
~= "" then
369 openSub
.request("LogOut")
381 function meta_changed()
385 function input_changed()
391 --[[ Interface data ]]--
393 function interface_main()
394 dlg
:add_label(lang
["int_default_lang"]..':', 1, 1, 1, 1)
395 input_table
['language'] = dlg
:add_dropdown(2, 1, 2, 1)
396 dlg
:add_button(lang
["int_search_hash"],
397 searchHash
, 4, 1, 1, 1)
399 dlg
:add_label(lang
["int_title"]..':', 1, 2, 1, 1)
400 input_table
['title'] = dlg
:add_text_input(
401 openSub
.movie
.title
or "", 2, 2, 2, 1)
402 dlg
:add_button(lang
["int_search_name"],
403 searchIMBD
, 4, 2, 1, 1)
404 dlg
:add_label(lang
["int_season"]..':', 1, 3, 1, 1)
405 input_table
['seasonNumber'] = dlg
:add_text_input(
406 openSub
.movie
.seasonNumber
or "", 2, 3, 2, 1)
407 dlg
:add_label(lang
["int_episode"]..':', 1, 4, 1, 1)
408 input_table
['episodeNumber'] = dlg
:add_text_input(
409 openSub
.movie
.episodeNumber
or "", 2, 4, 2, 1)
410 input_table
['mainlist'] = dlg
:add_list(1, 5, 4, 1)
411 input_table
['message'] = nil
412 input_table
['message'] = dlg
:add_label(' ', 1, 6, 4, 1)
414 lang
["int_show_help"], show_help
, 1, 7, 1, 1)
416 ' '..lang
["int_show_conf"]..' ', show_conf
, 2, 7, 1, 1)
418 lang
["int_dowload_sel"], download_subtitles
, 3, 7, 1, 1)
420 lang
["int_close"], deactivate
, 4, 7, 1, 1)
425 openSub
.conf
.languages
,
432 function set_interface_main()
433 -- Update movie title and co. if video input change
434 if not type(input_table
['title']) == 'userdata' then return false end
436 openSub
.getFileInfo()
437 openSub
.getMovieInfo()
439 input_table
['title']:set_text(
440 openSub
.movie
.title
or "")
441 input_table
['episodeNumber']:set_text(
442 openSub
.movie
.episodeNumber
or "")
443 input_table
['seasonNumber']:set_text(
444 openSub
.movie
.seasonNumber
or "")
447 function interface_config()
448 input_table
['intLangLab'] = dlg
:add_label(
449 lang
["int_int_lang"]..':', 1, 1, 1, 1)
450 input_table
['intLangBut'] = dlg
:add_button(
451 lang
["int_search_transl"],
452 get_available_translations
, 2, 1, 1, 1)
453 input_table
['intLang'] = dlg
:add_dropdown(3, 1, 1, 1)
455 lang
["int_default_lang"]..':', 1, 2, 2, 1)
456 input_table
['default_language'] = dlg
:add_dropdown(3, 2, 1, 1)
458 lang
["int_dowload_behav"]..':', 1, 3, 2, 1)
459 input_table
['downloadBehaviour'] = dlg
:add_dropdown(3, 3, 1, 1)
461 lang
["int_display_code"]..':', 1, 4, 0, 1)
462 input_table
['langExt'] = dlg
:add_dropdown(3, 4, 1, 1)
464 lang
["int_remove_tag"]..':', 1, 5, 0, 1)
465 input_table
['removeTag'] = dlg
:add_dropdown(3, 5, 1, 1)
468 lang
["int_os_username"]..':', 1, 7, 0, 1)
469 input_table
['os_username'] = dlg
:add_text_input(
470 type(openSub
.option
.os_username
) == "string"
471 and openSub
.option
.os_username
or "", 2, 7, 2, 1)
473 lang
["int_os_password"]..':', 1, 8, 0, 1)
474 input_table
['os_password'] = dlg
:add_text_input(
475 type(openSub
.option
.os_password
) == "string"
476 and openSub
.option
.os_password
or "", 2, 8, 2, 1)
478 input_table
['message'] = nil
479 input_table
['message'] = dlg
:add_label(' ', 1, 9, 3, 1)
483 show_main
, 2, 10, 1, 1)
486 apply_config
, 3, 10, 1, 1)
488 input_table
['langExt']:add_value(
489 lang
["int_bool_"..tostring(openSub
.option
.langExt
)], 1)
490 input_table
['langExt']:add_value(
491 lang
["int_bool_"..tostring(not openSub
.option
.langExt
)], 2)
492 input_table
['removeTag']:add_value(
493 lang
["int_bool_"..tostring(openSub
.option
.removeTag
)], 1)
494 input_table
['removeTag']:add_value(
495 lang
["int_bool_"..tostring(not openSub
.option
.removeTag
)], 2)
500 openSub
.conf
.translations_avail
,
505 openSub
.conf
.languages
,
511 openSub
.conf
.downloadBehaviours
,
515 function interface_help()
516 local help_html
= lang
["int_help_mess"]
518 input_table
['help'] = dlg
:add_html(
519 help_html
, 1, 1, 4, 1)
521 string.rep (" ", 100), 1, 2, 3, 1)
523 lang
["int_ok"], show_main
, 4, 2, 1, 1)
526 function interface_no_support()
527 local no_support_html
= lang
["int_no_support_mess"]
529 input_table
['no_support'] = dlg
:add_html(
530 no_support_html
, 1, 1, 4, 1)
532 string.rep (" ", 100), 1, 2, 3, 1)
535 function trigger_menu(dlg_id
)
539 openSub
.conf
.useragent
)
541 elseif dlg_id
== 2 then
544 openSub
.conf
.useragent
..': '..lang
["int_configuration"])
546 elseif dlg_id
== 3 then
549 openSub
.conf
.useragent
..': '..lang
["int_help"])
552 collectgarbage() --~ !important
568 vlc
.msg
.dbg("[VLSub] Closing dialog")
571 --~ dlg:delete() -- Throw an error
578 collectgarbage() --~ !important
581 --[[ Drop down / config association]]--
583 function assoc_select_conf(select_id
, option
, conf
, ind
, default
)
584 -- Helper for i/o interaction between drop down and option list
585 select_conf
[select_id
] = {
591 set_default_option(select_id
)
592 display_select(select_id
)
595 function set_default_option(select_id
)
596 -- Put the selected option of a list in first place of the associated table
597 local opt
= select_conf
[select_id
].opt
598 local cfg
= select_conf
[select_id
].cf
599 local ind
= select_conf
[select_id
].ind
600 if openSub
.option
[opt
] then
601 table.sort(cfg
, function(a
, b
)
602 if a
[1] == openSub
.option
[opt
] then
604 elseif b
[1] == openSub
.option
[opt
] then
607 return a
[ind
] < b
[ind
]
613 function display_select(select_id
)
614 -- Display the drop down values with an optional default value at the top
615 local conf
= select_conf
[select_id
].cf
616 local opt
= select_conf
[select_id
].opt
617 local option
= openSub
.option
[opt
]
618 local default
= select_conf
[select_id
].dflt
619 local default_isset
= false
625 for k
, l
in ipairs(conf
) do
626 if default_isset
then
627 input_table
[select_id
]:add_value(l
[2], k
)
630 input_table
[select_id
]:add_value(l
[2], k
)
631 input_table
[select_id
]:add_value(default
, 0)
633 input_table
[select_id
]:add_value(default
, 0)
634 input_table
[select_id
]:add_value(l
[2], k
)
641 --[[ Config & interface localization]]--
643 function check_config()
644 -- Make a copy of english translation to use it as default
645 -- in case some element aren't translated in other translations
647 for k
, v
in pairs(openSub
.option
.translation
) do
648 eng_translation
[k
] = v
651 -- Get available translation full name from code
653 for i
, lg
in ipairs(languages
) do
654 trsl_names
[lg
[1]]
= lg
[2]
657 if is_window_path(vlc
.config
.datadir()) then
658 openSub
.conf
.os
= "win"
661 openSub
.conf
.os
= "lin"
665 local filePath
= slash
.."vlsub_conf.xml"
667 openSub
.conf
.dirPath
= vlc
.config
.userdatadir()
668 local subdirs
= { "lua", "extensions", "userdata", "vlsub" }
669 for _
, dir
in ipairs(subdirs
) do
670 if not vlc
.misc
.mkdir( openSub
.conf
.dirPath
.. slash
.. dir
, "0700" ) then
671 vlc
.msg
.warn("Failed to create " .. openSub
.conf
.dirPath
.. slash
.. dir
)
674 openSub
.conf
.dirPath
= openSub
.conf
.dirPath
.. slash
.. dir
677 if openSub
.conf
.dirPath
then
678 vlc
.msg
.dbg("[VLSub] Working directory: " .. openSub
.conf
.dirPath
)
680 openSub
.conf
.filePath
= openSub
.conf
.dirPath
..filePath
681 openSub
.conf
.localePath
= openSub
.conf
.dirPath
..slash
.."locale"
683 if file_exist(openSub
.conf
.filePath
) then
684 vlc
.msg
.dbg("[VLSub] Loading config file: "..openSub
.conf
.filePath
)
687 vlc
.msg
.dbg("[VLSub] No config file")
689 config_saved
= save_config()
690 if not config_saved
then
691 vlc
.msg
.dbg("[VLSub] Unable to save config")
695 -- Check presence of a translation file
696 -- in "%vlsub_directory%/locale"
697 -- Add translation files to available translation list
698 local file_list
= list_dir(openSub
.conf
.localePath
)
699 local translations_avail
= openSub
.conf
.translations_avail
702 for i
, file_name
in ipairs(file_list
) do
703 local lg
= string.gsub(
708 and not openSub
.option
.translations_avail
[lg
] then
709 table.insert(translations_avail
, {
717 -- Load selected translation from file
718 if openSub
.option
.intLang
~= "eng"
719 and not openSub
.conf
.translated
721 local transl_file_path
= openSub
.conf
.localePath
..
722 slash
..openSub
.option
.intLang
..".xml"
723 if file_exist(transl_file_path
) then
725 "[VLSub] Loading translation from file: "..
727 load_transl(transl_file_path
)
731 vlc
.msg
.dbg("[VLSub] Unable to find a suitable path"..
732 "to save config, please set it manually")
737 lang
= options
.translation
-- just a short cut
739 if not vlc
.net
or not vlc
.net
.poll
then
741 openSub
.conf
.useragent
..': '..lang
["mess_error"])
742 interface_no_support()
747 SetDownloadBehaviours()
749 -- Set table list of available translations from assoc. array
752 for k
, l
in pairs(openSub
.option
.translations_avail
) do
753 if k
== openSub
.option
.int_research
then
754 table.insert(openSub
.conf
.translations_avail
, 1, {k
, l
})
756 table.insert(openSub
.conf
.translations_avail
, {k
, l
})
763 function load_config()
764 -- Overwrite default conf with loaded conf
765 local tmpFile
= io
.open(vlc
.strings
.to_codepage(openSub
.conf
.filePath
), "rb")
766 if not tmpFile
then return false end
767 local resp
= tmpFile
:read("*all")
770 local option
= parse_xml(resp
)
772 for key
, value
in pairs(option
) do
773 if type(value
) == "table" then
774 if key
== "translation" then
775 openSub
.conf
.translated
= true
776 for k
, v
in pairs(value
) do
777 openSub
.option
.translation
[k
] = v
780 openSub
.option
[key
] = value
783 if value
== "true" then
784 openSub
.option
[key
] = true
785 elseif value
== "false" then
786 openSub
.option
[key
] = false
788 openSub
.option
[key
] = value
795 function load_transl(path
)
796 -- Overwrite default conf with loaded conf
797 local tmpFile
= assert(io
.open(vlc
.strings
.to_codepage(path
), "rb"))
798 local resp
= tmpFile
:read("*all")
801 openSub
.option
.translation
= nil
803 openSub
.option
.translation
= parse_xml(resp
)
807 function apply_translation()
808 -- Overwrite default conf with loaded conf
809 for k
, v
in pairs(eng_translation
) do
810 if not openSub
.option
.translation
[k
] then
811 openSub
.option
.translation
[k
] = eng_translation
[k
]
816 function getenv_lang()
817 -- Retrieve the user OS language
818 local os_lang
= os
.getenv("LANG")
820 if os_lang
then -- unix, mac
821 os_lang
= string.sub(os_lang
, 0, 2)
822 if type(lang_os_to_iso
[os_lang
]) then
823 openSub
.option
.language
= lang_os_to_iso
[os_lang
]
826 local lang_w
= string.match(
827 os
.setlocale("", "collate"),
829 for i
, v
in ipairs(openSub
.conf
.languages
) do
830 if v
[2] == lang_w
then
831 openSub
.option
.language
= v
[1]
837 function apply_config()
838 -- Apply user config selection to local config
839 local lg_sel
= input_table
['intLang']:get_value()
844 if lg_sel
and lg_sel
~= 1
845 and openSub
.conf
.translations_avail
[lg_sel
] then
846 local lg
= openSub
.conf
.translations_avail
[lg_sel
][1]
848 SetDownloadBehaviours()
851 for select_id
, v
in pairs(select_conf
) do
852 if input_table
[select_id
]
853 and select_conf
[select_id
] then
854 sel_val
= input_table
[select_id
]:get_value()
855 sel_cf
= select_conf
[select_id
]
859 openSub
.option
[opt
] = nil
861 openSub
.option
[opt
] = sel_cf
.cf
[sel_val
][1]
864 set_default_option(select_id
)
869 openSub
.option
.os_username
= input_table
['os_username']:get_text()
870 openSub
.option
.os_password
= input_table
['os_password']:get_text()
872 if input_table
["langExt"]:get_value() == 2 then
873 openSub
.option
.langExt
= not openSub
.option
.langExt
876 if input_table
["removeTag"]:get_value() == 2 then
877 openSub
.option
.removeTag
= not openSub
.option
.removeTag
880 local config_saved
= save_config()
882 if not config_saved
then
883 setError(lang
["mess_err_conf_access"])
887 function save_config()
888 -- Dump local config into config file
889 if openSub
.conf
.dirPath
890 and openSub
.conf
.filePath
then
892 "[VLSub] Saving config file: "..
893 openSub
.conf
.filePath
)
895 if file_touch(openSub
.conf
.filePath
) then
896 local tmpFile
= assert(
897 io
.open(vlc
.strings
.to_codepage(openSub
.conf
.filePath
), "wb"))
898 local resp
= dump_xml(openSub
.option
)
909 vlc
.msg
.dbg("[VLSub] Unable fount a suitable path "..
910 "to save config, please set it manually")
911 setError(lang
["mess_err_conf_access"])
916 function SetDownloadBehaviours()
917 openSub
.conf
.downloadBehaviours
= nil
918 openSub
.conf
.downloadBehaviours
= {
919 {'save', lang
["int_dowload_save"]},
920 {'manual', lang
["int_dowload_manual"]}
924 function get_available_translations()
925 -- Get all available translation files from the internet
926 -- (drop previous direct download from github repo
927 -- causing error with github https CA certficate on OS X an XP)
928 -- https://github.com/exebetche/vlsub/tree/master/locale
930 local translations_url
= "http://addons.videolan.org/CONTENT/"..
931 "content-files/148752-vlsub_translations.xml"
933 if input_table
['intLangBut']:get_text() == lang
["int_search_transl"]
935 openSub
.actionLabel
= lang
["int_searching_transl"]
937 local translations_content
, status
, resp
= get(translations_url
)
938 local translations_avail
= openSub
.option
.translations_avail
940 if translations_content
== false then
941 -- Translation list download error
942 setMessage(error_tag(lang
["mess_error"] .. " (" .. status
.. ")"))
946 all_trsl
= parse_xml(translations_content
)
949 for lg
, trsl
in pairs(all_trsl
) do
950 if lg
~= options
.intLang
[1]
951 and not translations_avail
[lg
] then
952 translations_avail
[lg
] = trsl_names
[lg
] or ""
953 table.insert(openSub
.conf
.translations_avail
, {
957 input_table
['intLang']:add_value(
959 #openSub
.conf
.translations_avail
)
963 setMessage(success_tag(lang
["mess_complete"]))
968 function set_translation(lg
)
969 openSub
.option
.translation
= nil
970 openSub
.option
.translation
= {}
973 for k
, v
in pairs(eng_translation
) do
974 openSub
.option
.translation
[k
] = v
977 -- If translation file exists in /locale directory load it
978 if openSub
.conf
.localePath
979 and file_exist(openSub
.conf
.localePath
..
980 slash
..lg
..".xml") then
981 local transl_file_path
= openSub
.conf
.localePath
..
983 vlc
.msg
.dbg("[VLSub] Loading translation from file: "..
985 load_transl(transl_file_path
)
988 -- Load translation file from internet
990 get_available_translations()
993 if not all_trsl
or not all_trsl
[lg
] then
994 vlc
.msg
.dbg("[VLSub] Error, translation not found")
997 openSub
.option
.translation
= all_trsl
[lg
]
1004 lang
= openSub
.option
.translation
1014 url
= "http://api.opensubtitles.org/xml-rpc",
1016 userAgentHTTP
= "VLSub",
1017 useragent
= "VLSub 0.10.0",
1018 translations_avail
= {},
1019 downloadBehaviours
= nil,
1020 languages
= languages
1048 request
= function(methodName
)
1049 local params
= openSub
.methods
[methodName
].params()
1050 local reqTable
= openSub
.getMethodBase(methodName
, params
)
1051 local request
= "<?xml version='1.0'?>"..dump_xml(reqTable
)
1052 local host
, path
= parse_url(openSub
.conf
.url
)
1054 "POST "..path
.." HTTP/1.0",
1056 "User-Agent: "..openSub
.conf
.userAgentHTTP
,
1057 "Content-Type: text/xml",
1058 "Content-Length: "..string.len(request
),
1062 request
= table.concat(header
, "\r\n")..request
1065 local status
, responseStr
= http_req(host
, 80, request
)
1067 if status
== 200 then
1068 response
= parse_xmlrpc(responseStr
)
1070 if response
.status
== "200 OK" then
1071 return openSub
.methods
[methodName
]
1073 elseif response
.status
== "406 No session" then
1074 openSub
.request("LogIn")
1075 elseif response
then
1082 setError("Server not responding")
1085 elseif status
== 401 then
1086 setError("Request unauthorized")
1088 response
= parse_xmlrpc(responseStr
)
1089 if openSub
.session
.token
~= response
.token
then
1090 setMessage("Session expired, retrying")
1091 openSub
.session
.token
= response
.token
1092 openSub
.request(methodName
)
1095 elseif status
== 503 then
1096 setError("Server overloaded, please retry later")
1101 getMethodBase
= function(methodName
, param
)
1102 if openSub
.methods
[methodName
].methodName
then
1103 methodName
= openSub
.methods
[methodName
].methodName
1108 methodName
=methodName
,
1109 params
={ param
=param
}}}
1116 openSub
.actionLabel
= lang
["action_login"]
1118 { value
={ string=openSub
.option
.os_username
} },
1119 { value
={ string=openSub
.option
.os_password
} },
1120 { value
={ string=openSub
.movie
.sublanguageid
} },
1121 { value
={ string=openSub
.conf
.useragent
} }
1124 callback
= function(resp
)
1125 openSub
.session
.token
= resp
.token
1126 openSub
.session
.loginTime
= os
.time()
1132 openSub
.actionLabel
= lang
["action_logout"]
1134 { value
={ string=openSub
.session
.token
} }
1137 callback
= function()
1143 openSub
.actionLabel
= lang
["action_noop"]
1145 { value
={ string=openSub
.session
.token
} }
1148 callback
= function(resp
)
1152 SearchSubtitlesByHash
= {
1153 methodName
= "SearchSubtitles",
1155 openSub
.actionLabel
= lang
["action_search"]
1156 setMessage(openSub
.actionLabel
..": "..
1157 progressBarContent(0))
1160 { value
={ string=openSub
.session
.token
} },
1167 { name
="sublanguageid", value
={
1168 string=openSub
.movie
.sublanguageid
}
1170 { name
="moviehash", value
={
1171 string=openSub
.file
.hash
} },
1172 { name
="moviebytesize", value
={
1173 double
=openSub
.file
.bytesize
} }
1177 callback
= function(resp
)
1178 openSub
.itemStore
= resp
.data
1182 methodName
= "SearchSubtitles",
1184 openSub
.actionLabel
= lang
["action_search"]
1185 setMessage(openSub
.actionLabel
..": "..
1186 progressBarContent(0))
1189 { name
="sublanguageid", value
={
1190 string=openSub
.movie
.sublanguageid
} },
1191 { name
="query", value
={
1192 string=openSub
.movie
.title
} } }
1195 if openSub
.movie
.seasonNumber
~= nil then
1196 table.insert(member
, { name
="season", value
={
1197 string=openSub
.movie
.seasonNumber
} })
1200 if openSub
.movie
.episodeNumber
~= nil then
1201 table.insert(member
, { name
="episode", value
={
1202 string=openSub
.movie
.episodeNumber
} })
1206 { value
={ string=openSub
.session
.token
} },
1216 callback
= function(resp
)
1217 openSub
.itemStore
= resp
.data
1221 getInputItem
= function()
1222 return vlc
.item
or vlc
.input
.item()
1224 getFileInfo
= function()
1225 -- Get video file path, name, extension from input uri
1226 local item
= openSub
.getInputItem()
1227 local file
= openSub
.file
1229 file
.hasInput
= false;
1230 file
.cleanName
= nil;
1231 file
.protocol
= nil;
1236 vlc
.msg
.dbg("[VLSub] Video URI: "..item
:uri())
1237 local parsed_uri
= vlc
.strings
.url_parse(item
:uri())
1238 file
.uri
= item
:uri()
1239 file
.protocol
= parsed_uri
["protocol"]
1240 file
.path
= parsed_uri
["path"]
1245 file
.path
= string.match(file
.path
, "^/(%a:/.+)$") or file
.path
1247 -- For file in archive
1248 local archive_path
, name_in_archive
= string.match(
1249 file
.path
, '^([^!]+)!/([^!/]*)$')
1250 if archive_path
and archive_path
~= "" then
1251 file
.path
= string.gsub(
1255 file
.path
= vlc
.strings
.decode_uri(file
.path
)
1256 file
.completeName
= string.gsub(
1260 file
.completeName
= vlc
.strings
.decode_uri(
1262 file
.is_archive
= true
1263 else -- "classic" input
1264 file
.path
= vlc
.strings
.decode_uri(file
.path
)
1265 file
.dir
, file
.completeName
= string.match(
1269 local file_stat
= vlc
.net
.stat(file
.path
)
1272 file
.stat
= file_stat
1275 file
.is_archive
= false
1278 file
.name
, file
.ext
= string.match(
1280 '^([^/]-)%.?([^%.]*)$')
1282 if file
.ext
== "part" then
1283 file
.name
, file
.ext
= string.match(
1285 '^([^/]+)%.([^%.]+)$')
1288 file
.hasInput
= true;
1289 file
.cleanName
= string.gsub(
1292 vlc
.msg
.dbg("[VLSub] file info "..(dump_xml(file
)))
1296 getMovieInfo
= function()
1297 -- Clean video file name and check for season/episode pattern in title
1298 if not openSub
.file
.name
then
1299 openSub
.movie
.title
= ""
1300 openSub
.movie
.seasonNumber
= ""
1301 openSub
.movie
.episodeNumber
= ""
1305 local showName
, seasonNumber
, episodeNumber
= string.match(
1306 openSub
.file
.cleanName
or "",
1307 "(.+)[sS](%d%d)[eE](%d%d).*")
1309 if not showName
then
1310 showName
, seasonNumber
, episodeNumber
= string.match(
1311 openSub
.file
.cleanName
or "",
1312 "(.+)(%d)[xX](%d%d).*")
1316 openSub
.movie
.title
= showName
1317 openSub
.movie
.seasonNumber
= seasonNumber
1318 openSub
.movie
.episodeNumber
= episodeNumber
1320 openSub
.movie
.title
= openSub
.file
.cleanName
1321 openSub
.movie
.seasonNumber
= ""
1322 openSub
.movie
.episodeNumber
= ""
1326 getMovieHash
= function()
1327 -- Calculate movie hash
1328 openSub
.actionLabel
= lang
["action_hash"]
1329 setMessage(openSub
.actionLabel
..": "..
1330 progressBarContent(0))
1332 local item
= openSub
.getInputItem()
1335 setError(lang
["mess_no_input"])
1339 openSub
.getFileInfo()
1341 if not openSub
.file
.path
then
1342 setError(lang
["mess_not_found"])
1346 local data_start
= ""
1349 local chunk_size
= 65536
1351 -- Get data for hash calculation
1352 if openSub
.file
.is_archive
then
1353 vlc
.msg
.dbg("[VLSub] Read hash data from stream")
1355 local file
= vlc
.stream(openSub
.file
.uri
)
1360 data_start
= file
:read(chunk_size
)
1363 size
= size
+ string.len(data_end
)
1366 data_end
= file
:read(chunk_size
)
1369 data_end
= string.sub((dataTmp1
..dataTmp2
), -chunk_size
)
1370 elseif not file_exist(openSub
.file
.path
)
1371 and openSub
.file
.stat
then
1372 vlc
.msg
.dbg("[VLSub] Read hash data from stream")
1374 local file
= vlc
.stream(openSub
.file
.uri
)
1377 vlc
.msg
.dbg("[VLSub] No stream")
1381 size
= openSub
.file
.stat
.size
1382 local decal
= size
%chunk_size
1384 data_start
= file
:read(chunk_size
)
1386 -- "Seek" to the end
1389 for i
= 1, math
.floor(((size
-decal
)/chunk_size
))-2 do
1390 file
:read(chunk_size
)
1393 data_end
= file
:read(chunk_size
)
1397 vlc
.msg
.dbg("[VLSub] Read hash data from file")
1398 local file
= io
.open(vlc
.strings
.to_codepage(openSub
.file
.path
), "rb")
1400 vlc
.msg
.dbg("[VLSub] No stream")
1404 data_start
= file
:read(chunk_size
)
1405 size
= file
:seek("end", -chunk_size
) + chunk_size
1406 data_end
= file
:read(chunk_size
)
1413 local o
,a
,b
,c
,d
,e
,f
,g
,h
1414 local hash_data
= data_start
..data_end
1415 local max_size
= 4294967296
1418 for i
= 1, #hash_data
, 8 do
1419 a
,b
,c
,d
,e
,f
,g
,h
= hash_data
:byte(i
,i
+7)
1420 lo
= lo
+ a
+ b
*256 + c
*65536 + d
*16777216
1421 hi
= hi
+ e
+ f
*256 + g
*65536 + h
*16777216
1423 if lo
> max_size
then
1424 overflow
= math
.floor(lo
/max_size
)
1425 lo
= lo
-(overflow
*max_size
)
1429 if hi
> max_size
then
1430 overflow
= math
.floor(hi
/max_size
)
1431 hi
= hi
-(overflow
*max_size
)
1435 openSub
.file
.bytesize
= size
1436 openSub
.file
.hash
= string.format("%08x%08x", hi
,lo
)
1437 vlc
.msg
.dbg("[VLSub] Video hash: "..openSub
.file
.hash
)
1438 vlc
.msg
.dbg("[VLSub] Video bytesize: "..size
)
1442 checkSession
= function()
1444 if openSub
.session
.token
== "" then
1445 openSub
.request("LogIn")
1447 openSub
.request("NoOperation")
1452 function searchHash()
1453 local sel
= input_table
["language"]:get_value()
1455 openSub
.movie
.sublanguageid
= 'all'
1457 openSub
.movie
.sublanguageid
= openSub
.conf
.languages
[sel
][1]
1460 openSub
.getMovieHash()
1462 if openSub
.file
.hash
then
1463 openSub
.checkSession()
1464 openSub
.request("SearchSubtitlesByHash")
1467 setError(lang
["mess_err_hash"])
1471 function searchIMBD()
1472 openSub
.movie
.title
= trim(input_table
["title"]:get_text())
1473 openSub
.movie
.seasonNumber
= tonumber(
1474 input_table
["seasonNumber"]:get_text())
1475 openSub
.movie
.episodeNumber
= tonumber(
1476 input_table
["episodeNumber"]:get_text())
1478 local sel
= input_table
["language"]:get_value()
1480 openSub
.movie
.sublanguageid
= 'all'
1482 openSub
.movie
.sublanguageid
= openSub
.conf
.languages
[sel
][1]
1485 if openSub
.movie
.title
~= "" then
1486 openSub
.checkSession()
1487 openSub
.request("SearchSubtitles")
1492 function display_subtitles()
1493 local mainlist
= input_table
["mainlist"]
1496 if not openSub
.itemStore
then return end
1497 if openSub
.itemStore
~= "0" then
1499 for i
, item
in ipairs(openSub
.itemStore
) do
1503 " ["..item
.SubLanguageID
.."]"..
1504 " ("..item
.SubSumCD
.." CD)", i
)
1509 setMessage("<b>"..lang
["mess_complete"]..":</b> "..
1510 #(openSub
.itemStore
).." "..lang
["mess_res"])
1514 mainlist
:add_value(lang
["mess_no_res"], 1)
1515 setMessage("<b>"..lang
["mess_complete"]..":</b> "..
1516 lang
["mess_no_res"])
1519 function get_first_sel(list
)
1520 local selection
= list
:get_selection()
1521 for index
, name
in pairs(selection
) do
1527 function find_subtitle_in_archive(archivePath
, subfileExt
)
1528 local archive
= vlc
.directory_stream(vlc
.strings
.make_uri(archivePath
))
1529 local items
= archive
:readdir()
1533 subfileExt
= "." .. subfileExt
1534 for _
, item
in pairs(items
) do
1535 if string.sub(item
:uri(), -string.len(subfileExt
)) == subfileExt
then
1542 function download_subtitles()
1543 local index
= get_first_sel(input_table
["mainlist"])
1546 setMessage(lang
["mess_no_selection"])
1550 openSub
.actionLabel
= lang
["mess_downloading"]
1552 display_subtitles() -- reset selection
1554 local item
= openSub
.itemStore
[index
]
1556 if openSub
.option
.downloadBehaviour
== 'manual'
1557 or not openSub
.file
.hasInput
then
1558 local link
= "<span style='color:#181'>"
1559 link
= link
.."<b>"..lang
["mess_dowload_link"]..":</b>"
1560 link
= link
.."</span> "
1561 link
= link
.."</span> <a href='"..
1562 item
.ZipDownloadLink
.."'>"
1563 link
= link
..item
.MovieReleaseName
.."</a>"
1570 local subfileName
= openSub
.file
.name
or ""
1572 if openSub
.option
.langExt
then
1573 subfileName
= subfileName
.."."..item
.SubLanguageID
1576 subfileName
= subfileName
.."."..item
.SubFormat
1577 local tmp_dir
= vlc
.config
.cachedir()
1578 local file_target_access
= true
1580 local tmpFileName
= dump_zip(
1581 item
.ZipDownloadLink
,
1585 if not tmpFileName
then
1586 setError(lang
["mess_save_fail"].." "..
1587 "<a href='"..item
.ZipDownloadLink
.."'>"..
1588 lang
["mess_click_link"].."</a>")
1591 vlc
.msg
.dbg("[VLsub] tmpFileName: "..tmpFileName
)
1593 local subtitleMrl
= find_subtitle_in_archive(tmpFileName
, item
.SubFormat
)
1595 if not subtitleMrl
then
1596 setMessage( lang
['mess_not_load'] )
1600 -- Determine if the path to the video file is accessible for writing
1602 local target
= openSub
.file
.dir
..subfileName
1604 if not file_touch(target
) then
1605 if openSub
.conf
.dirPath
then
1606 target
= openSub
.conf
.dirPath
..slash
..subfileName
1608 warn_tag(lang
["mess_save_warn"].." "..
1609 "<a href='"..vlc
.strings
.make_uri(
1610 openSub
.conf
.dirPath
).."'>"..
1611 lang
["mess_click_link"].."</a>")
1613 setError(lang
["mess_save_fail"].." "..
1614 "<a href='"..item
.ZipDownloadLink
.."'>"..
1615 lang
["mess_click_link"].."</a>")
1620 vlc
.msg
.dbg("[VLsub] Subtitles files: "..target
)
1622 -- Unzipped data into file target
1624 local stream
= vlc
.stream(subtitleMrl
)
1626 local subfile
= io
.open(vlc
.strings
.to_codepage(target
), "wb")
1630 data
= stream
:read(65536)
1639 if not os
.remove(tmpFileName
) then
1640 vlc
.msg
.err("[VLsub] Unable to remove temp: "..tmpFileName
)
1644 if add_sub(target
) then
1645 message
= success_tag(lang
["mess_loaded"]) .. message
1647 message
= error_tag(lang
["mess_not_load"]) .. message
1653 function dump_zip(url
, dir
, subfileName
)
1654 -- Dump zipped data in a temporary file
1655 setMessage(openSub
.actionLabel
..": "..progressBarContent(0))
1656 local resp
= get(url
)
1659 setError(lang
["mess_no_response"])
1663 local tmpFileName
= dir
..subfileName
..".gz"
1664 if not file_touch(tmpFileName
) then
1667 local tmpFile
= assert(io
.open(vlc
.strings
.to_codepage(tmpFileName
), "wb"))
1678 function add_sub(subPath
)
1679 if vlc
.item
or vlc
.input
.item() then
1680 vlc
.msg
.dbg("[VLsub] Adding subtitle :" .. subPath
)
1681 return vlc
.input
.add_subtitle(subPath
, true)
1686 --[[ Interface helpers]]--
1688 function progressBarContent(pct
)
1689 local accomplished
= math
.ceil(
1690 openSub
.option
.progressBarSize
*pct
/100)
1691 local left
= openSub
.option
.progressBarSize
- accomplished
1692 local content
= "<span style='background-color:#181;color:#181;'>"..
1693 string.rep ("-", accomplished
).."</span>"..
1694 "<span style='background-color:#fff;color:#fff;'>"..
1695 string.rep ("-", left
)..
1700 function setMessage(str
)
1701 if input_table
["message"] then
1702 input_table
["message"]:set_text(str
)
1707 function setError(mess
)
1708 setMessage(error_tag(mess
))
1711 function success_tag(str
)
1712 return "<span style='color:#181'><b>"..
1713 lang
["mess_success"]..":</b></span> "..str
..""
1716 function error_tag(str
)
1717 return "<span style='color:#B23'><b>"..
1718 lang
["mess_error"]..":</b></span> "..str
..""
1721 function warn_tag(str
)
1722 return "<span style='color:#CF0'><b>"..
1723 lang
["mess_warn"]..":</b></span> "..str
..""
1726 --[[ Network utils]]--
1729 local host
, path
= parse_url(url
)
1731 "GET "..path
.." HTTP/1.0",
1733 "User-Agent: "..openSub
.conf
.userAgentHTTP
,
1737 local request
= table.concat(header
, "\r\n")
1740 local status
, response
= http_req(host
, 80, request
)
1742 if status
== 200 then
1745 return false, status
, response
1749 function http_req(host
, port
, request
)
1750 local fd
= vlc
.net
.connect_tcp(host
, port
)
1751 if not fd
then return false end
1754 pollfds
[fd
] = vlc
.net
.POLLIN
1755 vlc
.net
.send(fd
, request
)
1756 vlc
.net
.poll(pollfds
)
1758 local chunk
= vlc
.net
.recv(fd
, 2048)
1760 local headerStr
, header
, body
1761 local contentLength
, status
1765 response
= response
..chunk
1767 headerStr
, body
= response
:match("(.-\r?\n)\r?\n(.*)")
1770 header
= parse_header(headerStr
)
1771 contentLength
= tonumber(header
["Content-Length"])
1772 status
= tonumber(header
["statuscode"])
1776 if contentLength
then
1777 bodyLenght
= #response
1778 pct
= bodyLenght
/ contentLength
* 100
1779 setMessage(openSub
.actionLabel
..": "..progressBarContent(pct
))
1780 if bodyLenght
>= contentLength
then
1785 vlc
.net
.poll(pollfds
)
1786 chunk
= vlc
.net
.recv(fd
, 1024)
1792 and header
["Location"] then
1793 local host
, path
= parse_url(trim(header
["Location"]))
1795 :gsub("^([^%s]+ )([^%s]+)", "%1"..path
)
1796 :gsub("(Host: )([^\n]*)", "%1"..host
)
1798 return http_req(host
, port
, request
)
1801 return status
, response
1804 function parse_header(data
)
1807 for name
, s
, val
in string.gmatch(
1809 "([^%s:]+)(:?)%s([^\n]+)\r?\n")
1812 header
['statuscode'] = tonumber(string.sub(val
, 1 , 3))
1820 function parse_url(url
)
1821 local url_parsed
= vlc
.strings
.url_parse(url
)
1822 return url_parsed
["host"],
1824 url_parsed
["option"]
1829 function parse_xml(data
)
1834 local op
, tag, p
, empty
, val
1835 table.insert(stack
, tree
)
1836 local resolve_xml
= vlc
.strings
.resolve_xml_special_chars
1838 for op
, tag, p
, empty
, val
in string.gmatch(
1840 "[%s\r\n\t]*<(%/?)([%w:_]+)(.-)(%/?)>"..
1841 "[%s\r\n\t]*([^<]*)[%s\r\n\t]*"
1851 if type(stack
[level
][tag]) == "nil" then
1852 stack
[level
][tag] = {}
1853 table.insert(stack
, stack
[level
][tag])
1855 if type(stack
[level
][tag][1]) == "nil" then
1857 tmp
= stack
[level
][tag]
1858 stack
[level
][tag] = nil
1859 stack
[level
][tag] = {}
1860 table.insert(stack
[level
][tag], tmp
)
1864 table.insert(stack
[level
][tag], tmp
)
1865 table.insert(stack
, tmp
)
1868 if type(stack
[level
][tag]) == "nil" then
1869 stack
[level
][tag] = {}
1871 stack
[level
][tag] = resolve_xml(val
)
1872 table.insert(stack
, {})
1875 stack
[level
][tag] = ""
1886 function parse_xmlrpc(data
)
1892 local op
, tag, p
, empty
, val
1893 local resolve_xml
= vlc
.strings
.resolve_xml_special_chars
1894 table.insert(stack
, tree
)
1896 for op
, tag, p
, empty
, val
in string.gmatch(
1898 "<(%/?)([%w:]+)(.-)(%/?)>[%s\r\n\t]*([^<]*)"
1901 if tag == "member" or tag == "array" then
1907 elseif tag == "name" then
1909 if val
~= "" then tmpTag
= resolve_xml(val
) end
1911 if type(stack
[level
][tmpTag
]) == "nil" then
1912 stack
[level
][tmpTag
] = {}
1913 table.insert(stack
, stack
[level
][tmpTag
])
1917 table.insert(stack
[level
-1], tmp
)
1921 table.insert(stack
, tmp
)
1925 stack
[level
][tmpTag
] = ""
1928 elseif tag == "array" then
1932 table.insert(stack
[level
], tmp
)
1933 table.insert(stack
, tmp
)
1934 elseif val
~= "" then
1935 stack
[level
][tmpTag
] = resolve_xml(val
)
1942 function dump_xml(data
)
1946 local convert_xml
= vlc
.strings
.convert_xml_special_chars
1948 local function parse(data
, stack
)
1949 local data_index
= {}
1955 for k
,v
in pairs(data
) do
1956 table.insert(data_index
, {k
, v
})
1957 table.sort(data_index
, function(a
, b
)
1962 for i
,tb
in pairs(data_index
) do
1965 if type(k
)=="string" then
1966 dump
= dump
.."\r\n"..string.rep(
1970 table.insert(stack
, k
)
1972 elseif type(k
)=="number" and k
~= 1 then
1973 dump
= dump
.."\r\n"..string.rep(
1976 "<"..stack
[level
]..">"
1979 if type(v
)=="table" then
1981 elseif type(v
)=="string" then
1982 dump
= dump
..(convert_xml(v
) or v
)
1983 elseif type(v
)=="number" then
1986 dump
= dump
..tostring(v
)
1989 if type(k
)=="string" then
1990 if type(v
)=="table" then
1991 dump
= dump
.."\r\n"..string.rep(
1996 dump
= dump
.."</"..k
..">"
2001 elseif type(k
)=="number" and k
~= #data
then
2002 if type(v
)=="table" then
2003 dump
= dump
.."\r\n"..string.rep(
2006 "</"..stack
[level
]..">"
2008 dump
= dump
.."</"..stack
[level
]..">"
2020 function file_touch(name
) -- test write ability
2021 if not name
or trim(name
) == ""
2022 then return false end
2024 local f
=io
.open(vlc
.strings
.to_codepage(name
) ,"w")
2033 function file_exist(name
) -- test readability
2034 if not name
or trim(name
) == ""
2035 then return false end
2036 local f
=io
.open(vlc
.strings
.to_codepage(name
), "r")
2045 function is_dir(path
)
2046 if not path
or trim(path
) == ""
2047 then return false end
2048 -- Remove slash at the end or it won't work on Windows
2049 path
= string.gsub(path
, "^(.-)[\\/]?$", "%1")
2050 local f
, _
, code
= io
.open(vlc
.strings
.to_codepage(path
), "rb")
2053 _
, _
, code
= f
:read("*a")
2058 elseif code
== 13 then
2065 function list_dir(path
)
2066 if not path
or trim(path
) == ""
2067 then return false end
2070 if not is_dir(path
) then return false end
2072 if openSub
.conf
.os
== "win" then
2073 dir_list_cmd
= io
.popen('dir /b "'..path
..'"')
2074 elseif openSub
.conf
.os
== "lin" then
2075 dir_list_cmd
= io
.popen('ls -1 "'..path
..'"')
2078 if dir_list_cmd
then
2079 for filename
in dir_list_cmd
:lines() do
2080 if string.match(filename
, "^[^%s]+.+$") then
2081 table.insert(list
, filename
)
2090 function decode_uri(str
)
2092 return str
:gsub("/", slash
)
2095 function is_window_path(path
)
2096 return string.match(path
, "^(%a:.+)$")
2099 function is_win_safe(path
)
2100 if not path
or trim(path
) == ""
2101 or not is_window_path(path
)
2102 then return false end
2103 return string.match(path
, "^%a?%:?[\\%w%p%s§¤]+$")
2107 if not str
then return "" end
2108 return string.gsub(str
, "^[\r\n%s]*(.-)[\r\n%s]*$", "%1")
2111 function remove_tag(str
)
2112 return string.gsub(str
, "{[^}]+}", "")