]> git.sesse.net Git - vlc/commitdiff
Add VLSub to find subtitles
authorJean-Baptiste Kempf <jb@videolan.org>
Tue, 23 Apr 2013 16:36:41 +0000 (18:36 +0200)
committerJean-Baptiste Kempf <jb@videolan.org>
Tue, 23 Apr 2013 16:36:41 +0000 (18:36 +0200)
Ref #8131

share/lua/extensions/VLSub.lua [new file with mode: 0644]

diff --git a/share/lua/extensions/VLSub.lua b/share/lua/extensions/VLSub.lua
new file mode 100644 (file)
index 0000000..559ed17
--- /dev/null
@@ -0,0 +1,1383 @@
+--[[
+ VLSub Extension for VLC media player 1.1 and 2.0
+ Copyright 2010 - 2013 Guillaume Le Maout
+
+ Authors:  Guillaume Le Maout
+ Contact: http://addons.videolan.org/messages/?action=newmessage&username=exebetche
+ Bug report: http://addons.videolan.org/content/show.php/?content=148752
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+--]]
+
+-- Extension description
+function descriptor()
+       return { title = "VLsub" ;
+               version = "0.8" ;
+               author = "exebetche" ;
+               url = 'http://www.opensubtitles.org/';
+               shortdesc = "VLsub";
+               description = "<center><b>VLsub</b></center>"
+                               .. "Download subtitles from OpenSubtitles.org" ;
+               capabilities = { "input-listener", "meta-listener" }
+       }
+end
+
+-- Global variables
+dlg = nil     -- Dialog
+conflocation = 'subdownloader.conf'
+url = "http://api.opensubtitles.org/xml-rpc"
+progressBarSize = 40
+interface_state = 0
+result_state = {}
+default_language = "eng"
+
+function set_default_language()
+       if default_language then
+               for k,v in ipairs(languages) do
+                       if v[2] == default_language then
+                               table.insert(languages, 1, v)
+                               return true
+                       end
+               end
+       end
+end
+
+function activate()
+    vlc.msg.dbg("[VLsub] Welcome")
+    set_default_language()
+    create_dialog()
+       openSub.getFileInfo()
+       openSub.getMovieInfo()
+    --~ openSub.request("LogIn")
+end
+
+function deactivate()
+       if openSub.token then
+               openSub.LogOut()
+       end
+    vlc.msg.dbg("[VLsub] Bye bye!")
+end
+
+function close()
+    vlc.deactivate()
+end
+
+function meta_changed()
+       openSub.getFileInfo()
+       openSub.getMovieInfo()
+       if tmp_method_id == "hash" then
+               searchHash()
+       elseif tmp_method_id == "imdb" then
+               widget.get("title").input:set_text(openSub.movie.name)
+               widget.get("season").input:set_text(openSub.movie.seasonNumber)
+               widget.get("episode").input:set_text(openSub.movie.episodeNumber)
+       end
+end
+
+function input_changed()
+       return false
+end
+
+openSub = {
+       itemStore = nil,
+       actionLabel = "",
+       conf = {
+               url = "http://api.opensubtitles.org/xml-rpc",
+               userAgentHTTP = "VLSub",
+               useragent = "VLSub 0.8",
+               username = "",
+               password = "",
+               language = "",
+               downloadSub = true,
+               removeTag = true,
+               justgetlink = false
+       },
+       session = {
+               loginTime = 0,
+               token = ""
+       },
+       file = {
+               uri = nil,
+               ext = nil,
+               name = nil,
+               path = nil,
+               dir = nil,
+               hash = nil,
+               bytesize = nil,
+               fps = nil,
+               timems = nil,
+               frames = nil
+       },
+       movie = {
+               name = "",
+               season = "",
+               episode = "",
+               imdbid = nil,
+               imdbidShow = nil,
+               imdbidEpisode = nil,
+               imdbRequest = nil,
+               year = nil,
+               releasename = nil,
+               aka = nil
+       },
+       sub = {
+               id = nil,
+               authorcomment = nil,
+               hash = nil,
+               idfile = nil,
+               filename = nil,
+               content = nil,
+               IDSubMovieFile = nil,
+               score = nil,
+               comment = nil,
+               bad = nil,
+               languageid = nil
+       },
+       request = function(methodName)
+               local params = openSub.methods[methodName].params()
+               local reqTable = openSub.getMethodBase(methodName, params)
+               local request = "<?xml version='1.0'?>"..dump_xml(reqTable)
+               local host, path = parse_url(openSub.conf.url)
+               local header = {
+                       "POST "..path.." HTTP/1.1",
+                       "Host: "..host,
+                       "User-Agent: "..openSub.conf.userAgentHTTP,
+                       "Content-Type: text/xml",
+                       "Content-Length: "..string.len(request),
+                       "",
+                       ""
+               }
+               request = table.concat(header, "\r\n")..request
+
+               local response
+               local status, responseStr = http_req(host, 80, request)
+
+               if status == 200 then
+                       response = parse_xmlrpc(responseStr)
+                       if (response and response.status == "200 OK") then
+                               vlc.msg.dbg(responseStr)
+                               return openSub.methods[methodName].callback(response)
+                       elseif response then
+                               setError("code "..response.status.."("..status..")")
+                               return false
+                       else
+                               setError("Server not responding")
+                               return false
+                       end
+               elseif status == 401 then
+                       setError("Request unauthorized")
+
+                       response = parse_xmlrpc(responseStr)
+                       if openSub.session.token ~= response.token then
+                               setMessage("Session expired, retrying")
+                               openSub.session.token = response.token
+                               openSub.request(methodName)
+                       end
+                       return false
+               elseif status == 503 then
+                       setError("Server overloaded, please retry later")
+                       return false
+               end
+
+       end,
+       getMethodBase = function(methodName, param)
+               if openSub.methods[methodName].methodName then
+                       methodName = openSub.methods[methodName].methodName
+               end
+
+               local request = {
+                 methodCall={
+                       methodName=methodName,
+                       params={ param=param }}}
+
+               return request
+       end,
+       methods = {
+               LogIn = {
+                       params = function()
+                               openSub.actionLabel = "Logging in"
+                               return {
+                                       { value={ string=openSub.conf.username } },
+                                       { value={ string=openSub.conf.password } },
+                                       { value={ string=openSub.conf.language } },
+                                       { value={ string=openSub.conf.useragent } }
+                               }
+                       end,
+                       callback = function(resp)
+                               openSub.session.token = resp.token
+                               openSub.session.loginTime = os.time()
+                               return true
+                       end
+               },
+               LogOut = {
+                       params = function()
+                               openSub.actionLabel = "Logging out"
+                               return {
+                                       { value={ string=openSub.session.token } }
+                               }
+                       end,
+                       callback = function()
+                               return true
+                       end
+               },
+               NoOperation = {
+                       params = function()
+                               return {
+                                       { value={ string=openSub.session.token } }
+                               }
+                       end,
+                       callback = function()
+                               return true
+                       end
+               },
+               SearchSubtitlesByHash = {
+                       methodName = "SearchSubtitles",
+                       params = function()
+                               openSub.actionLabel = "Searching subtitles"
+                               setMessage(openSub.actionLabel..": "..progressBarContent(0))
+
+                               return {
+                                       { value={ string=openSub.session.token } },
+                                       { value={
+                                               array={
+                                                 data={
+                                                       value={
+                                                         struct={
+                                                               member={
+                                                                 { name="sublanguageid", value={ string=openSub.sub.languageid } },
+                                                                 { name="moviehash", value={ string=openSub.file.hash } },
+                                                                 { name="moviebytesize", value={ double=openSub.file.bytesize } } }}}}}}}
+                               }
+                       end,
+                       callback = function(resp)
+                               openSub.itemStore = resp.data
+
+                               if openSub.itemStore ~= "0" then
+                                       return true
+                               else
+                                       openSub.itemStore = nil
+                                       return false
+                               end
+                       end
+               },
+               SearchMoviesOnIMDB = {
+                       params = function()
+                               openSub.actionLabel = "Searching movie on IMDB"
+                               setMessage(openSub.actionLabel..": "..progressBarContent(0))
+
+                               return {
+                                       { value={ string=openSub.session.token } },
+                                       { value={ string=openSub.movie.imdbRequest } }
+                               }
+                       end,
+                       callback = function(resp)
+                               openSub.itemStore = resp.data
+
+                               if openSub.itemStore ~= "0" then
+                                       return true
+                               else
+                                       openSub.itemStore = nil
+                                       return false
+                               end
+                       end
+               },
+               SearchSubtitlesByIdIMDB = {
+                       methodName = "SearchSubtitles",
+                       params = function()
+                               openSub.actionLabel = "Searching subtitles"
+                               setMessage(openSub.actionLabel..": "..progressBarContent(0))
+
+                               return {
+                                       { value={ string=openSub.session.token } },
+                                       { value={
+                                               array={
+                                                 data={
+                                                       value={
+                                                         struct={
+                                                               member={
+                                                                 { name="sublanguageid", value={ string=openSub.sub.languageid } },
+                                                                 { name="imdbid", value={ string=openSub.movie.imdbid } } }}}}}}}
+                               }
+                       end,
+                       callback = function(resp)
+                               openSub.itemStore = resp.data
+
+                               if openSub.itemStore ~= "0" then
+                                       return true
+                               else
+                                       openSub.itemStore = nil
+                                       return false
+                               end
+                       end
+               },
+               GetIMDBMovieDetails = {
+                       params = function()
+                               return {
+                                       { value={ string=openSub.session.token } },
+                                       { value={ string=openSub.movie.imdbid } }
+                               }
+                       end,
+                       callback = function(resp)
+                               print(dump_xml(resp))
+                       end
+               },
+               IsTVserie = {
+                       methodName = "GetIMDBMovieDetails",
+                       params = function()
+                               return {
+                                       { value={ string=openSub.session.token } },
+                                       { value={ string=openSub.movie.imdbid } }
+                               }
+                       end,
+                       callback = function(resp)
+                               return (string.lower(resp.data.kind)=="tv series")
+                       end
+               }
+       },
+       getInputItem = function()
+               return vlc.item or vlc.input.item()
+       end,
+       getFileInfo = function()
+               local item = openSub.getInputItem()
+               if not item then
+                       return false
+               else
+                       local file = openSub.file
+                       local parsed_uri = vlc.net.url_parse(item:uri())
+                       file.uri = item:uri()
+                       file.protocol = parsed_uri["protocol"]
+                       file.path = vlc.strings.decode_uri(parsed_uri["path"])
+                       --correction needed for windows
+                       local windowPath = string.match(file.path, "^/(%a:/.+)$")
+                       if windowPath then
+                               file.path = windowPath
+                       end
+                       file.dir, file.completeName = string.match(file.path, "^([^\n]-/?)([^/]+)$")
+                       file.name, file.ext = string.match(file.path, "([^/]-)%.?([^%.]*)$")
+
+                       if file.ext == "part" then
+                               file.name, file.ext = string.match(file.name, "^([^/]+)%.([^%.]+)$")
+                       end
+                       file.cleanName = string.gsub(file.name, "[%._]", " ")
+               end
+       end,
+       getMovieInfo = function()
+               if not openSub.file.name then
+                       return false
+               end
+
+               local showName, seasonNumber, episodeNumber = string.match(openSub.file.cleanName, "(.+)[sS](%d%d)[eE](%d%d).*")
+
+               if not showName then
+                  showName, seasonNumber, episodeNumber = string.match(openSub.file.cleanName, "(.+)(%d)[xX](%d%d).*")
+               end
+
+               if showName then
+                       openSub.movie.name = showName
+                       openSub.movie.seasonNumber = seasonNumber
+                       openSub.movie.episodeNumber = episodeNumber
+               else
+                       openSub.movie.name = openSub.file.cleanName
+                       openSub.movie.seasonNumber = ""
+                       openSub.movie.episodeNumber = ""
+               end
+       end,
+       getMovieHash = function()
+               openSub.actionLabel = "Calculating movie hash"
+               setMessage(openSub.actionLabel..": "..progressBarContent(0))
+
+               local item = openSub.getInputItem()
+
+               if not item then
+                       setError("Please use this method during playing")
+                       return false
+               end
+
+               openSub.getFileInfo()
+               if openSub.file.protocol ~= "file" then
+                       setError("This method works with local file only (for now)")
+                       return false
+               end
+
+               local path = openSub.file.path
+               if not path then
+                       setError("File not found")
+                       return false
+               end
+
+               local file = assert(io.open(path, "rb"))
+               if not file then
+                       setError("File not found")
+                       return false
+               end
+
+               local i = 1
+               local a = {0, 0, 0, 0, 0, 0, 0, 0}
+               local hash = ""
+
+               local size = file:seek("end")
+               file:seek("set", 0)
+               local bytes = file:read(65536)
+               file:seek("set", size-65536)
+               bytes = bytes..file:read("*all")
+               file:close ()
+
+               for b in string.gfind(string.format("%16X ", size), "..") do
+                       d = tonumber(b, 16)
+                       if type(d) ~= "nil" then a[9-i] = d end
+                       i=i+1
+               end
+
+               i = 1
+               for b in string.gfind(bytes, ".") do
+                       a[i] = a[i] + string.byte(b)
+                       d = math.floor(a[i]/255)
+
+                       if d>=1 then
+                               a[i] = a[i] - d * 256
+                               if i<8 then a[i+1] = a[i+1] + d end
+                       end
+
+                       i=i+1
+                       if i==9 then i=1 end
+               end
+
+               for i=8, 1, -1 do
+                       hash = hash..string.format("%02x",a[i])
+               end
+
+               openSub.file.bytesize = size
+               openSub.file.hash = hash
+
+               return true
+       end,
+       getImdbEpisodeId = function(season, episode)
+               openSub.actionLabel = "Searching episode id on IMDB"
+               setMessage(openSub.actionLabel..": "..progressBarContent(0))
+               local IMDBurl = "http://www.imdb.com/title/tt"..openSub.movie.imdbid.."/episodes/_ajax?season="..season
+
+               local host, path = parse_url(IMDBurl)
+
+               local stream = vlc.stream(IMDBurl)
+               local data = ""
+
+               while data do
+                       data = stream:read(65536)
+                       local id = string.match(data, 'data%-const="tt(%d+)"[^>]+>\r?\n<img[^>]+>\r?\n<div> S'..season..', Ep'..episode)
+                       return id
+               end
+               return false
+       end,
+       getImdbEpisodeIdYQL = function(season, episode)
+               openSub.actionLabel = "Searching episode on IMDB"
+               setMessage(openSub.actionLabel..": "..progressBarContent(0))
+
+               local url = "http://pipes.yahoo.com/pipes/pipe.run?_id=5f525406f2b2b376eeb20b97a216bcb1&_render=json&imdbid="..openSub.movie.imdbid.."&season="..season.."&episode="..episode
+               local host, path = parse_url(url)
+               local header = {
+                       "GET "..path.." HTTP/1.1",
+                       "Host: "..host,
+                       "User-Agent: "..openSub.conf.userAgentHTTP,
+                       "",
+                       ""
+               }
+               local request = table.concat(header, "\r\n")
+               local fd = vlc.net.connect_tcp(host, 80)
+               local data = ""
+               if fd >= 0 then
+                       local pollfds = {}
+
+                       pollfds[fd] = vlc.net.POLLIN
+                       vlc.net.send(fd, request)
+                       vlc.net.poll(pollfds)
+
+                       data = vlc.net.recv(fd, 2048)
+                       print(data)
+               end
+
+               setMessage(openSub.actionLabel..": "..progressBarContent(100))
+
+               local id = string.match(data, '"content":"(%d+)"')
+               return id
+       end,
+       getImdbEpisodeIdGoogle = function(season, episode, title)
+               openSub.actionLabel = "Searching episode on IMDB"
+               setMessage(openSub.actionLabel..": "..progressBarContent(0))
+
+               local query = 'site:imdb.com tv episode "'..title..'" (#'..season..'.'..episode..')'
+               local url = "https://www.google.com/uds/GwebSearch?hl=fr&source=gsc&gss=.com&gl=www.google.com&context=1&key=notsupplied&v=1.0&&q="..vlc.strings.encode_uri_component(query)
+               local host, path = parse_url(url)
+               local header = {
+                       "GET "..path.." HTTP/1.1",
+                       "Host: "..host,
+                       "User-Agent: "..openSub.conf.userAgentHTTP,
+                       "",
+                       ""
+               }
+               local request = table.concat(header, "\r\n")
+               local fd = vlc.net.connect_tcp(host, 80)
+               local data = ""
+               if fd >= 0 then
+                       local pollfds = {}
+
+                       pollfds[fd] = vlc.net.POLLIN
+                       vlc.net.send(fd, request)
+                       vlc.net.poll(pollfds)
+
+                       data = vlc.net.recv(fd, 2048)
+                       --print(data)
+               end
+
+               setMessage(openSub.actionLabel..": "..progressBarContent(100))
+
+               local id = string.match(data, '"url":"http://www.imdb.com/title/tt(%d+)/"')
+               return id
+       end,
+    loadSubtitles = function(url, fileDir, SubFileName, target)
+        openSub.actionLabel = "Downloading subtitle"
+
+        setMessage(openSub.actionLabel..": "..progressBarContent(0))
+        local resp = get(url)
+        local subfileURI = ""
+        if resp then
+            local tmpFileName = fileDir..SubFileName..".zip"
+            local tmpFile = assert(io.open(tmpFileName, "wb"))
+            tmpFile:write(resp)
+            tmpFile:close()
+
+            subfileURI = "zip://"..make_uri(tmpFileName, true).."!/"..SubFileName
+            if target then
+                local stream = vlc.stream(subfileURI)
+                local data = ""
+                local subfile = assert(io.open(target, "w")) -- FIXME: check for file presence before overwrite (maybe ask what to do)
+
+                while data do
+                    if openSub.conf.removeTag then
+                        subfile:write(remove_tag(data).."\n")
+                    else
+                        subfile:write(data.."\n")
+                    end
+                    data = stream:readline()
+                end
+                subfile:close()
+
+                stream = nil
+            end
+            subfileURI = make_uri(target, true)
+            collectgarbage() -- force gargabe collection in order to close the opened stream
+            os.remove(tmpFileName)
+        end
+
+        local item = vlc.item or vlc.input.item()
+        if item then
+            vlc.input.add_subtitle(subfileURI)
+        else
+            setError("No current input, unable to add subtitles "..target)
+        end
+    end
+}
+
+function make_uri(str, encode)
+    local iswindowPath = string.match(str, "^%a:/.+$")
+       -- vlc.msg.dbg(iswindowPath)
+       if encode then
+               local encodedPath = ""
+               for w in string.gmatch(str, "/([^/]+)") do
+                       vlc.msg.dbg(w)
+                       encodedPath = encodedPath.."/"..vlc.strings.encode_uri_component(w)
+               end
+               str = encodedPath
+       end
+    if iswindowPath then
+        return "file:///"..str
+    else
+        return "file://"..str
+    end
+end
+
+function searchHash()
+       if not hasAssociatedResult() then
+               openSub.sub.languageid = languages[widget.getVal("language")][2]
+
+               openSub.getMovieHash()
+               associatedResult()
+
+               if openSub.file.hash then
+                       openSub.request("SearchSubtitlesByHash")
+                       display_subtitles()
+               end
+       else
+               local selection = widget.getVal("hashmainlist")
+               if #selection > 0 then
+                       download_subtitles(selection)
+               end
+       end
+end
+
+function searchIMBD()
+       local title = trim(widget.getVal("title"))
+       local old_title = trim(widget.get("title").value)
+       local season = tonumber(widget.getVal("season"))
+       local old_season = tonumber(widget.get("season").value)
+       local episode = tonumber(widget.getVal("episode"))
+       local old_episode = tonumber(widget.get("episode").value)
+       local language = languages[widget.getVal("language")][2]
+       local selection = widget.getVal("imdbmainlist")
+       local sel = (#selection > 0)
+       local newTitle = (title ~= old_title)
+       local newEpisode = (season ~= old_season or episode ~= old_episode)
+       local newLanguage = (language ~= openSub.sub.languageid)
+       local movie = openSub.movie
+       local imdbResults = {}
+       widget.get("title").value = title
+       widget.get("season").value = season
+       widget.get("episode").value = episode
+       openSub.sub.languageid = language
+
+       if newTitle then
+               movie.imdbRequest = title
+               movie.imdbid = nil
+               movie.imdbidShow = nil
+               if openSub.request("SearchMoviesOnIMDB") then -- search exact match
+                       local lowerTitle = string.lower(title)
+                       local itemTitle = ""
+                       for i, item in ipairs(openSub.itemStore) do
+                               -- itemTitle = string.match(item.title, "[%s\"]*([^%(\"]*)[%s\"']*%(?")
+                               item.cleanTitle = string.match(item.title, "[%s\"]*([^%(\"]*)[%s\"']*%(?")
+                               -- vlc.msg.dbg(itemTitle)
+
+                               --[[if string.lower(itemTitle) == lowerTitle then
+                                       movie.imdbid = item.id
+                                       break
+                               end]]
+                               table.insert(imdbResults, item.title)
+                       end
+                       if not movie.imdbid then
+                               widget.setVal("imdbmainlist")
+                               widget.setVal("imdbmainlist", imdbResults)
+                       end
+               end
+       end
+
+       if not movie.imdbid and sel then
+               local index = selection[1][1]
+               local item = openSub.itemStore[index]
+               movie.imdbid = item.id
+               movie.title = item.cleanTitle
+               movie.imdbidShow = movie.imdbid
+               newEpisode = true
+       end
+
+       if movie.imdbid then
+               if season and episode and (newTitle or newEpisode) then
+                       if not newTitle then
+                               movie.imdbid = movie.imdbidShow
+                       end
+
+                       movie.imdbidEpisode = openSub.getImdbEpisodeIdGoogle(season, episode, movie.title)
+                       -- movie.imdbidEpisode = openSub.getImdbEpisodeId(season, episode)
+
+
+                       if movie.imdbidEpisode then
+                               vlc.msg.dbg("Episode imdbid: "..movie.imdbidEpisode)
+                               movie.imdbidShow = movie.imdbid
+                               movie.imdbid = movie.imdbidEpisode
+                       elseif openSub.request("IsTVserie") then
+                               movie.imdbidEpisode = openSub.getImdbEpisodeIdYQL(season, episode)
+                               if movie.imdbidEpisode then
+                                       movie.imdbidShow = movie.imdbid
+                                       movie.imdbid = movie.imdbidEpisode
+                               else
+                                       setError("Season/episode don't match for this title")
+                               end
+                       else
+                               setError("Title not referenced as a TV serie on IMDB")
+                               --~ -- , choose an other one and/or empty episode/season field")
+                               widget.setVal("imdbmainlist", imdbResults)
+                       end
+               end
+
+               if newTitle or newEpisode or newLanguage then
+                       openSub.request("SearchSubtitlesByIdIMDB")
+                       display_subtitles()
+               elseif sel and openSub.itemStore then
+                       download_subtitles(selection)
+               end
+       end
+end
+
+function associatedResult()
+       local item = openSub.getInputItem()
+       if not item then return false end
+       result_state[tmp_method_id] = item:uri()
+end
+
+function hasAssociatedResult()
+       local item = openSub.getInputItem()
+       if not item then return false end
+       return (result_state[tmp_method_id] == item:uri())
+end
+
+function display_subtitles()
+       local list = tmp_method_id.."mainlist"
+       widget.setVal(list)
+       if openSub.itemStore then
+               for i, item in ipairs(openSub.itemStore) do
+                       widget.setVal(list, item.SubFileName.." ["..item.SubLanguageID.."] ("..item.SubSumCD.." CD)")
+               end
+       else
+               widget.setVal(list, "No result")
+       end
+end
+
+function download_subtitles(selection)
+       local list = tmp_method_id.."mainlist"
+       widget.resetSel(list) -- reset selection
+       local index = selection[1][1]
+       local item = openSub.itemStore[index]
+       local subfileTarget = ""
+       if openSub.file.dir and openSub.file.name then
+               subfileTarget = openSub.file.dir..openSub.file.name.."."..item.SubLanguageID.."."..item.SubFormat
+       else
+               subfileTarget = os.tmpname() --FIXME: ask the user where to put it instaed
+       end
+
+       if openSub.conf.justgetlink then
+               setMessage("Link: <a href='"..item.ZipDownloadLink.."'>"..item.ZipDownloadLink.."</a>")
+       else
+               openSub.loadSubtitles(item.ZipDownloadLink, openSub.file.dir, item.SubFileName, subfileTarget)
+       end
+end
+
+widget = {
+       stack = {},
+    meta = {},
+       registered_table = {},
+       main_table = {},
+       set_node = function(node, parent)
+               local left = parent.left
+               for k, l in pairs(node) do --parse items
+                       local tmpTop = parent.height
+                       local tmpLeft = left
+                       local ltmpLeft = l.left
+                       local ltmpTop = l.top
+                       local tmphidden = l.hidden
+
+                       l.top = parent.height + parent.top
+                       l.left = left
+                       l.parent = parent
+
+                       if l.display == "none" or parent.hidden then
+                               l.hidden = true
+                       else
+                               l.hidden = false
+                       end
+
+                       if l.type == "div" then --that's a container
+                               l.display = (l.display or "block")
+                               l.height = 1
+                               for _, newNode in ipairs(l.content) do --parse lines
+                                       widget.set_node(newNode, l)
+                                       l.height = l.height+1
+                               end
+                               l.height = l.height - 1
+                               left = left - 1
+                       else --that's an item
+                               l.display = (l.display or "inline")
+
+                               if not l.input then
+                                       tmphidden = true
+                               end
+
+                               if tmphidden and not l.hidden then --~ create
+                                       widget.create(l)
+                               elseif not tmphidden and l.hidden then --~ destroy
+                                       widget.destroy(l)
+                               end
+
+                               if not l.hidden and (ltmpTop ~= l.top or ltmpLeft ~= l.left) then
+                                       if l.input then --~ destroy
+                                               widget.destroy(l)
+                                       end
+                                       --~ recreate
+                                       widget.create(l)
+                               end
+                       end
+
+                       --~  Store reference ID
+                       if l.id and not widget.registered_table[l.id] then
+                               widget.registered_table[l.id] = l
+                       end
+
+                       if l.display == "block" then
+                               parent.height = parent.height + (l.height or 1)
+                               left = parent.left
+                       elseif l.display == "none" then
+                               parent.height = (tmpTop or parent.height)
+                               left = (tmpLeft or left)
+                       elseif l.display == "inline" then
+                               left = left + (l.width or 1)
+                       end
+               end
+       end,
+       set_interface = function(intf_map)
+               local root = {left = 1, top = 0, height = 0, hidden = false}
+               widget.set_node(intf_map, root)
+       end,
+       destroy = function(w)
+               dlg:del_widget(w.input)
+               --~ w.input = nil
+               --~ w.value = nil
+               if widget.registered_table[w.id] then
+                       widget.registered_table[w.id] = nil
+               end
+       end,
+       create = function(w)
+               local cur_widget
+               if w.type == "button" then
+                       cur_widget = dlg:add_button(w.value or "", w.callback, w.left, w.top, w.width or 1, w.height or 1)
+               elseif w.type == "label" then
+                       cur_widget = dlg:add_label(w.value or "", w.left, w.top, w.width or 1, w.height or 1)
+               elseif w.type == "html" then
+                       cur_widget = dlg:add_html(w.value or "", w.left, w.top, w.width or 1, w.height or 1)
+               elseif w.type == "text_input" then
+                       cur_widget = dlg:add_text_input(w.value or "", w.left, w.top, w.width or 1, w.height or 1)
+               elseif w.type == "password" then
+                       cur_widget = dlg:add_password(w.value or "", w.left, w.top, w.width or 1, w.height or 1)
+               elseif w.type == "check_box" then
+                       cur_widget = dlg:add_check_box(w.value or "", w.left, w.top, w.width or 1, w.height or 1)
+               elseif w.type == "dropdown" then
+                       cur_widget = dlg:add_dropdown(w.left, w.top, w.width or 1, w.height or 1)
+               elseif w.type == "list" then
+                       cur_widget = dlg:add_list(w.left, w.top, w.width or 1, w.height or 1)
+               elseif w.type == "image" then
+
+               end
+
+               if w.type == "dropdown" or w.type == "list" then
+                       if type(w.value) == "table" then
+                               for k, l in ipairs(w.value) do
+                                       if type(l) == "table" then
+                                               cur_widget:add_value(l[1], k)
+                                       else
+                                               cur_widget:add_value(l, k)
+                                       end
+                               end
+                       end
+               end
+
+               if w.type and w.type ~= "div" then
+                       w.input = cur_widget
+               end
+       end,
+       get = function(h)
+                return widget.registered_table[h]
+       end,
+       setVal = function(h, val, index)
+               widget.set_val(widget.registered_table[h], val, index)
+       end,
+       set_val = function(w, val, index)
+               local input = w.input
+               local t = w.type
+               if t == "button" or
+               t == "label" or
+               t == "html" or
+               t == "text_input" or
+               t == "password" then
+                       if type(val) == "string" then
+                               input:set_text(val)
+                               w.value = val
+                       end
+               elseif t == "check_box" then
+                       if type(val) == "bool" then
+                               input:set_checked(val)
+                       else
+                               input:set_text(val)
+                       end
+               elseif t == "dropdown" or t == "list" then
+                       if val and index then
+                               input:add_value(val, index)
+                               w.value[index] = val
+                       elseif val and not index then
+                               if type(val) == "table" then
+                                       for k, l in ipairs(val) do
+                                               input:add_value(l, k)
+                                               table.insert(w.value, l)
+                                       end
+                               else
+                                       input:add_value(val, #w.value+1)
+                                       table.insert(w.value, val)
+                               end
+                       elseif not val and not index then
+                               input:clear()
+                               w.value = nil
+                               w.value = {}
+                       end
+               end
+       end,
+       getVal = function(h, typeval)
+               if not widget.registered_table[h] then print(h) return false end
+               return widget.get_val(widget.registered_table[h], typeval)
+       end,
+       get_val = function(w, typeval)
+               local input = w.input
+               local t = w.type
+
+               if t == "button" or
+                  t == "label" or
+                  t == "html" or
+                  t == "text_input" or
+                  t == "password" then
+                       return input:get_text()
+               elseif t == "check_box" then
+                       if typeval == "checked" then
+                               return input:get_checked()
+                       else
+                               return input:get_text()
+                       end
+               elseif t == "dropdown" then
+                       return input:get_value()
+               elseif t == "list" then
+                       local selection = input:get_selection()
+                       local output = {}
+
+                       for index, name in  pairs(selection)do
+                               table.insert(output, {index, name})
+                       end
+                       return output
+               end
+       end,
+       resetSel = function(h, typeval)
+               local w = widget.registered_table[h]
+               local val = w.value
+               widget.set_val(w)
+               widget.set_val(w, val)
+       end
+}
+
+function create_dialog()
+       dlg = vlc.dialog("VLSub")
+       widget.set_interface(interface)
+end
+
+function set_interface()
+       local method_index = widget.getVal("method")
+       local method_id = methods[method_index][2]
+       if tmp_method_id then
+               if tmp_method_id == method_id then
+                       return false
+               end
+               widget.get(tmp_method_id).display = "none"
+       else
+               openSub.request("LogIn")
+       end
+       tmp_method_id = method_id
+       widget.get(method_id).display = "block"
+       widget.set_interface(interface)
+       setMessage("")
+
+       if method_id == "hash" then
+               searchHash()
+       elseif method_id == "imdb" then
+               if openSub.file.name and not hasAssociatedResult() then
+                       associatedResult()
+                       widget.get("title").input:set_text(openSub.movie.name)
+                       widget.get("season").input:set_text(openSub.movie.seasonNumber)
+                       widget.get("episode").input:set_text(openSub.movie.episodeNumber)
+               end
+       end
+end
+
+function progressBarContent(pct)
+       local content = "<span style='color:#181'>"
+       local accomplished = math.ceil(progressBarSize*pct/100)
+
+       local left = progressBarSize - accomplished
+       content = content .. string.rep ("|", accomplished)
+       content = content .. "</span>"
+       content = content .. string.rep ("|", left)
+       return content
+end
+
+function setError(str)
+       setMessage("<span style='color:#B23'>Error: "..str.."</span>")
+end
+
+function setMessage(str)
+       if widget.get("message") then
+               widget.setVal("message", str)
+               dlg:update()
+       end
+end
+
+function get(url)
+       local host, path = parse_url(url)
+       local header = {
+               "GET "..path.." HTTP/1.1",
+               "Host: "..host,
+               "User-Agent: "..openSub.conf.userAgentHTTP,
+               --~ "TE: identity", -- useless, and that's a shame
+               "",
+               ""
+       }
+       local request = table.concat(header, "\r\n")
+
+       local response
+       local status, response = http_req(host, 80, request)
+
+       if status == 200 then
+               return response
+       else
+               return false
+       end
+end
+
+function http_req(host, port, request)
+       local fd = vlc.net.connect_tcp(host, port)
+       if fd >= 0 then
+               local pollfds = {}
+
+               pollfds[fd] = vlc.net.POLLIN
+               vlc.net.send(fd, request)
+               vlc.net.poll(pollfds)
+
+               local response = vlc.net.recv(fd, 1024)
+               local headerStr, body = string.match(response, "(.-\r?\n)\r?\n(.*)")
+               local header = parse_header(headerStr)
+               local contentLength = tonumber(header["Content-Length"])
+               local TransferEncoding = header["Transfer-Encoding"]
+               local status = tonumber(header["statuscode"])
+               local bodyLenght = string.len(body)
+               local pct = 0
+
+               if status ~= 200 then return status end
+
+               while contentLength and bodyLenght < contentLength do
+                       vlc.net.poll(pollfds)
+                       response = vlc.net.recv(fd, 1024)
+
+                       if response then
+                               body = body..response
+                       else
+                               vlc.net.close(fd)
+                               return false
+                       end
+                       bodyLenght = string.len(body)
+                       pct = bodyLenght / contentLength * 100
+                       setMessage(openSub.actionLabel..": "..progressBarContent(pct))
+               end
+               vlc.net.close(fd)
+
+               return status, body
+       end
+       return ""
+end
+
+function parse_header(data)
+       local header = {}
+
+       for name, s, val in string.gfind(data, "([^%s:]+)(:?)%s([^\n]+)\r?\n") do
+               if s == "" then header['statuscode'] =  tonumber(string.sub (val, 1 , 3))
+               else header[name] = val end
+       end
+       return header
+end
+
+function parse_url(url)
+       local url_parsed = vlc.net.url_parse(url)
+       return  url_parsed["host"], url_parsed["path"], url_parsed["option"]
+end
+
+function parse_xml(data)
+       local tree = {}
+       local stack = {}
+       local tmp = {}
+       local level = 0
+
+       table.insert(stack, tree)
+
+       for op, tag, p, empty, val in string.gmatch(data, "<(%/?)([%w:]+)(.-)(%/?)>[%s\r\n\t]*([^<]*)") do
+               if op=="/" then
+                       if level>1 then
+                               level = level - 1
+                               table.remove(stack)
+                       end
+               else
+                       level = level + 1
+                       if val == "" then
+                               if type(stack[level][tag]) == "nil" then
+                                       stack[level][tag] = {}
+                                       table.insert(stack, stack[level][tag])
+                               else
+                                       if type(stack[level][tag][1]) == "nil" then
+                                               tmp = nil
+                                               tmp = stack[level][tag]
+                                               stack[level][tag] = nil
+                                               stack[level][tag] = {}
+                                               table.insert(stack[level][tag], tmp)
+                                       end
+                                       tmp = nil
+                                       tmp = {}
+                                       table.insert(stack[level][tag], tmp)
+                                       table.insert(stack, tmp)
+                               end
+                       else
+                               if type(stack[level][tag]) == "nil" then
+                                       stack[level][tag] = {}
+                               end
+                               stack[level][tag] = vlc.strings.resolve_xml_special_chars(val)
+                               table.insert(stack,  {})
+                       end
+                       if empty ~= "" then
+                               stack[level][tag] = ""
+                               level = level - 1
+                               table.remove(stack)
+                       end
+               end
+       end
+       return tree
+end
+
+function parse_xmlrpc(data)
+       local tree = {}
+       local stack = {}
+       local tmp = {}
+       local tmpTag = ""
+       local level = 0
+       table.insert(stack, tree)
+
+       for op, tag, p, empty, val in string.gmatch(data, "<(%/?)([%w:]+)(.-)(%/?)>[%s\r\n\t]*([^<]*)") do
+
+               if op=="/" then
+                       if tag == "member" or tag == "array" then
+                               if level>0  then
+                                       level = level - 1
+                                       table.remove(stack)
+                               end
+                       end
+               elseif tag == "name" then
+                       level = level + 1
+                       if val~=""then tmpTag  = vlc.strings.resolve_xml_special_chars(val) end
+
+                       if type(stack[level][tmpTag]) == "nil" then
+                               stack[level][tmpTag] = {}
+                               table.insert(stack, stack[level][tmpTag])
+                       else
+                               tmp = nil
+                               tmp = {}
+                               table.insert(stack[level-1], tmp)
+
+                               stack[level] = nil
+                               stack[level] = tmp
+                               table.insert(stack, tmp)
+                       end
+                       if empty ~= "" then
+                               level = level - 1
+                               stack[level][tmpTag] = ""
+                               table.remove(stack)
+                       end
+               elseif tag == "array" then
+                       level = level + 1
+                       tmp = nil
+                       tmp = {}
+                       table.insert(stack[level], tmp)
+                       table.insert(stack, tmp)
+               elseif val ~= "" then
+                       stack[level][tmpTag] = vlc.strings.resolve_xml_special_chars(val)
+               end
+       end
+       return tree
+end
+
+function dump_xml(data)
+       local level = 0
+       local stack = {}
+       local dump = ""
+
+       local function parse(data, stack)
+               for k,v in pairs(data) do
+                       if type(k)=="string" then
+                               --~ print(k)
+                               dump = dump.."\r\n"..string.rep (" ", level).."<"..k..">"
+                               table.insert(stack, k)
+                               level = level + 1
+                       elseif type(k)=="number" and k ~= 1 then
+                               dump = dump.."\r\n"..string.rep (" ", level-1).."<"..stack[level]..">"
+                       end
+
+                       if type(v)=="table" then
+                               parse(v, stack)
+                       elseif type(v)=="string" then
+                               dump = dump..vlc.strings.convert_xml_special_chars(v)
+                       elseif type(v)=="number" then
+                               dump = dump..v
+                       end
+
+                       if type(k)=="string" then
+                               if type(v)=="table" then
+                                       dump = dump.."\r\n"..string.rep (" ", level-1).."</"..k..">"
+                               else
+                                       dump = dump.."</"..k..">"
+                               end
+                               table.remove(stack)
+                               level = level - 1
+
+                       elseif type(k)=="number" and k ~= #data then
+                               if type(v)=="table" then
+                                       dump = dump.."\r\n"..string.rep (" ", level-1).."</"..stack[level]..">"
+                               else
+                                       dump = dump.."</"..stack[level]..">"
+                               end
+                       end
+               end
+       end
+       parse(data, stack)
+       return dump
+end
+
+function trim(str)
+    if not str then return "" end
+    return string.gsub(str, "^%s*(.-)%s*$", "%1")
+end
+
+function remove_tag(str)
+       return string.gsub(str, "{[^}]+}", "")
+end
+
+languages = {
+       {'All', 'all'},
+       {'Albanian', 'alb'},
+       {'Arabic', 'ara'},
+       {'Armenian', 'arm'},
+       {'Malay', 'may'},
+       {'Bosnian', 'bos'},
+       {'Bulgarian', 'bul'},
+       {'Catalan', 'cat'},
+       {'Basque', 'eus'},
+       {'Chinese (China)', 'chi'},
+       {'Croatian', 'hrv'},
+       {'Czech', 'cze'},
+       {'Danish', 'dan'},
+       {'Dutch', 'dut'},
+       {'English (US)', 'eng'},
+       {'English (UK)', 'bre'},
+       {'Esperanto', 'epo'},
+       {'Estonian', 'est'},
+       {'Finnish', 'fin'},
+       {'French', 'fre'},
+       {'Galician', 'glg'},
+       {'Georgian', 'geo'},
+       {'German', 'ger'},
+       {'Greek', 'ell'},
+       {'Hebrew', 'heb'},
+       {'Hungarian', 'hun'},
+       {'Indonesian', 'ind'},
+       {'Italian', 'ita'},
+       {'Japanese', 'jpn'},
+       {'Kazakh', 'kaz'},
+       {'Korean', 'kor'},
+       {'Latvian', 'lav'},
+       {'Lithuanian', 'lit'},
+       {'Luxembourgish', 'ltz'},
+       {'Macedonian', 'mac'},
+       {'Norwegian', 'nor'},
+       {'Persian', 'per'},
+       {'Polish', 'pol'},
+       {'Portuguese (Portugal)', 'por'},
+       {'Portuguese (Brazil)', 'pob'},
+       {'Romanian', 'rum'},
+       {'Russian', 'rus'},
+       {'Serbian', 'scc'},
+       {'Slovak', 'slo'},
+       {'Slovenian', 'slv'},
+       {'Spanish (Spain)', 'spa'},
+       {'Swedish', 'swe'},
+       {'Thai', 'tha'},
+       {'Turkish', 'tur'},
+       {'Ukrainian', 'ukr'},
+       {'Vietnamese', 'vie'}
+}
+
+methods = {
+       {"Video hash", "hash"},
+       {"IMDB ID", "imdb"}
+}
+
+interface = {
+       {
+               id = "header",
+               type = "div",
+               content = {
+                       {
+                               { type = "label", value = "Search method:" },
+                               {
+                                       type = "dropdown",
+                                       value = methods,
+                                       id = "method",
+                                       width = 2
+                               },
+                               { type = "button", value = "Go", callback = set_interface }
+                       },
+                       {
+                               { type = "label", value = "Language:" },
+                               { type = "dropdown", value = languages, id = "language" , width = 2 }
+                       }
+               }
+       },
+       {
+               id = "hash",
+               type = "div",
+               display = "none",
+               content = {
+                       {
+                               { type = "list", width = 4, id = "hashmainlist" }
+                       },{
+                               { type = "span", width = 2},
+                               { type = "button", value = "Ok", callback = searchHash },
+                               { type = "button", value = "Close", callback = close }
+                       }
+               }
+       },
+       {
+               id = "imdb",
+               type = "div",
+               display = "none",
+               content = {
+                       {
+                               { type = "label", value = "Title:"},
+                               { type = "text_input", value = openSub.movie.name or "", id = "title" }
+                       },{
+                               { type = "label", value = "Season (series):"},
+                               { type = "text_input", value = openSub.movie.seasonNumber or "", id = "season" }
+                       },{
+                               { type = "label", value = "Episode (series):"},
+                               { type = "text_input", value = openSub.movie.episodeNumber or "", id = "episode" },
+                               { type = "button", value = "Ok", callback = searchIMBD },
+                               { type = "button", value = "Close", callback = close }
+                       },{
+                               { type = "list", width = 4, id = "imdbmainlist" }
+                       }
+               }
+       },
+       {
+               id = "progressBar",
+               type = "div",
+               content = {
+                       {
+                               { type = "label", width = 4, value = "Powered by <a href='http://www.opensubtitles.org/'>opensubtitles.org</a>", id = "message" }
+                       }
+               }
+       }
+}