2 VLSub Extension for VLC media player 1.1 and 2.0
3 Copyright 2010 - 2013 Guillaume Le Maout
5 Authors: Guillaume Le Maout
6 Contact: http://addons.videolan.org/messages/?action=newmessage&username=exebetche
7 Bug report: http://addons.videolan.org/content/show.php/?content=148752
9 This program is free software; you can redistribute it and/or modify
10 it under the terms of the GNU General Public License as published by
11 the Free Software Foundation; either version 2 of the License, or
12 (at your option) any later version.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
25 return { title = "VLsub 0.9" ;
27 author = "exebetche" ;
28 url = 'http://www.opensubtitles.org/';
30 description = "<center><b>VLsub</b></center>"
31 .. "Dowload subtitles from OpenSubtitles.org" ;
32 capabilities = { "input-listener", "meta-listener" }
40 --~ conflocation = 'subdownloader.conf'
41 url = "http://api.opensubtitles.org/xml-rpc"
44 --~ default_language = "fre"
45 default_language = nil
46 refresh_toggle = false
48 function set_default_language()
49 if default_language then
50 for k,v in ipairs(languages) do
51 if v[2] == default_language then
52 table.insert(languages, 1, v)
60 vlc.msg.dbg("[VLsub] Welcome")
61 set_default_language()
64 openSub.request("LogIn")
72 vlc.msg.dbg("[VLsub] Bye bye!")
79 function meta_changed()
83 function input_changed()
89 function update_fields()
91 openSub.getMovieInfo()
93 if openSub.movie.name ~= nil then
94 widget.get("title").input:set_text(openSub.movie.name)
97 if openSub.movie.seasonNumber ~= nil then
98 widget.get("season").input:set_text(openSub.movie.seasonNumber)
101 if openSub.movie.episodeNumber ~= nil then
102 widget.get("episode").input:set_text(openSub.movie.episodeNumber)
110 url = "http://api.opensubtitles.org/xml-rpc",
111 userAgentHTTP = "VLSub",
112 useragent = "VLSub 0.9",
148 IDSubMovieFile = nil,
154 request = function(methodName)
155 local params = openSub.methods[methodName].params()
156 local reqTable = openSub.getMethodBase(methodName, params)
157 local request = "<?xml version='1.0'?>"..dump_xml(reqTable)
158 local host, path = parse_url(openSub.conf.url)
160 "POST "..path.." HTTP/1.1",
162 "User-Agent: "..openSub.conf.userAgentHTTP,
163 "Content-Type: text/xml",
164 "Content-Length: "..string.len(request),
168 request = table.concat(header, "\r\n")..request
171 local status, responseStr = http_req(host, 80, request)
173 if status == 200 then
174 response = parse_xmlrpc(responseStr)
175 --~ vlc.msg.dbg(responseStr)
176 if (response and response.status == "200 OK") then
177 return openSub.methods[methodName].callback(response)
179 setError("code "..response.status.."("..status..")")
182 setError("Server not responding")
185 elseif status == 401 then
186 setError("Request unauthorized")
188 response = parse_xmlrpc(responseStr)
189 if openSub.session.token ~= response.token then
190 setMessage("Session expired, retrying")
191 openSub.session.token = response.token
192 openSub.request(methodName)
195 elseif status == 503 then
196 setError("Server overloaded, please retry later")
201 getMethodBase = function(methodName, param)
202 if openSub.methods[methodName].methodName then
203 methodName = openSub.methods[methodName].methodName
208 methodName=methodName,
209 params={ param=param }}}
216 openSub.actionLabel = "Logging in"
218 { value={ string=openSub.conf.username } },
219 { value={ string=openSub.conf.password } },
220 { value={ string=openSub.conf.language } },
221 { value={ string=openSub.conf.useragent } }
224 callback = function(resp)
225 openSub.session.token = resp.token
226 openSub.session.loginTime = os.time()
232 openSub.actionLabel = "Logging out"
234 { value={ string=openSub.session.token } }
237 callback = function()
244 { value={ string=openSub.session.token } }
247 callback = function()
251 SearchSubtitlesByHash = {
252 methodName = "SearchSubtitles",
254 openSub.actionLabel = "Searching subtitles"
255 setMessage(openSub.actionLabel..": "..progressBarContent(0))
258 { value={ string=openSub.session.token } },
265 { name="sublanguageid", value={ string=openSub.sub.languageid } },
266 { name="moviehash", value={ string=openSub.file.hash } },
267 { name="moviebytesize", value={ double=openSub.file.bytesize } } }}}}}}}
270 callback = function(resp)
271 openSub.itemStore = resp.data
273 if openSub.itemStore ~= "0" then
276 openSub.itemStore = nil
282 methodName = "SearchSubtitles",
284 openSub.actionLabel = "Searching subtitles"
285 setMessage(openSub.actionLabel..": "..progressBarContent(0))
288 { name="sublanguageid", value={ string=openSub.sub.languageid } },
289 { name="query", value={ string=openSub.movie.name } } }
292 if openSub.movie.season ~= nil then
293 table.insert(member, { name="season", value={ string=openSub.movie.season } })
296 if openSub.movie.episode ~= nil then
297 table.insert(member, { name="episode", value={ string=openSub.movie.episode } })
301 { value={ string=openSub.session.token } },
311 callback = function(resp)
312 openSub.itemStore = resp.data
314 if openSub.itemStore ~= "0" then
317 openSub.itemStore = nil
323 getInputItem = function()
324 return vlc.item or vlc.input.item()
326 getFileInfo = function()
327 local item = openSub.getInputItem()
328 local file = openSub.file
330 file.hasInput = false;
334 local parsed_uri = vlc.net.url_parse(item:uri())
335 file.uri = item:uri()
336 file.protocol = parsed_uri["protocol"]
337 file.path = vlc.strings.decode_uri(parsed_uri["path"])
338 --correction needed for windows
339 local windowPath = string.match(file.path, "^/(%a:/.+)$")
341 file.path = windowPath
343 file.dir, file.completeName = string.match(file.path, "^([^\n]-/?)([^/]+)$")
344 file.name, file.ext = string.match(file.path, "([^/]-)%.?([^%.]*)$")
346 if file.ext == "part" then
347 file.name, file.ext = string.match(file.name, "^([^/]+)%.([^%.]+)$")
349 file.hasInput = true;
350 file.cleanName = string.gsub(file.name, "[%._]", " ")
351 vlc.msg.dbg(file.cleanName)
354 getMovieInfo = function()
355 if not openSub.file.name then
356 openSub.movie.name = ""
357 openSub.movie.seasonNumber = ""
358 openSub.movie.episodeNumber = ""
362 local showName, seasonNumber, episodeNumber = string.match(openSub.file.cleanName, "(.+)[sS](%d%d)[eE](%d%d).*")
365 showName, seasonNumber, episodeNumber = string.match(openSub.file.cleanName, "(.+)(%d)[xX](%d%d).*")
369 openSub.movie.name = showName
370 openSub.movie.seasonNumber = seasonNumber
371 openSub.movie.episodeNumber = episodeNumber
373 openSub.movie.name = openSub.file.cleanName
374 openSub.movie.seasonNumber = ""
375 openSub.movie.episodeNumber = ""
377 vlc.msg.dbg(openSub.movie.name)
380 getMovieHash = function()
381 openSub.actionLabel = "Calculating movie hash"
382 setMessage(openSub.actionLabel..": "..progressBarContent(0))
384 local item = openSub.getInputItem()
387 setError("Please use this method during playing")
391 openSub.getFileInfo()
392 if openSub.file.protocol ~= "file" then
393 setError("This method works with local file only (for now)")
397 local path = openSub.file.path
399 setError("File not found")
403 local file = assert(io.open(path, "rb"))
405 setError("File not found")
411 local a,b,c,d = file:read(4):byte(1,4)
412 lo = lo + a + b*256 + c*65536 + d*16777216
413 a,b,c,d = file:read(4):byte(1,4)
414 hi = hi + a + b*256 + c*65536 + d*16777216
415 while lo>=4294967296 do
419 while hi>=4294967296 do
423 local size = file:seek("end", -65536) + 65536
425 local a,b,c,d = file:read(4):byte(1,4)
426 lo = lo + a + b*256 + c*65536 + d*16777216
427 a,b,c,d = file:read(4):byte(1,4)
428 hi = hi + a + b*256 + c*65536 + d*16777216
429 while lo>=4294967296 do
433 while hi>=4294967296 do
438 while lo>=4294967296 do
442 while hi>=4294967296 do
446 openSub.file.bytesize = size
447 openSub.file.hash = string.format("%08x%08x", hi,lo)
451 loadSubtitles = function(url, SubFileName, target)
452 openSub.actionLabel = "Downloading subtitle"
453 setMessage(openSub.actionLabel..": "..progressBarContent(0))
454 local subfileURI = nil
455 local resp = get(url)
457 local tmpFileName = openSub.file.dir..SubFileName..".zip"
458 subfileURI = "zip://"..make_uri(tmpFileName, true).."!/"..SubFileName
459 local tmpFile = assert(io.open(tmpFileName, "wb"))
465 local stream = vlc.stream(subfileURI)
467 local subfile = assert(io.open(target, "w")) -- FIXME: check for file presence before overwrite (maybe ask what to do)
470 if openSub.conf.removeTag then
471 subfile:write(remove_tag(data).."\n")
473 subfile:write(data.."\n")
475 data = stream:readline()
482 subfileURI = make_uri(target, true)
484 os.remove(tmpFileName)
487 if vlc.item or vlc.input.item() then
488 vlc.msg.dbg("Adding subtitle :" .. subfileURI)
489 vlc.input.add_subtitle(subfileURI)
490 setMessage("Success: Subtitles loaded.")
492 setError("No current input, unable to add subtitles "..target)
497 function make_uri(str, encode)
498 local windowdrive = string.match(str, "^(%a:/).+$")
500 local encodedPath = ""
501 for w in string.gmatch(str, "/([^/]+)") do
503 encodedPath = encodedPath.."/"..vlc.strings.encode_uri_component(w)
508 return "file:///"..windowdrive..str
510 return "file://"..str
514 function download_selection()
515 local selection = widget.getVal("mainlist")
516 if #selection > 0 and openSub.itemStore then
517 download_subtitles(selection)
521 function searchHash()
522 openSub.sub.languageid = languages[widget.getVal("language")][2]
524 message = widget.get("message")
525 if message.display == "none" then
526 message.display = "block"
527 widget.set_interface(interface)
530 openSub.getMovieHash()
532 if openSub.file.hash then
533 openSub.request("SearchSubtitlesByHash")
538 function searchIMBD()
539 openSub.movie.name = trim(widget.getVal("title"))
540 openSub.movie.season = tonumber(widget.getVal("season"))
541 openSub.movie.episode = tonumber(widget.getVal("episode"))
542 openSub.sub.languageid = languages[widget.getVal("language")][2]
544 message = widget.get("message")
545 if message.display == "none" then
546 message.display = "block"
547 widget.set_interface(interface)
550 if openSub.file.name ~= "" then
551 openSub.request("SearchSubtitles")
556 function display_subtitles()
557 local list = "mainlist"
558 widget.setVal(list) --~ Reset list
559 if openSub.itemStore then
560 for i, item in ipairs(openSub.itemStore) do
561 widget.setVal(list, item.SubFileName.." ["..item.SubLanguageID.."] ("..item.SubSumCD.." CD)")
564 widget.setVal(list, "No result")
568 function download_subtitles(selection)
569 local list = "mainlist"
570 widget.resetSel(list) -- reset selection
571 local index = selection[1][1]
572 local item = openSub.itemStore[index]
573 local subfileTarget = ""
575 if openSub.conf.justgetlink
576 or not (vlc.item or vlc.input.item())
577 or not openSub.file.dir
578 or not openSub.file.name then
579 setMessage("Link : <a href='"..item.ZipDownloadLink.."'>"..item.ZipDownloadLink.."</a>")
581 subfileTarget = openSub.file.dir..openSub.file.name.."."..item.SubLanguageID.."."..item.SubFormat
582 vlc.msg.dbg("subfileTarget: "..subfileTarget)
583 openSub.loadSubtitles(item.ZipDownloadLink, item.SubFileName, subfileTarget)
590 registered_table = {},
592 set_node = function(node, parent)
593 local left = parent.left
594 for k, l in pairs(node) do --parse items
595 local tmpTop = parent.height
597 local ltmpLeft = l.left
598 local ltmpTop = l.top
599 local tmphidden = l.hidden
601 l.top = parent.height + parent.top
605 if l.display == "none" or parent.hidden then
611 if l.type == "div" then --that's a container
612 l.display = (l.display or "block")
614 for _, newNode in ipairs(l.content) do --parse lines
615 widget.set_node(newNode, l)
616 l.height = l.height+1
618 l.height = l.height - 1
620 else --that's an item
621 l.display = (l.display or "inline")
627 if tmphidden and not l.hidden then --~ create
629 elseif not tmphidden and l.hidden then --~ destroy
633 if not l.hidden and (ltmpTop ~= l.top or ltmpLeft ~= l.left) then
634 if l.input then --~ destroy
642 --~ Store reference ID
643 if l.id and not widget.registered_table[l.id] then
644 widget.registered_table[l.id] = l
647 if l.display == "block" then
648 parent.height = parent.height + (l.height or 1)
650 elseif l.display == "none" then
651 parent.height = (tmpTop or parent.height)
652 left = (tmpLeft or left)
653 elseif l.display == "inline" then
654 left = left + (l.width or 1)
658 set_interface = function(intf_map)
659 local root = {left = 1, top = 0, height = 0, hidden = false}
660 widget.set_node(intf_map, root)
661 widget.force_refresh()
663 force_refresh = function() --~ Hacky
664 if refresh_toggle then
665 refresh_toggle = false
666 dlg:set_title(openSub.conf.useragent)
668 refresh_toggle = true
669 dlg:set_title(openSub.conf.useragent.." ")
672 destroy = function(w)
673 dlg:del_widget(w.input)
674 if widget.registered_table[w.id] then
675 widget.registered_table[w.id] = nil
680 if w.type == "button" then
681 cur_widget = dlg:add_button(w.value or "", w.callback, w.left, w.top, w.width or 1, w.height or 1)
682 elseif w.type == "label" then
683 cur_widget = dlg:add_label(w.value or "", w.left, w.top, w.width or 1, w.height or 1)
684 elseif w.type == "html" then
685 cur_widget = dlg:add_html(w.value or "", w.left, w.top, w.width or 1, w.height or 1)
686 elseif w.type == "text_input" then
687 cur_widget = dlg:add_text_input(w.value or "", w.left, w.top, w.width or 1, w.height or 1)
688 elseif w.type == "password" then
689 cur_widget = dlg:add_password(w.value or "", w.left, w.top, w.width or 1, w.height or 1)
690 elseif w.type == "check_box" then
691 cur_widget = dlg:add_check_box(w.value or "", w.left, w.top, w.width or 1, w.height or 1)
692 elseif w.type == "dropdown" then
693 cur_widget = dlg:add_dropdown(w.left, w.top, w.width or 1, w.height or 1)
694 elseif w.type == "list" then
695 cur_widget = dlg:add_list(w.left, w.top, w.width or 1, w.height or 1)
696 elseif w.type == "image" then
700 if w.type == "dropdown" or w.type == "list" then
701 if type(w.value) == "table" then
702 for k, l in ipairs(w.value) do
703 if type(l) == "table" then
704 cur_widget:add_value(l[1], k)
706 cur_widget:add_value(l, k)
712 if w.type and w.type ~= "div" then
717 return widget.registered_table[h]
719 setVal = function(h, val, index)
720 widget.set_val(widget.registered_table[h], val, index)
722 set_val = function(w, val, index)
723 local input = w.input
730 if type(val) == "string" then
734 elseif t == "check_box" then
735 if type(val) == "bool" then
736 input:set_checked(val)
740 elseif t == "dropdown" or t == "list" then
741 if val and index then
742 input:add_value(val, index)
744 elseif val and not index then
745 if type(val) == "table" then
746 for k, l in ipairs(val) do
747 input:add_value(l, k)
748 table.insert(w.value, l)
751 input:add_value(val, #w.value+1)
752 table.insert(w.value, val)
754 elseif not val and not index then
761 getVal = function(h, typeval)
762 if not widget.registered_table[h] then print(h) return false end
763 return widget.get_val(widget.registered_table[h], typeval)
765 get_val = function(w, typeval)
766 local input = w.input
774 return input:get_text()
775 elseif t == "check_box" then
776 if typeval == "checked" then
777 return input:get_checked()
779 return input:get_text()
781 elseif t == "dropdown" then
782 return input:get_value()
783 elseif t == "list" then
784 local selection = input:get_selection()
787 for index, name in pairs(selection)do
788 table.insert(output, {index, name})
793 resetSel = function(h, typeval)
794 local w = widget.registered_table[h]
797 widget.set_val(w, val)
801 function create_dialog()
802 dlg = vlc.dialog(openSub.conf.useragent)
803 widget.set_interface(interface)
807 function toggle_help()
808 helpMessage = widget.get("helpMessage")
809 if helpMessage.display == "block" then
810 helpMessage.display = "none"
811 elseif helpMessage.display == "none" then
812 helpMessage.display = "block"
815 widget.set_interface(interface)
819 function set_interface() --~ old
820 local method_index = widget.getVal("method")
821 local method_id = methods[method_index][2]
822 if tmp_method_id then
823 if tmp_method_id == method_id then
826 widget.get(tmp_method_id).display = "none"
828 openSub.request("LogIn")
830 tmp_method_id = method_id
831 widget.get(method_id).display = "block"
832 widget.set_interface(interface)
835 if method_id == "hash" then
837 elseif method_id == "imdb" then
838 if openSub.file.name and not hasAssociatedResult() then
840 widget.get("title").input:set_text(openSub.movie.name)
841 widget.get("season").input:set_text(openSub.movie.seasonNumber)
842 widget.get("episode").input:set_text(openSub.movie.episodeNumber)
847 function progressBarContent(pct)
848 local content = "<span style='background-color:#181;color:#181;'>"
849 local accomplished = math.ceil(progressBarSize*pct/100)
851 local left = progressBarSize - accomplished
852 content = content .. string.rep ("-", accomplished)
853 content = content .. "</span><span style='background-color:#fff;color:#fff;'>"
854 content = content .. string.rep ("-", left)
855 content = content .. "</span>"
859 function setError(str)
860 setMessage("<span style='color:#B23'>Error: "..str.."</span>")
863 function setMessage(str)
864 if widget.get("message") then
865 widget.setVal("message", str)
872 function file_exists(name)
873 local f=io.open(name ,"r")
876 vlc.msg.dbg("File found!" .. name)
879 vlc.msg.dbg("File not found. "..name)
884 function wait(seconds)
885 local _start = os.time()
886 local _end = _start+seconds
887 while (_end ~= os.time()) do
892 if not str then return "" end
893 return string.gsub(str, "^%s*(.-)%s*$", "%1")
896 function remove_tag(str)
897 return string.gsub(str, "{[^}]+}", "")
903 local host, path = parse_url(url)
905 "GET "..path.." HTTP/1.1",
907 "User-Agent: "..openSub.conf.userAgentHTTP,
911 local request = table.concat(header, "\r\n")
914 local status, response = http_req(host, 80, request)
916 if status == 200 then
923 function http_req(host, port, request)
924 local fd = vlc.net.connect_tcp(host, port)
928 pollfds[fd] = vlc.net.POLLIN
929 vlc.net.send(fd, request)
930 vlc.net.poll(pollfds)
932 local response = vlc.net.recv(fd, 1024)
933 local headerStr, body = string.match(response, "(.-\r?\n)\r?\n(.*)")
934 local header = parse_header(headerStr)
935 local contentLength = tonumber(header["Content-Length"])
936 local TransferEncoding = header["Transfer-Encoding"]
937 local status = tonumber(header["statuscode"])
938 local bodyLenght = string.len(body)
941 if status ~= 200 then return status end
943 while contentLength and bodyLenght < contentLength do
944 vlc.net.poll(pollfds)
945 response = vlc.net.recv(fd, 1024)
948 body = body..response
953 bodyLenght = string.len(body)
954 pct = bodyLenght / contentLength * 100
955 setMessage(openSub.actionLabel..": "..progressBarContent(pct))
964 function parse_header(data)
967 for name, s, val in string.gfind(data, "([^%s:]+)(:?)%s([^\n]+)\r?\n") do
968 if s == "" then header['statuscode'] = tonumber(string.sub (val, 1 , 3))
969 else header[name] = val end
974 function parse_url(url)
975 local url_parsed = vlc.net.url_parse(url)
976 return url_parsed["host"], url_parsed["path"], url_parsed["option"]
981 function parse_xml(data)
987 table.insert(stack, tree)
989 for op, tag, p, empty, val in string.gmatch(data, "<(%/?)([%w:]+)(.-)(%/?)>[%s\r\n\t]*([^<]*)") do
998 if type(stack[level][tag]) == "nil" then
999 stack[level][tag] = {}
1000 table.insert(stack, stack[level][tag])
1002 if type(stack[level][tag][1]) == "nil" then
1004 tmp = stack[level][tag]
1005 stack[level][tag] = nil
1006 stack[level][tag] = {}
1007 table.insert(stack[level][tag], tmp)
1011 table.insert(stack[level][tag], tmp)
1012 table.insert(stack, tmp)
1015 if type(stack[level][tag]) == "nil" then
1016 stack[level][tag] = {}
1018 stack[level][tag] = vlc.strings.resolve_xml_special_chars(val)
1019 table.insert(stack, {})
1022 stack[level][tag] = ""
1031 function parse_xmlrpc(data)
1037 table.insert(stack, tree)
1039 for op, tag, p, empty, val in string.gmatch(data, "<(%/?)([%w:]+)(.-)(%/?)>[%s\r\n\t]*([^<]*)") do
1041 if tag == "member" or tag == "array" then
1047 elseif tag == "name" then
1049 if val~=""then tmpTag = vlc.strings.resolve_xml_special_chars(val) end
1051 if type(stack[level][tmpTag]) == "nil" then
1052 stack[level][tmpTag] = {}
1053 table.insert(stack, stack[level][tmpTag])
1057 table.insert(stack[level-1], tmp)
1061 table.insert(stack, tmp)
1065 stack[level][tmpTag] = ""
1068 elseif tag == "array" then
1072 table.insert(stack[level], tmp)
1073 table.insert(stack, tmp)
1074 elseif val ~= "" then
1075 stack[level][tmpTag] = vlc.strings.resolve_xml_special_chars(val)
1081 function dump_xml(data)
1086 local function parse(data, stack)
1087 for k,v in pairs(data) do
1088 if type(k)=="string" then
1089 dump = dump.."\r\n"..string.rep (" ", level).."<"..k..">"
1090 table.insert(stack, k)
1092 elseif type(k)=="number" and k ~= 1 then
1093 dump = dump.."\r\n"..string.rep (" ", level-1).."<"..stack[level]..">"
1096 if type(v)=="table" then
1098 elseif type(v)=="string" then
1099 dump = dump..vlc.strings.convert_xml_special_chars(v)
1100 elseif type(v)=="number" then
1104 if type(k)=="string" then
1105 if type(v)=="table" then
1106 dump = dump.."\r\n"..string.rep (" ", level-1).."</"..k..">"
1108 dump = dump.."</"..k..">"
1113 elseif type(k)=="number" and k ~= #data then
1114 if type(v)=="table" then
1115 dump = dump.."\r\n"..string.rep (" ", level-1).."</"..stack[level]..">"
1117 dump = dump.."</"..stack[level]..">"
1130 {'Albanian', 'alb'},
1132 {'Armenian', 'arm'},
1135 {'Bulgarian', 'bul'},
1138 {'Chinese (China)', 'chi'},
1139 {'Croatian', 'hrv'},
1143 {'English (US)', 'eng'},
1144 {'English (UK)', 'bre'},
1145 {'Esperanto', 'epo'},
1146 {'Estonian', 'est'},
1149 {'Galician', 'glg'},
1150 {'Georgian', 'geo'},
1154 {'Hungarian', 'hun'},
1155 {'Indonesian', 'ind'},
1157 {'Japanese', 'jpn'},
1161 {'Lithuanian', 'lit'},
1162 {'Luxembourgish', 'ltz'},
1163 {'Macedonian', 'mac'},
1164 {'Norwegian', 'nor'},
1167 {'Portuguese (Portugal)', 'por'},
1168 {'Portuguese (Brazil)', 'pob'},
1169 {'Romanian', 'rum'},
1173 {'Slovenian', 'slv'},
1174 {'Spanish (Spain)', 'spa'},
1178 {'Ukrainian', 'ukr'},
1179 {'Vietnamese', 'vie'}
1183 {"Video hash", "hash"},
1193 { type = "label", value = "Language:" },
1194 { type = "dropdown", value = languages, id = "language" , width = 2 },
1195 { type = "button", value = "Search by hash", callback = searchHash },
1204 { type = "label", value = "Title:"},
1205 { type = "text_input", value = openSub.movie.name or "", id = "title", width = 2 },
1206 { type = "button", value = "Search by name", callback = searchIMBD }
1208 { type = "label", value = "Season (series):"},
1209 { type = "text_input", value = openSub.movie.seasonNumber or "", id = "season", width = 2 }
1211 { type = "label", value = "Episode (series):"},
1212 { type = "text_input", value = openSub.movie.episodeNumber or "", id = "episode", width = 2 }
1214 { type = "list", width = 4, id = "mainlist" }
1222 { type = "button", value = "Help", callback = toggle_help },
1223 { type = "span", width = 1},
1224 { type = "button", value = "Download", callback = download_selection },
1225 { type = "button", value = "Close", callback = close }
1234 { type = "html", width = 4,
1237 " Download subtittles from <a href='http://www.opensubtitles.org/'>opensubtitles.org</a> and display them while watching a video.<br>"..
1239 " <b><u>Usage:</u></b><br>"..
1241 " VLSub is meant to be used while your watching the video, so start it first (if nothing is playing you will get a link to download the subtitles in your browser).<br>"..
1243 " Choose the language for your subtitles and click on the button corresponding to one of the two research method provided by VLSub:<br>"..
1245 " <b>Method 1: Search by hash</b><br>"..
1246 " It is recommended to try this method first, because it performs a research based on the video file print, so you can find subtitles synchronized with your video.<br>"..
1248 " <b>Method 2: Search by name</b><br>"..
1249 " If you have no luck with the first method, just check the title is correct before clicking. If you search subtitles for a serie, you can also provide a season and episode number.<br>"..
1251 " <b>Downloading Subtitles</b><br>"..
1252 " Select one subtitle in the list and click on 'Download'.<br>"..
1253 " It will be put in the same directory that your video, with the same name (different extension)"..
1254 " so Vlc will load them automatically the next time you'll start the video.<br>"..
1256 " <b>/!\\ Beware :</b> Existing subtitles are overwrited without asking confirmation, so put them elsewhere if thet're important."
1257 , id = "helpMessage"