VLSub: Display a user visible error when the hash can't be generated
[vlc.git] / share / lua / extensions / VLSub.lua
blobc1ae5a606ce8da7b4f38faf1798360222a196383
1 --[[
2 VLSub Extension for VLC media player 1.1 and 2.0
3 Copyright 2013 Guillaume Le Maout
5 Authors: Guillaume Le Maout
6 Contact:
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.
23 --]]
26 --[[ Global var ]]--
28 -- You can set here your default language by replacing nil with
29 -- your language code (see below).Example:
30 -- language = "fre",
31 -- language = "ger",
32 -- language = "eng",
33 -- ...
35 local options = {
36 language = nil,
37 downloadBehaviour = 'save',
38 langExt = false,
39 removeTag = false,
40 showMediaInformation = true,
41 progressBarSize = 80,
42 intLang = 'eng',
43 translations_avail = {
44 eng = 'English',
45 cze = 'Czech',
46 dan = 'Danish',
47 dut = 'Nederlands',
48 fre = 'Français',
49 ell = 'Greek',
50 baq = 'Basque',
51 pob = 'Brazilian Portuguese',
52 por = 'Portuguese (Portugal)',
53 rum = 'Romanian',
54 slo = 'Slovak',
55 spa = 'Spanish',
56 swe = 'Swedish',
57 ukr = 'Ukrainian',
58 hun = 'Hungarian'
60 translation = {
61 int_all = 'All',
62 int_descr = 'Download subtitles from OpenSubtitles.org',
63 int_research = 'Research',
64 int_config = 'Config',
65 int_configuration = 'Configuration',
66 int_help = 'Help',
67 int_search_hash = 'Search by hash',
68 int_search_name = 'Search by name',
69 int_title = 'Title',
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',
75 int_close = 'Close',
76 int_ok = 'Ok',
77 int_save = 'Save',
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',
94 int_help_mess =[[
95 Download subtitles from
96 <a href='http://www.opensubtitles.org/'>
97 opensubtitles.org
98 </a> and display them while watching a video.<br>
99 <br>
100 <b><u>Usage:</u></b><br>
101 <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>
105 <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>
109 <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>
114 <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
119 number.<br>
120 <br>
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
126 start the video.<br>
127 <br>
128 <b>/!\\ Beware :</b> Existing subtitles are overwritten
129 without asking confirmation, so put them elsewhere if
130 they're important.<br>
131 <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.
141 <br>
142 <strong>Works with VLC 2.2 on mac and linux.</strong>
143 <br>
144 <strong>On windows you have to install an older version
145 of VLC (2.0.8 for example)</strong>
146 to use Vlsub:
147 <br>
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, '..
184 'please correct it',
185 mess_err_hash = 'Failed to generate hash'
189 local languages = {
190 {'alb', 'Albanian'},
191 {'ara', 'Arabic'},
192 {'arm', 'Armenian'},
193 {'baq', 'Basque'},
194 {'ben', 'Bengali'},
195 {'bos', 'Bosnian'},
196 {'bre', 'Breton'},
197 {'bul', 'Bulgarian'},
198 {'bur', 'Burmese'},
199 {'cat', 'Catalan'},
200 {'chi', 'Chinese'},
201 {'hrv', 'Croatian'},
202 {'cze', 'Czech'},
203 {'dan', 'Danish'},
204 {'dut', 'Dutch'},
205 {'eng', 'English'},
206 {'epo', 'Esperanto'},
207 {'est', 'Estonian'},
208 {'fin', 'Finnish'},
209 {'fre', 'French'},
210 {'glg', 'Galician'},
211 {'geo', 'Georgian'},
212 {'ger', 'German'},
213 {'ell', 'Greek'},
214 {'heb', 'Hebrew'},
215 {'hin', 'Hindi'},
216 {'hun', 'Hungarian'},
217 {'ice', 'Icelandic'},
218 {'ind', 'Indonesian'},
219 {'ita', 'Italian'},
220 {'jpn', 'Japanese'},
221 {'kaz', 'Kazakh'},
222 {'khm', 'Khmer'},
223 {'kor', 'Korean'},
224 {'lav', 'Latvian'},
225 {'lit', 'Lithuanian'},
226 {'ltz', 'Luxembourgish'},
227 {'mac', 'Macedonian'},
228 {'may', 'Malay'},
229 {'mal', 'Malayalam'},
230 {'mon', 'Mongolian'},
231 {'nor', 'Norwegian'},
232 {'oci', 'Occitan'},
233 {'per', 'Persian'},
234 {'pol', 'Polish'},
235 {'por', 'Portuguese'},
236 {'pob', 'Brazilian Portuguese'},
237 {'rum', 'Romanian'},
238 {'rus', 'Russian'},
239 {'scc', 'Serbian'},
240 {'sin', 'Sinhalese'},
241 {'slo', 'Slovak'},
242 {'slv', 'Slovenian'},
243 {'spa', 'Spanish'},
244 {'swa', 'Swahili'},
245 {'swe', 'Swedish'},
246 {'syr', 'Syriac'},
247 {'tgl', 'Tagalog'},
248 {'tel', 'Telugu'},
249 {'tha', 'Thai'},
250 {'tur', 'Turkish'},
251 {'ukr', 'Ukrainian'},
252 {'urd', 'Urdu'},
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 = {
259 sq = "alb",
260 ar = "ara",
261 hy = "arm",
262 eu = "baq",
263 bn = "ben",
264 bs = "bos",
265 br = "bre",
266 bg = "bul",
267 my = "bur",
268 ca = "cat",
269 zh = "chi",
270 hr = "hrv",
271 cs = "cze",
272 da = "dan",
273 nl = "dut",
274 en = "eng",
275 eo = "epo",
276 et = "est",
277 fi = "fin",
278 fr = "fre",
279 gl = "glg",
280 ka = "geo",
281 de = "ger",
282 el = "ell",
283 he = "heb",
284 hi = "hin",
285 hu = "hun",
286 is = "ice",
287 id = "ind",
288 it = "ita",
289 ja = "jpn",
290 kk = "kaz",
291 km = "khm",
292 ko = "kor",
293 lv = "lav",
294 lt = "lit",
295 lb = "ltz",
296 mk = "mac",
297 ms = "may",
298 ml = "mal",
299 mn = "mon",
300 no = "nor",
301 oc = "oci",
302 fa = "per",
303 pl = "pol",
304 pt = "por",
305 po = "pob",
306 ro = "rum",
307 ru = "rus",
308 sr = "scc",
309 si = "sin",
310 sk = "slo",
311 sl = "slv",
312 es = "spa",
313 sw = "swa",
314 sv = "swe",
315 tl = "tgl",
316 te = "tel",
317 th = "tha",
318 tr = "tur",
319 uk = "ukr",
320 ur = "urd",
321 vi = "vie"
324 local dlg = nil
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()
331 return {
332 title = "VLsub 0.10.0",
333 version = "0.10.0",
334 author = "exebetche",
335 url = 'http://www.opensubtitles.org/',
336 shortdesc = "VLsub";
337 description = options.translation.int_descr,
338 capabilities = {"menu", "input-listener" }
342 function activate()
343 vlc.msg.dbg("[VLsub] Welcome")
345 if not check_config() then
346 vlc.msg.err("[VLsub] Unsupported VLC version")
347 return false
350 if vlc.input.item() then
351 openSub.getFileInfo()
352 openSub.getMovieInfo()
355 show_main()
358 function close()
359 vlc.deactivate()
362 function deactivate()
363 vlc.msg.dbg("[VLsub] Bye bye!")
364 if dlg then
365 dlg:hide()
368 if openSub.session.token and openSub.session.token ~= "" then
369 openSub.request("LogOut")
373 function menu()
374 return {
375 lang.int_research,
376 lang.int_config,
377 lang.int_help
381 function meta_changed()
382 return false
385 function input_changed()
386 collectgarbage()
387 set_interface_main()
388 collectgarbage()
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)
413 dlg:add_button(
414 lang["int_show_help"], show_help, 1, 7, 1, 1)
415 dlg:add_button(
416 ' '..lang["int_show_conf"]..' ', show_conf, 2, 7, 1, 1)
417 dlg:add_button(
418 lang["int_dowload_sel"], download_subtitles, 3, 7, 1, 1)
419 dlg:add_button(
420 lang["int_close"], deactivate, 4, 7, 1, 1)
422 assoc_select_conf(
423 'language',
424 'language',
425 openSub.conf.languages,
427 lang["int_all"])
429 display_subtitles()
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)
454 dlg:add_label(
455 lang["int_default_lang"]..':', 1, 2, 2, 1)
456 input_table['default_language'] = dlg:add_dropdown(3, 2, 1, 1)
457 dlg:add_label(
458 lang["int_dowload_behav"]..':', 1, 3, 2, 1)
459 input_table['downloadBehaviour'] = dlg:add_dropdown(3, 3, 1, 1)
460 dlg:add_label(
461 lang["int_display_code"]..':', 1, 4, 0, 1)
462 input_table['langExt'] = dlg:add_dropdown(3, 4, 1, 1)
463 dlg:add_label(
464 lang["int_remove_tag"]..':', 1, 5, 0, 1)
465 input_table['removeTag'] = dlg:add_dropdown(3, 5, 1, 1)
467 dlg:add_label(
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)
472 dlg:add_label(
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)
481 dlg:add_button(
482 lang["int_cancel"],
483 show_main, 2, 10, 1, 1)
484 dlg:add_button(
485 lang["int_save"],
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)
497 assoc_select_conf(
498 'intLang',
499 'intLang',
500 openSub.conf.translations_avail,
502 assoc_select_conf(
503 'default_language',
504 'language',
505 openSub.conf.languages,
507 lang["int_all"])
508 assoc_select_conf(
509 'downloadBehaviour',
510 'downloadBehaviour',
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)
520 dlg:add_label(
521 string.rep ("&nbsp;", 100), 1, 2, 3, 1)
522 dlg:add_button(
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)
531 dlg:add_label(
532 string.rep ("&nbsp;", 100), 1, 2, 3, 1)
535 function trigger_menu(dlg_id)
536 if dlg_id == 1 then
537 close_dlg()
538 dlg = vlc.dialog(
539 openSub.conf.useragent)
540 interface_main()
541 elseif dlg_id == 2 then
542 close_dlg()
543 dlg = vlc.dialog(
544 openSub.conf.useragent..': '..lang["int_configuration"])
545 interface_config()
546 elseif dlg_id == 3 then
547 close_dlg()
548 dlg = vlc.dialog(
549 openSub.conf.useragent..': '..lang["int_help"])
550 interface_help()
552 collectgarbage() --~ !important
555 function show_main()
556 trigger_menu(1)
559 function show_conf()
560 trigger_menu(2)
563 function show_help()
564 trigger_menu(3)
567 function close_dlg()
568 vlc.msg.dbg("[VLSub] Closing dialog")
570 if dlg ~= nil then
571 --~ dlg:delete() -- Throw an error
572 dlg:hide()
575 dlg = nil
576 input_table = nil
577 input_table = {}
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] = {
586 cf = conf,
587 opt = option,
588 dflt = default,
589 ind = ind
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
603 return true
604 elseif b[1] == openSub.option[opt] then
605 return false
606 else
607 return a[ind] < b[ind]
609 end)
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
621 if not default then
622 default_isset = true
625 for k, l in ipairs(conf) do
626 if default_isset then
627 input_table[select_id]:add_value(l[2], k)
628 else
629 if option then
630 input_table[select_id]:add_value(l[2], k)
631 input_table[select_id]:add_value(default, 0)
632 else
633 input_table[select_id]:add_value(default, 0)
634 input_table[select_id]:add_value(l[2], k)
636 default_isset = true
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
646 eng_translation = {}
647 for k, v in pairs(openSub.option.translation) do
648 eng_translation[k] = v
651 -- Get available translation full name from code
652 trsl_names = {}
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"
659 slash = "\\"
660 else
661 openSub.conf.os = "lin"
662 slash = "/"
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 )
672 return false
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)
685 load_config()
686 else
687 vlc.msg.dbg("[VLSub] No config file")
688 getenv_lang()
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
701 if file_list then
702 for i, file_name in ipairs(file_list) do
703 local lg = string.gsub(
704 file_name,
705 "^(%w%w%w).xml$",
706 "%1")
707 if lg
708 and not openSub.option.translations_avail[lg] then
709 table.insert(translations_avail, {
711 trsl_names[lg]
717 -- Load selected translation from file
718 if openSub.option.intLang ~= "eng"
719 and not openSub.conf.translated
720 then
721 local transl_file_path = openSub.conf.localePath..
722 slash..openSub.option.intLang..".xml"
723 if file_exist(transl_file_path) then
724 vlc.msg.dbg(
725 "[VLSub] Loading translation from file: "..
726 transl_file_path)
727 load_transl(transl_file_path)
730 else
731 vlc.msg.dbg("[VLSub] Unable to find a suitable path"..
732 "to save config, please set it manually")
733 return false
736 lang = nil
737 lang = options.translation -- just a short cut
739 if not vlc.net or not vlc.net.poll then
740 dlg = vlc.dialog(
741 openSub.conf.useragent..': '..lang["mess_error"])
742 interface_no_support()
743 dlg:show()
744 return false
747 SetDownloadBehaviours()
749 -- Set table list of available translations from assoc. array
750 -- so it is sortable
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})
755 else
756 table.insert(openSub.conf.translations_avail, {k, l})
759 collectgarbage()
760 return true
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")
768 tmpFile:flush()
769 tmpFile:close()
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
779 else
780 openSub.option[key] = value
782 else
783 if value == "true" then
784 openSub.option[key] = true
785 elseif value == "false" then
786 openSub.option[key] = false
787 else
788 openSub.option[key] = value
792 collectgarbage()
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")
799 tmpFile:flush()
800 tmpFile:close()
801 openSub.option.translation = nil
803 openSub.option.translation = parse_xml(resp)
804 collectgarbage()
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]
825 else -- Windows
826 local lang_w = string.match(
827 os.setlocale("", "collate"),
828 "^[^_]+")
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()
840 local sel_val
841 local opt
842 local sel_cf
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]
847 set_translation(lg)
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]
856 opt = sel_cf.opt
858 if sel_val == 0 then
859 openSub.option[opt] = nil
860 else
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()
881 trigger_menu(1)
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
891 vlc.msg.dbg(
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)
899 tmpFile:write(resp)
900 tmpFile:flush()
901 tmpFile:close()
902 tmpFile = nil
903 else
904 return false
906 collectgarbage()
907 return true
908 else
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"])
912 return false
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"]
934 then
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 .. ")"))
943 return
946 all_trsl = parse_xml(translations_content)
947 local lg, trsl
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, {
955 trsl_names[lg]
957 input_table['intLang']:add_value(
958 trsl_names[lg],
959 #openSub.conf.translations_avail)
963 setMessage(success_tag(lang["mess_complete"]))
964 collectgarbage()
968 function set_translation(lg)
969 openSub.option.translation = nil
970 openSub.option.translation = {}
972 if lg == 'eng' then
973 for k, v in pairs(eng_translation) do
974 openSub.option.translation[k] = v
976 else
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..
982 slash..lg..".xml"
983 vlc.msg.dbg("[VLSub] Loading translation from file: "..
984 transl_file_path)
985 load_transl(transl_file_path)
986 apply_translation()
987 else
988 -- Load translation file from internet
989 if not all_trsl then
990 get_available_translations()
993 if not all_trsl or not all_trsl[lg] then
994 vlc.msg.dbg("[VLSub] Error, translation not found")
995 return false
997 openSub.option.translation = all_trsl[lg]
998 apply_translation()
999 all_trsl = nil
1003 lang = nil
1004 lang = openSub.option.translation
1005 collectgarbage()
1008 --[[ Core ]]--
1010 openSub = {
1011 itemStore = nil,
1012 actionLabel = "",
1013 conf = {
1014 url = "http://api.opensubtitles.org/xml-rpc",
1015 path = nil,
1016 userAgentHTTP = "VLSub",
1017 useragent = "VLSub 0.10.0",
1018 translations_avail = {},
1019 downloadBehaviours = nil,
1020 languages = languages
1022 option = options,
1023 session = {
1024 loginTime = 0,
1025 token = ""
1027 file = {
1028 hasInput = false,
1029 uri = nil,
1030 ext = nil,
1031 name = nil,
1032 path = nil,
1033 protocol = nil,
1034 cleanName = nil,
1035 dir = nil,
1036 hash = nil,
1037 bytesize = nil,
1038 fps = nil,
1039 timems = nil,
1040 frames = nil
1042 movie = {
1043 title = "",
1044 seasonNumber = "",
1045 episodeNumber = "",
1046 sublanguageid = ""
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)
1053 local header = {
1054 "POST "..path.." HTTP/1.0",
1055 "Host: "..host,
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
1064 local response
1065 local status, responseStr = http_req(host, 80, request)
1067 if status == 200 then
1068 response = parse_xmlrpc(responseStr)
1069 if response then
1070 if response.status == "200 OK" then
1071 return openSub.methods[methodName]
1072 .callback(response)
1073 elseif response.status == "406 No session" then
1074 openSub.request("LogIn")
1075 elseif response then
1076 setError("code '"..
1077 response.status..
1078 "' ("..status..")")
1079 return false
1081 else
1082 setError("Server not responding")
1083 return false
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)
1094 return false
1095 elseif status == 503 then
1096 setError("Server overloaded, please retry later")
1097 return false
1100 end,
1101 getMethodBase = function(methodName, param)
1102 if openSub.methods[methodName].methodName then
1103 methodName = openSub.methods[methodName].methodName
1106 local request = {
1107 methodCall={
1108 methodName=methodName,
1109 params={ param=param }}}
1111 return request
1112 end,
1113 methods = {
1114 LogIn = {
1115 params = function()
1116 openSub.actionLabel = lang["action_login"]
1117 return {
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 } }
1123 end,
1124 callback = function(resp)
1125 openSub.session.token = resp.token
1126 openSub.session.loginTime = os.time()
1127 return true
1130 LogOut = {
1131 params = function()
1132 openSub.actionLabel = lang["action_logout"]
1133 return {
1134 { value={ string=openSub.session.token } }
1136 end,
1137 callback = function()
1138 return true
1141 NoOperation = {
1142 params = function()
1143 openSub.actionLabel = lang["action_noop"]
1144 return {
1145 { value={ string=openSub.session.token } }
1147 end,
1148 callback = function(resp)
1149 return true
1152 SearchSubtitlesByHash = {
1153 methodName = "SearchSubtitles",
1154 params = function()
1155 openSub.actionLabel = lang["action_search"]
1156 setMessage(openSub.actionLabel..": "..
1157 progressBarContent(0))
1159 return {
1160 { value={ string=openSub.session.token } },
1161 { value={
1162 array={
1163 data={
1164 value={
1165 struct={
1166 member={
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 } }
1174 }}}}}}}
1176 end,
1177 callback = function(resp)
1178 openSub.itemStore = resp.data
1181 SearchSubtitles = {
1182 methodName = "SearchSubtitles",
1183 params = function()
1184 openSub.actionLabel = lang["action_search"]
1185 setMessage(openSub.actionLabel..": "..
1186 progressBarContent(0))
1188 local member = {
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 } })
1205 return {
1206 { value={ string=openSub.session.token } },
1207 { value={
1208 array={
1209 data={
1210 value={
1211 struct={
1212 member=member
1213 }}}}}}
1215 end,
1216 callback = function(resp)
1217 openSub.itemStore = resp.data
1221 getInputItem = function()
1222 return vlc.item or vlc.input.item()
1223 end,
1224 getFileInfo = function()
1225 -- Get video file path, name, extension from input uri
1226 local item = openSub.getInputItem()
1227 local file = openSub.file
1228 if not item then
1229 file.hasInput = false;
1230 file.cleanName = nil;
1231 file.protocol = nil;
1232 file.path = nil;
1233 file.ext = nil;
1234 file.uri = nil;
1235 else
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"]
1242 -- Corrections
1244 -- For windows
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(
1252 archive_path,
1253 '\063',
1254 '%%')
1255 file.path = vlc.strings.decode_uri(file.path)
1256 file.completeName = string.gsub(
1257 name_in_archive,
1258 '\063',
1259 '%%')
1260 file.completeName = vlc.strings.decode_uri(
1261 file.completeName)
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(
1266 file.path,
1267 '^(.*/)([^/]*)$')
1269 local file_stat = vlc.net.stat(file.path)
1270 if file_stat
1271 then
1272 file.stat = file_stat
1275 file.is_archive = false
1278 file.name, file.ext = string.match(
1279 file.completeName,
1280 '^([^/]-)%.?([^%.]*)$')
1282 if file.ext == "part" then
1283 file.name, file.ext = string.match(
1284 file.name,
1285 '^([^/]+)%.([^%.]+)$')
1288 file.hasInput = true;
1289 file.cleanName = string.gsub(
1290 file.name,
1291 "[%._]", " ")
1292 vlc.msg.dbg("[VLSub] file info "..(dump_xml(file)))
1294 collectgarbage()
1295 end,
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 = ""
1302 return false
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).*")
1315 if showName then
1316 openSub.movie.title = showName
1317 openSub.movie.seasonNumber = seasonNumber
1318 openSub.movie.episodeNumber = episodeNumber
1319 else
1320 openSub.movie.title = openSub.file.cleanName
1321 openSub.movie.seasonNumber = ""
1322 openSub.movie.episodeNumber = ""
1324 collectgarbage()
1325 end,
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()
1334 if not item then
1335 setError(lang["mess_no_input"])
1336 return false
1339 openSub.getFileInfo()
1341 if not openSub.file.path then
1342 setError(lang["mess_not_found"])
1343 return false
1346 local data_start = ""
1347 local data_end = ""
1348 local size
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)
1356 local dataTmp1 = ""
1357 local dataTmp2 = ""
1358 size = chunk_size
1360 data_start = file:read(chunk_size)
1362 while data_end do
1363 size = size + string.len(data_end)
1364 dataTmp1 = dataTmp2
1365 dataTmp2 = data_end
1366 data_end = file:read(chunk_size)
1367 collectgarbage()
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)
1376 if not file then
1377 vlc.msg.dbg("[VLSub] No stream")
1378 return false
1381 size = openSub.file.stat.size
1382 local decal = size%chunk_size
1384 data_start = file:read(chunk_size)
1386 -- "Seek" to the end
1387 file:read(decal)
1389 for i = 1, math.floor(((size-decal)/chunk_size))-2 do
1390 file:read(chunk_size)
1393 data_end = file:read(chunk_size)
1395 file = nil
1396 else
1397 vlc.msg.dbg("[VLSub] Read hash data from file")
1398 local file = io.open(vlc.strings.to_codepage(openSub.file.path), "rb")
1399 if not file then
1400 vlc.msg.dbg("[VLSub] No stream")
1401 return false
1404 data_start = file:read(chunk_size)
1405 size = file:seek("end", -chunk_size) + chunk_size
1406 data_end = file:read(chunk_size)
1407 file = nil
1410 -- Hash calculation
1411 local lo = size
1412 local hi = 0
1413 local o,a,b,c,d,e,f,g,h
1414 local hash_data = data_start..data_end
1415 local max_size = 4294967296
1416 local overflow
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)
1426 hi = hi+overflow
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)
1439 collectgarbage()
1440 return true
1441 end,
1442 checkSession = function()
1444 if openSub.session.token == "" then
1445 openSub.request("LogIn")
1446 else
1447 openSub.request("NoOperation")
1452 function searchHash()
1453 local sel = input_table["language"]:get_value()
1454 if sel == 0 then
1455 openSub.movie.sublanguageid = 'all'
1456 else
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")
1465 display_subtitles()
1466 else
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()
1479 if sel == 0 then
1480 openSub.movie.sublanguageid = 'all'
1481 else
1482 openSub.movie.sublanguageid = openSub.conf.languages[sel][1]
1485 if openSub.movie.title ~= "" then
1486 openSub.checkSession()
1487 openSub.request("SearchSubtitles")
1488 display_subtitles()
1492 function display_subtitles()
1493 local mainlist = input_table["mainlist"]
1494 mainlist:clear()
1496 if not openSub.itemStore then return end
1497 if openSub.itemStore ~= "0" then
1498 local nbRes = 0
1499 for i, item in ipairs(openSub.itemStore) do
1500 if next(item) then
1501 mainlist:add_value(
1502 item.SubFileName..
1503 " ["..item.SubLanguageID.."]"..
1504 " ("..item.SubSumCD.." CD)", i)
1505 nbRes = nbRes + 1
1508 if nbRes > 0 then
1509 setMessage("<b>"..lang["mess_complete"]..":</b> "..
1510 #(openSub.itemStore).." "..lang["mess_res"])
1511 return
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
1522 return index
1524 return 0
1527 function find_subtitle_in_archive(archivePath, subfileExt)
1528 local archive = vlc.directory_stream(vlc.strings.make_uri(archivePath))
1529 local items = archive:readdir()
1530 if not items then
1531 return nil
1533 subfileExt = "." .. subfileExt
1534 for _, item in pairs(items) do
1535 if string.sub(item:uri(), -string.len(subfileExt)) == subfileExt then
1536 return item:uri()
1539 return nil
1542 function download_subtitles()
1543 local index = get_first_sel(input_table["mainlist"])
1545 if index == 0 then
1546 setMessage(lang["mess_no_selection"])
1547 return false
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> &nbsp;"
1561 link = link.."</span> &nbsp;<a href='"..
1562 item.ZipDownloadLink.."'>"
1563 link = link..item.MovieReleaseName.."</a>"
1565 setMessage(link)
1566 return false
1569 local message = ""
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,
1582 tmp_dir,
1583 item.SubFileName)
1585 if not tmpFileName then
1586 setError(lang["mess_save_fail"].." &nbsp;"..
1587 "<a href='"..item.ZipDownloadLink.."'>"..
1588 lang["mess_click_link"].."</a>")
1589 return false
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'] )
1597 return false
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
1607 message = "<br>"..
1608 warn_tag(lang["mess_save_warn"].." &nbsp;"..
1609 "<a href='"..vlc.strings.make_uri(
1610 openSub.conf.dirPath).."'>"..
1611 lang["mess_click_link"].."</a>")
1612 else
1613 setError(lang["mess_save_fail"].." &nbsp;"..
1614 "<a href='"..item.ZipDownloadLink.."'>"..
1615 lang["mess_click_link"].."</a>")
1616 return false
1620 vlc.msg.dbg("[VLsub] Subtitles files: "..target)
1622 -- Unzipped data into file target
1624 local stream = vlc.stream(subtitleMrl)
1625 local data = ""
1626 local subfile = io.open(vlc.strings.to_codepage(target), "wb")
1628 while data do
1629 subfile:write(data)
1630 data = stream:read(65536)
1633 subfile:flush()
1634 subfile:close()
1636 stream = nil
1637 collectgarbage()
1639 if not os.remove(tmpFileName) then
1640 vlc.msg.err("[VLsub] Unable to remove temp: "..tmpFileName)
1643 -- load subtitles
1644 if add_sub(target) then
1645 message = success_tag(lang["mess_loaded"]) .. message
1646 else
1647 message = error_tag(lang["mess_not_load"]) .. message
1650 setMessage(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)
1658 if not resp then
1659 setError(lang["mess_no_response"])
1660 return false
1663 local tmpFileName = dir..subfileName..".gz"
1664 if not file_touch(tmpFileName) then
1665 return false
1667 local tmpFile = assert(io.open(vlc.strings.to_codepage(tmpFileName), "wb"))
1669 tmpFile:write(resp)
1670 tmpFile:flush()
1671 tmpFile:close()
1672 tmpFile = nil
1673 collectgarbage()
1675 return tmpFileName
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)
1683 return false
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)..
1696 "</span>"
1697 return content
1700 function setMessage(str)
1701 if input_table["message"] then
1702 input_table["message"]:set_text(str)
1703 dlg:update()
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]]--
1728 function get(url)
1729 local host, path = parse_url(url)
1730 local header = {
1731 "GET "..path.." HTTP/1.0",
1732 "Host: "..host,
1733 "User-Agent: "..openSub.conf.userAgentHTTP,
1737 local request = table.concat(header, "\r\n")
1739 local response
1740 local status, response = http_req(host, 80, request)
1742 if status == 200 then
1743 return response
1744 else
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
1752 local pollfds = {}
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)
1759 local response = ""
1760 local headerStr, header, body
1761 local contentLength, status
1762 local pct = 0
1764 while chunk do
1765 response = response..chunk
1766 if not header then
1767 headerStr, body = response:match("(.-\r?\n)\r?\n(.*)")
1768 if headerStr then
1769 response = body
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
1781 break
1785 vlc.net.poll(pollfds)
1786 chunk = vlc.net.recv(fd, 1024)
1789 vlc.net.close(fd)
1791 if status == 301
1792 and header["Location"] then
1793 local host, path = parse_url(trim(header["Location"]))
1794 request = request
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)
1805 local header = {}
1807 for name, s, val in string.gmatch(
1808 data,
1809 "([^%s:]+)(:?)%s([^\n]+)\r?\n")
1811 if s == "" then
1812 header['statuscode'] = tonumber(string.sub(val, 1 , 3))
1813 else
1814 header[name] = val
1817 return header
1820 function parse_url(url)
1821 local url_parsed = vlc.strings.url_parse(url)
1822 return url_parsed["host"],
1823 url_parsed["path"],
1824 url_parsed["option"]
1827 --[[ XML utils]]--
1829 function parse_xml(data)
1830 local tree = {}
1831 local stack = {}
1832 local tmp = {}
1833 local level = 0
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(
1839 data,
1840 "[%s\r\n\t]*<(%/?)([%w:_]+)(.-)(%/?)>"..
1841 "[%s\r\n\t]*([^<]*)[%s\r\n\t]*"
1842 ) do
1843 if op=="/" then
1844 if level>0 then
1845 level = level - 1
1846 table.remove(stack)
1848 else
1849 level = level + 1
1850 if val == "" then
1851 if type(stack[level][tag]) == "nil" then
1852 stack[level][tag] = {}
1853 table.insert(stack, stack[level][tag])
1854 else
1855 if type(stack[level][tag][1]) == "nil" then
1856 tmp = nil
1857 tmp = stack[level][tag]
1858 stack[level][tag] = nil
1859 stack[level][tag] = {}
1860 table.insert(stack[level][tag], tmp)
1862 tmp = nil
1863 tmp = {}
1864 table.insert(stack[level][tag], tmp)
1865 table.insert(stack, tmp)
1867 else
1868 if type(stack[level][tag]) == "nil" then
1869 stack[level][tag] = {}
1871 stack[level][tag] = resolve_xml(val)
1872 table.insert(stack, {})
1874 if empty ~= "" then
1875 stack[level][tag] = ""
1876 level = level - 1
1877 table.remove(stack)
1882 collectgarbage()
1883 return tree
1886 function parse_xmlrpc(data)
1887 local tree = {}
1888 local stack = {}
1889 local tmp = {}
1890 local tmpTag = ""
1891 local level = 0
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(
1897 data,
1898 "<(%/?)([%w:]+)(.-)(%/?)>[%s\r\n\t]*([^<]*)"
1899 ) do
1900 if op=="/" then
1901 if tag == "member" or tag == "array" then
1902 if level>0 then
1903 level = level - 1
1904 table.remove(stack)
1907 elseif tag == "name" then
1908 level = level + 1
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])
1914 else
1915 tmp = nil
1916 tmp = {}
1917 table.insert(stack[level-1], tmp)
1919 stack[level] = nil
1920 stack[level] = tmp
1921 table.insert(stack, tmp)
1923 if empty ~= "" then
1924 level = level - 1
1925 stack[level][tmpTag] = ""
1926 table.remove(stack)
1928 elseif tag == "array" then
1929 level = level + 1
1930 tmp = nil
1931 tmp = {}
1932 table.insert(stack[level], tmp)
1933 table.insert(stack, tmp)
1934 elseif val ~= "" then
1935 stack[level][tmpTag] = resolve_xml(val)
1938 collectgarbage()
1939 return tree
1942 function dump_xml(data)
1943 local level = 0
1944 local stack = {}
1945 local dump = ""
1946 local convert_xml = vlc.strings.convert_xml_special_chars
1948 local function parse(data, stack)
1949 local data_index = {}
1950 local k
1951 local v
1952 local i
1953 local tb
1955 for k,v in pairs(data) do
1956 table.insert(data_index, {k, v})
1957 table.sort(data_index, function(a, b)
1958 return a[1] < b[1]
1959 end)
1962 for i,tb in pairs(data_index) do
1963 k = tb[1]
1964 v = tb[2]
1965 if type(k)=="string" then
1966 dump = dump.."\r\n"..string.rep(
1967 " ",
1968 level)..
1969 "<"..k..">"
1970 table.insert(stack, k)
1971 level = level + 1
1972 elseif type(k)=="number" and k ~= 1 then
1973 dump = dump.."\r\n"..string.rep(
1974 " ",
1975 level-1)..
1976 "<"..stack[level]..">"
1979 if type(v)=="table" then
1980 parse(v, stack)
1981 elseif type(v)=="string" then
1982 dump = dump..(convert_xml(v) or v)
1983 elseif type(v)=="number" then
1984 dump = dump..v
1985 else
1986 dump = dump..tostring(v)
1989 if type(k)=="string" then
1990 if type(v)=="table" then
1991 dump = dump.."\r\n"..string.rep(
1992 " ",
1993 level-1)..
1994 "</"..k..">"
1995 else
1996 dump = dump.."</"..k..">"
1998 table.remove(stack)
1999 level = level - 1
2001 elseif type(k)=="number" and k ~= #data then
2002 if type(v)=="table" then
2003 dump = dump.."\r\n"..string.rep(
2004 " ",
2005 level-1)..
2006 "</"..stack[level]..">"
2007 else
2008 dump = dump.."</"..stack[level]..">"
2013 parse(data, stack)
2014 collectgarbage()
2015 return dump
2018 --[[ Misc utils]]--
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")
2025 if f~=nil then
2026 io.close(f)
2027 return true
2028 else
2029 return false
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")
2037 if f~=nil then
2038 io.close(f)
2039 return true
2040 else
2041 return false
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")
2052 if f then
2053 _, _, code = f:read("*a")
2054 f:close()
2055 if code == 21 then
2056 return true
2058 elseif code == 13 then
2059 return true
2062 return false
2065 function list_dir(path)
2066 if not path or trim(path) == ""
2067 then return false end
2068 local dir_list_cmd
2069 local list = {}
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)
2084 return list
2085 else
2086 return false
2090 function decode_uri(str)
2091 vlc.msg.err(slash)
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§¤]+$")
2106 function trim(str)
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, "{[^}]+}", "")