--[[ VLSub Extension for VLC media player 1.1 and 2.0 Copyright 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. --]] --[[ Global var ]]-- -- You can set here your default language by replacing nil with -- your language code (see below).Example: -- language = "fre", -- language = "ger", -- language = "eng", -- ... local options = { language = nil, downloadBehaviour = 'save', langExt = false, removeTag = false, showMediaInformation = true, progressBarSize = 80, intLang = 'eng', translations_avail = { eng = 'English', cze = 'Czech', dan = 'Danish', dut = 'Nederlands', fre = 'Français', ell = 'Greek', baq = 'Basque', pob = 'Brazilian Portuguese', por = 'Portuguese (Portugal)', rum = 'Romanian', slo = 'Slovak', spa = 'Spanish', swe = 'Swedish', ukr = 'Ukrainian', hun = 'Hungarian' }, translation = { int_all = 'All', int_descr = 'Download subtitles from OpenSubtitles.org', int_research = 'Research', int_config = 'Config', int_configuration = 'Configuration', int_help = 'Help', int_search_hash = 'Search by hash', int_search_name = 'Search by name', int_title = 'Title', int_season = 'Season (series)', int_episode = 'Episode (series)', int_show_help = 'Show help', int_show_conf = 'Show config', int_dowload_sel = 'Download selection', int_close = 'Close', int_ok = 'Ok', int_save = 'Save', int_cancel = 'Cancel', int_bool_true = 'Yes', int_bool_false = 'No', int_search_transl = 'Search translations', int_searching_transl = 'Searching translations ...', int_int_lang = 'Interface language', int_default_lang = 'Subtitles language', int_dowload_behav = 'What to do with subtitles', int_dowload_save = 'Load and save', int_dowload_load = 'Load only', int_dowload_manual = 'Manual download', int_display_code = 'Display language code in file name', int_remove_tag = 'Remove tags', int_vlsub_work_dir = 'VLSub working directory', int_os_username = 'Username', int_os_password = 'Password', int_help_mess =[[ Download subtitles from opensubtitles.org and display them while watching a video.

Usage:

Start your video. If you use Vlsub witout playing a video you will get a link to download the subtitles in your browser but the subtitles won't be saved and loaded automatically.

Choose the language for your subtitles and click on the button corresponding to one of the two research methods provided by VLSub:

Method 1: Search by hash
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.

Method 2: Search by name
If you have no luck with the first method, just check the title is correct before clicking. If you search subtitles for a series, you can also provide a season and episode number.

Downloading Subtitles
Select one subtitle in the list and click on 'Download'.
It will be put in the same directory that your video, with the same name (different extension) so VLC will load them automatically the next time you'll start the video.

/!\\ Beware : Existing subtitles are overwritten without asking confirmation, so put them elsewhere if they're important.

Find more VLC extensions at addons.videolan.org. ]], int_no_support_mess = [[ VLSub is not working with VLC 2.1.x on any platform because the lua "net" module needed to interact with opensubtitles has been removed in this release for the extensions.
Works with VLC 2.2 on mac and linux.
On windows you have to install an older version of VLC (2.0.8 for example) to use Vlsub:
http://download.videolan.org/pub/videolan/vlc/2.0.8/
]], action_login = 'Logging in', action_logout = 'Logging out', action_noop = 'Checking session', action_search = 'Searching subtitles', action_hash = 'Calculating movie hash', mess_success = 'Success', mess_error = 'Error', mess_warn = 'Warning', mess_no_response = 'Server not responding', mess_unauthorized = 'Request unauthorized', mess_expired = 'Session expired, retrying', mess_overloaded = 'Server overloaded, please retry later', mess_no_input = 'Please use this method during playing', mess_not_local = 'This method works with local file only (for now)', mess_not_found = 'File not found', mess_not_found2 = 'File not found (illegal character?)', mess_no_selection = 'No subtitles selected', mess_save_fail = 'Unable to save subtitles', mess_save_warn = 'Unable to save subtitles in file folder, using config folder', mess_click_link = 'Click here to open the file', mess_complete = 'Research complete', mess_no_res = 'No result', mess_res = 'result(s)', mess_loaded = 'Subtitles loaded', mess_not_load = 'Unable to load subtitles', mess_downloading = 'Downloading subtitle', mess_dowload_link = 'Download link', mess_err_conf_access ='Can\'t find a suitable path to save'.. 'config, please set it manually', mess_err_wrong_path ='the path contains illegal character, '.. 'please correct it', mess_err_hash = 'Failed to generate hash' } } local languages = { {'abk', 'Abkhazian'}, {'afr', 'Afrikaans'}, {'alb', 'Albanian'}, {'ara', 'Arabic'}, {'arg', 'Aragonese'}, {'arm', 'Armenian'}, {'ast', 'Asturian'}, {'aze', 'Azerbaijani'}, {'baq', 'Basque'}, {'bel', 'Belarusian'}, {'ben', 'Bengali'}, {'bos', 'Bosnian'}, {'bre', 'Breton'}, {'bul', 'Bulgarian'}, {'bur', 'Burmese'}, {'cat', 'Catalan'}, {'chi', 'Chinese (simplified)'}, {'zht', 'Chinese (traditional)'}, {'hrv', 'Croatian'}, {'cze', 'Czech'}, {'dan', 'Danish'}, {'prs', 'Dari'}, {'dut', 'Dutch'}, {'eng', 'English'}, {'epo', 'Esperanto'}, {'est', 'Estonian'}, {'ext', 'Extremaduran'}, {'fin', 'Finnish'}, {'fre', 'French'}, {'gla', 'Gaelic'}, {'glg', 'Galician'}, {'geo', 'Georgian'}, {'ger', 'German'}, {'ell', 'Greek'}, {'heb', 'Hebrew'}, {'hin', 'Hindi'}, {'hun', 'Hungarian'}, {'ice', 'Icelandic'}, {'ibo', 'Igbo'}, {'ind', 'Indonesian'}, {'gle', 'Irish'}, {'ita', 'Italian'}, {'jpn', 'Japanese'}, {'kan', 'Kannada'}, {'kaz', 'Kazakh'}, {'khm', 'Khmer'}, {'kor', 'Korean'}, {'kur', 'Kurdish'}, {'lav', 'Latvian'}, {'lit', 'Lithuanian'}, {'ltz', 'Luxembourgish'}, {'mac', 'Macedonian'}, {'may', 'Malay'}, {'mal', 'Malayalam'}, {'mni', 'Manipuri'}, {'mar', 'Marathi'}, {'mon', 'Mongolian'}, {'mne', 'Montenegrin'}, {'nav', 'Navajo'}, {'nep', 'Nepali'}, {'sme', 'Northern Sami'}, {'nor', 'Norwegian'}, {'oci', 'Occitan'}, {'ori', 'Odia'}, {'per', 'Persian'}, {'pol', 'Polish'}, {'por', 'Portuguese'}, {'pob', 'Brazilian Portuguese'}, {'pus', 'Pushto'}, {'rum', 'Romanian'}, {'rus', 'Russian'}, {'sat', 'Santali'}, {'scc', 'Serbian'}, {'snd', 'Sindhi'}, {'sin', 'Sinhalese'}, {'slo', 'Slovak'}, {'slv', 'Slovenian'}, {'spa', 'Spanish'}, {'swa', 'Swahili'}, {'swe', 'Swedish'}, {'syr', 'Syriac'}, {'tgl', 'Tagalog'}, {'tam', 'Tamil'}, {'tat', 'Tatar'}, {'tel', 'Telugu'}, {'tha', 'Thai'}, {'tok', 'Toki Pona'}, {'tur', 'Turkish'}, {'tuk', 'Turkmen'}, {'ukr', 'Ukrainian'}, {'urd', 'Urdu'}, {'vie', 'Vietnamese'}, {'wel', 'Welsh'} } -- Languages code conversion table: iso-639-1 to iso-639-3 -- See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes local lang_os_to_iso = { sq = "alb", ar = "ara", hy = "arm", eu = "baq", bn = "ben", bs = "bos", br = "bre", bg = "bul", my = "bur", ca = "cat", zh = "chi", hr = "hrv", cs = "cze", da = "dan", nl = "dut", en = "eng", eo = "epo", et = "est", fi = "fin", fr = "fre", gl = "glg", ka = "geo", de = "ger", el = "ell", he = "heb", hi = "hin", hu = "hun", is = "ice", id = "ind", it = "ita", ja = "jpn", kk = "kaz", km = "khm", ko = "kor", lv = "lav", lt = "lit", lb = "ltz", mk = "mac", ms = "may", ml = "mal", mn = "mon", no = "nor", oc = "oci", fa = "per", pl = "pol", pt = "por", po = "pob", ro = "rum", ru = "rus", sr = "scc", si = "sin", sk = "slo", sl = "slv", es = "spa", sw = "swa", sv = "swe", tl = "tgl", te = "tel", th = "tha", tr = "tur", uk = "ukr", ur = "urd", vi = "vie" } local dlg = nil local input_table = {} -- General widget id reference local select_conf = {} -- Drop down widget / option table association --[[ VLC extension stuff ]]-- function descriptor() return { title = "VLsub 0.11.1", version = "0.11.1", author = "exebetche", url = 'http://www.opensubtitles.org/', shortdesc = "VLsub"; description = options.translation.int_descr, capabilities = {"menu", "input-listener" } } end function activate() vlc.msg.dbg("[VLsub] Welcome") if not check_config() then vlc.msg.err("[VLsub] Unsupported VLC version") return false end if vlc.input.item() then openSub.getFileInfo() openSub.getMovieInfo() end show_main() end function close() vlc.deactivate() end function deactivate() vlc.msg.dbg("[VLsub] Bye bye!") if dlg then dlg:hide() end if openSub.session.token and openSub.session.token ~= "" then openSub.request("LogOut") end end function menu() return { lang.int_research, lang.int_config, lang.int_help } end function meta_changed() return false end function input_changed() collectgarbage() set_interface_main() collectgarbage() end --[[ Interface data ]]-- function interface_main() dlg:add_label(lang["int_default_lang"]..':', 1, 1, 1, 1) input_table['language'] = dlg:add_dropdown(2, 1, 2, 1) dlg:add_button(lang["int_search_hash"], searchHash, 4, 1, 1, 1) dlg:add_label(lang["int_title"]..':', 1, 2, 1, 1) input_table['title'] = dlg:add_text_input( openSub.movie.title or "", 2, 2, 2, 1) dlg:add_button(lang["int_search_name"], searchIMBD, 4, 2, 1, 1) dlg:add_label(lang["int_season"]..':', 1, 3, 1, 1) input_table['seasonNumber'] = dlg:add_text_input( openSub.movie.seasonNumber or "", 2, 3, 2, 1) dlg:add_label(lang["int_episode"]..':', 1, 4, 1, 1) input_table['episodeNumber'] = dlg:add_text_input( openSub.movie.episodeNumber or "", 2, 4, 2, 1) input_table['mainlist'] = dlg:add_list(1, 5, 4, 1) input_table['message'] = nil input_table['message'] = dlg:add_label(' ', 1, 6, 4, 1) dlg:add_button( lang["int_show_help"], show_help, 1, 7, 1, 1) dlg:add_button( ' '..lang["int_show_conf"]..' ', show_conf, 2, 7, 1, 1) dlg:add_button( lang["int_dowload_sel"], download_subtitles, 3, 7, 1, 1) dlg:add_button( lang["int_close"], deactivate, 4, 7, 1, 1) assoc_select_conf( 'language', 'language', openSub.conf.languages, 2, lang["int_all"]) display_subtitles() end function set_interface_main() -- Update movie title and co. if video input change if not type(input_table['title']) == 'userdata' then return false end openSub.getFileInfo() openSub.getMovieInfo() input_table['title']:set_text( openSub.movie.title or "") input_table['episodeNumber']:set_text( openSub.movie.episodeNumber or "") input_table['seasonNumber']:set_text( openSub.movie.seasonNumber or "") end function interface_config() input_table['intLangLab'] = dlg:add_label( lang["int_int_lang"]..':', 1, 1, 1, 1) input_table['intLangBut'] = dlg:add_button( lang["int_search_transl"], get_available_translations, 2, 1, 1, 1) input_table['intLang'] = dlg:add_dropdown(3, 1, 1, 1) dlg:add_label( lang["int_default_lang"]..':', 1, 2, 2, 1) input_table['default_language'] = dlg:add_dropdown(3, 2, 1, 1) dlg:add_label( lang["int_dowload_behav"]..':', 1, 3, 2, 1) input_table['downloadBehaviour'] = dlg:add_dropdown(3, 3, 1, 1) dlg:add_label( lang["int_display_code"]..':', 1, 4, 0, 1) input_table['langExt'] = dlg:add_dropdown(3, 4, 1, 1) dlg:add_label( lang["int_remove_tag"]..':', 1, 5, 0, 1) input_table['removeTag'] = dlg:add_dropdown(3, 5, 1, 1) dlg:add_label( lang["int_os_username"]..':', 1, 7, 0, 1) input_table['os_username'] = dlg:add_text_input( type(openSub.option.os_username) == "string" and openSub.option.os_username or "", 2, 7, 2, 1) dlg:add_label( lang["int_os_password"]..':', 1, 8, 0, 1) input_table['os_password'] = dlg:add_password( type(openSub.option.os_password) == "string" and openSub.option.os_password or "", 2, 8, 2, 1) input_table['message'] = nil input_table['message'] = dlg:add_label(' ', 1, 9, 3, 1) dlg:add_button( lang["int_cancel"], show_main, 2, 10, 1, 1) dlg:add_button( lang["int_save"], apply_config, 3, 10, 1, 1) input_table['langExt']:add_value( lang["int_bool_"..tostring(openSub.option.langExt)], 1) input_table['langExt']:add_value( lang["int_bool_"..tostring(not openSub.option.langExt)], 2) input_table['removeTag']:add_value( lang["int_bool_"..tostring(openSub.option.removeTag)], 1) input_table['removeTag']:add_value( lang["int_bool_"..tostring(not openSub.option.removeTag)], 2) assoc_select_conf( 'intLang', 'intLang', openSub.conf.translations_avail, 2) assoc_select_conf( 'default_language', 'language', openSub.conf.languages, 2, lang["int_all"]) assoc_select_conf( 'downloadBehaviour', 'downloadBehaviour', openSub.conf.downloadBehaviours, 1) end function interface_help() local help_html = lang["int_help_mess"] input_table['help'] = dlg:add_html( help_html, 1, 1, 4, 1) dlg:add_label( string.rep (" ", 100), 1, 2, 3, 1) dlg:add_button( lang["int_ok"], show_main, 4, 2, 1, 1) end function interface_no_support() local no_support_html = lang["int_no_support_mess"] input_table['no_support'] = dlg:add_html( no_support_html, 1, 1, 4, 1) dlg:add_label( string.rep (" ", 100), 1, 2, 3, 1) end function trigger_menu(dlg_id) if dlg_id == 1 then close_dlg() dlg = vlc.dialog( openSub.conf.useragent) interface_main() elseif dlg_id == 2 then close_dlg() dlg = vlc.dialog( openSub.conf.useragent..': '..lang["int_configuration"]) interface_config() elseif dlg_id == 3 then close_dlg() dlg = vlc.dialog( openSub.conf.useragent..': '..lang["int_help"]) interface_help() end collectgarbage() --~ !important end function show_main() trigger_menu(1) end function show_conf() trigger_menu(2) end function show_help() trigger_menu(3) end function close_dlg() vlc.msg.dbg("[VLSub] Closing dialog") if dlg ~= nil then --~ dlg:delete() -- Throw an error dlg:hide() end dlg = nil input_table = nil input_table = {} collectgarbage() --~ !important end --[[ Drop down / config association]]-- function assoc_select_conf(select_id, option, conf, ind, default) -- Helper for i/o interaction between drop down and option list select_conf[select_id] = { cf = conf, opt = option, dflt = default, ind = ind } set_default_option(select_id) display_select(select_id) end function set_default_option(select_id) -- Put the selected option of a list in first place of the associated table local opt = select_conf[select_id].opt local cfg = select_conf[select_id].cf local ind = select_conf[select_id].ind if openSub.option[opt] then table.sort(cfg, function(a, b) if a[1] == openSub.option[opt] then return true elseif b[1] == openSub.option[opt] then return false else return a[ind] < b[ind] end end) end end function display_select(select_id) -- Display the drop down values with an optional default value at the top local conf = select_conf[select_id].cf local opt = select_conf[select_id].opt local option = openSub.option[opt] local default = select_conf[select_id].dflt local default_isset = false if not default then default_isset = true end for k, l in ipairs(conf) do if default_isset then input_table[select_id]:add_value(l[2], k) else if option then input_table[select_id]:add_value(l[2], k) input_table[select_id]:add_value(default, 0) else input_table[select_id]:add_value(default, 0) input_table[select_id]:add_value(l[2], k) end default_isset = true end end end --[[ Config & interface localization]]-- function check_config() -- Make a copy of english translation to use it as default -- in case some element aren't translated in other translations eng_translation = {} for k, v in pairs(openSub.option.translation) do eng_translation[k] = v end -- Get available translation full name from code trsl_names = {} for i, lg in ipairs(languages) do trsl_names[lg[1]] = lg[2] end slash = package.config:sub(1,1) if slash == "\\" then openSub.conf.os = "win" else openSub.conf.os = "lin" end local filePath = slash.."vlsub_conf.xml" openSub.conf.dirPath = vlc.config.userdatadir() local res,err = vlc.io.mkdir( openSub.conf.dirPath, "0700" ) if res ~= 0 and err ~= vlc.errno.EEXIST then vlc.msg.warn("Failed to create " .. openSub.conf.dirPath) return false end local subdirs = { "lua", "extensions", "userdata", "vlsub" } for _, dir in ipairs(subdirs) do res, err = vlc.io.mkdir( openSub.conf.dirPath .. slash .. dir, "0700" ) if res ~= 0 and err ~= vlc.errno.EEXIST then vlc.msg.warn("Failed to create " .. openSub.conf.dirPath .. slash .. dir ) return false end openSub.conf.dirPath = openSub.conf.dirPath .. slash .. dir end if openSub.conf.dirPath then vlc.msg.dbg("[VLSub] Working directory: " .. openSub.conf.dirPath) openSub.conf.filePath = openSub.conf.dirPath..filePath openSub.conf.localePath = openSub.conf.dirPath..slash.."locale" if file_exist(openSub.conf.filePath) then vlc.msg.dbg("[VLSub] Loading config file: "..openSub.conf.filePath) load_config() else vlc.msg.dbg("[VLSub] No config file") getenv_lang() config_saved = save_config() if not config_saved then vlc.msg.dbg("[VLSub] Unable to save config") end end -- Check presence of a translation file -- in "%vlsub_directory%/locale" -- Add translation files to available translation list local file_list = list_dir(openSub.conf.localePath) local translations_avail = openSub.conf.translations_avail if file_list then for i, file_name in ipairs(file_list) do local lg = string.gsub( file_name, "^(%w%w%w).xml$", "%1") if lg and not openSub.option.translations_avail[lg] then table.insert(translations_avail, { lg, trsl_names[lg] }) end end end -- Load selected translation from file if openSub.option.intLang ~= "eng" and not openSub.conf.translated then local transl_file_path = openSub.conf.localePath.. slash..openSub.option.intLang..".xml" if file_exist(transl_file_path) then vlc.msg.dbg( "[VLSub] Loading translation from file: ".. transl_file_path) load_transl(transl_file_path) end end else vlc.msg.dbg("[VLSub] Unable to find a suitable path".. "to save config, please set it manually") return false end lang = nil lang = options.translation -- just a short cut if not vlc.net or not vlc.net.poll then dlg = vlc.dialog( openSub.conf.useragent..': '..lang["mess_error"]) interface_no_support() dlg:show() return false end SetDownloadBehaviours() -- Set table list of available translations from assoc. array -- so it is sortable for k, l in pairs(openSub.option.translations_avail) do if k == openSub.option.int_research then table.insert(openSub.conf.translations_avail, 1, {k, l}) else table.insert(openSub.conf.translations_avail, {k, l}) end end collectgarbage() return true end function load_config() -- Overwrite default conf with loaded conf local tmpFile = vlc.io.open(openSub.conf.filePath, "rb") if not tmpFile then return false end local resp = tmpFile:read("*all") tmpFile:flush() local option = parse_xml(resp) for key, value in pairs(option) do if type(value) == "table" then if key == "translation" then openSub.conf.translated = true for k, v in pairs(value) do openSub.option.translation[k] = v end else openSub.option[key] = value end else if value == "true" then openSub.option[key] = true elseif value == "false" then openSub.option[key] = false else openSub.option[key] = value end end end collectgarbage() end function load_transl(path) -- Overwrite default conf with loaded conf local tmpFile = assert(vlc.io.open(path, "rb")) local resp = tmpFile:read("*all") tmpFile:flush() openSub.option.translation = nil openSub.option.translation = parse_xml(resp) collectgarbage() end function apply_translation() -- Overwrite default conf with loaded conf for k, v in pairs(eng_translation) do if not openSub.option.translation[k] then openSub.option.translation[k] = eng_translation[k] end end end function getenv_lang() -- Retrieve the user OS language local os_lang = os.getenv("LANG") if os_lang then -- unix, mac os_lang = string.sub(os_lang, 0, 2) if type(lang_os_to_iso[os_lang]) then openSub.option.language = lang_os_to_iso[os_lang] end else -- Windows local lang_w = string.match( os.setlocale("", "collate"), "^[^_]+") for i, v in ipairs(openSub.conf.languages) do if v[2] == lang_w then openSub.option.language = v[1] end end end end function apply_config() -- Apply user config selection to local config local lg_sel = input_table['intLang']:get_value() local sel_val local opt local sel_cf if lg_sel and lg_sel ~= 1 and openSub.conf.translations_avail[lg_sel] then local lg = openSub.conf.translations_avail[lg_sel][1] set_translation(lg) SetDownloadBehaviours() end for select_id, v in pairs(select_conf) do if input_table[select_id] and select_conf[select_id] then sel_val = input_table[select_id]:get_value() sel_cf = select_conf[select_id] opt = sel_cf.opt if sel_val == 0 then openSub.option[opt] = nil else openSub.option[opt] = sel_cf.cf[sel_val][1] end set_default_option(select_id) end end openSub.option.os_username = input_table['os_username']:get_text() openSub.option.os_password = input_table['os_password']:get_text() if input_table["langExt"]:get_value() == 2 then openSub.option.langExt = not openSub.option.langExt end if input_table["removeTag"]:get_value() == 2 then openSub.option.removeTag = not openSub.option.removeTag end local config_saved = save_config() trigger_menu(1) if not config_saved then setError(lang["mess_err_conf_access"]) end end function save_config() -- Dump local config into config file if openSub.conf.dirPath and openSub.conf.filePath then vlc.msg.dbg( "[VLSub] Saving config file: ".. openSub.conf.filePath) local tmpFile = vlc.io.open(openSub.conf.filePath, "wb") if tmpFile ~= nil then local resp = dump_xml(openSub.option) tmpFile:write(resp) tmpFile:flush() tmpFile = nil else return false end collectgarbage() return true else vlc.msg.dbg("[VLSub] Unable fount a suitable path ".. "to save config, please set it manually") setError(lang["mess_err_conf_access"]) return false end end function SetDownloadBehaviours() openSub.conf.downloadBehaviours = nil openSub.conf.downloadBehaviours = { {'save', lang["int_dowload_save"]}, {'manual', lang["int_dowload_manual"]} } end function get_available_translations() -- Get all available translation files from the internet -- (drop previous direct download from github repo -- causing error with github https CA certficate on OS X an XP) -- https://github.com/exebetche/vlsub/tree/master/locale local translations_url = "http://addons.videolan.org/CONTENT/".. "content-files/148752-vlsub_translations.xml" if input_table['intLangBut']:get_text() == lang["int_search_transl"] then openSub.actionLabel = lang["int_searching_transl"] local translations_content, status, resp = get(translations_url) local translations_avail = openSub.option.translations_avail if translations_content == false then -- Translation list download error setMessage(error_tag(lang["mess_error"] .. " (" .. status .. ")")) return end all_trsl = parse_xml(translations_content) local lg, trsl for lg, trsl in pairs(all_trsl) do if lg ~= options.intLang[1] and not translations_avail[lg] then translations_avail[lg] = trsl_names[lg] or "" table.insert(openSub.conf.translations_avail, { lg, trsl_names[lg] }) input_table['intLang']:add_value( trsl_names[lg], #openSub.conf.translations_avail) end end setMessage(success_tag(lang["mess_complete"])) collectgarbage() end end function set_translation(lg) openSub.option.translation = nil openSub.option.translation = {} if lg == 'eng' then for k, v in pairs(eng_translation) do openSub.option.translation[k] = v end else -- If translation file exists in /locale directory load it if openSub.conf.localePath and file_exist(openSub.conf.localePath.. slash..lg..".xml") then local transl_file_path = openSub.conf.localePath.. slash..lg..".xml" vlc.msg.dbg("[VLSub] Loading translation from file: ".. transl_file_path) load_transl(transl_file_path) apply_translation() else -- Load translation file from internet if not all_trsl then get_available_translations() end if not all_trsl or not all_trsl[lg] then vlc.msg.dbg("[VLSub] Error, translation not found") return false end openSub.option.translation = all_trsl[lg] apply_translation() all_trsl = nil end end lang = nil lang = openSub.option.translation collectgarbage() end --[[ Core ]]-- openSub = { itemStore = nil, actionLabel = "", conf = { url = "http://api.opensubtitles.org/xml-rpc", path = nil, userAgentHTTP = "VLSub", useragent = "VLSub 0.11.1", translations_avail = {}, downloadBehaviours = nil, languages = languages }, option = options, session = { loginTime = 0, token = "" }, file = { hasInput = false, uri = nil, ext = nil, name = nil, path = nil, protocol = nil, cleanName = nil, dir = nil, hash = nil, bytesize = nil, fps = nil, timems = nil, frames = nil }, movie = { title = "", seasonNumber = "", episodeNumber = "", sublanguageid = "" }, request = function(methodName) local params = openSub.methods[methodName].params() local reqTable = openSub.getMethodBase(methodName, params) local request = ""..dump_xml(reqTable) local host, path = parse_url(openSub.conf.url) local header = { "POST "..path.." HTTP/1.0", "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 then if response.status == "200 OK" then return openSub.methods[methodName] .callback(response) elseif response.status == "406 No session" then openSub.request("LogIn") elseif response then setError("code '".. response.status.. "' ("..status..")") return false end 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 = lang["action_login"] return { { value={ string=openSub.option.os_username } }, { value={ string=openSub.option.os_password } }, { value={ string=openSub.movie.sublanguageid } }, { 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 = lang["action_logout"] return { { value={ string=openSub.session.token } } } end, callback = function() return true end }, NoOperation = { params = function() openSub.actionLabel = lang["action_noop"] return { { value={ string=openSub.session.token } } } end, callback = function(resp) return true end }, SearchSubtitlesByHash = { methodName = "SearchSubtitles", params = function() openSub.actionLabel = lang["action_search"] setMessage(openSub.actionLabel..": ".. progressBarContent(0)) return { { value={ string=openSub.session.token } }, { value={ array={ data={ value={ struct={ member={ { name="sublanguageid", value={ string=openSub.movie.sublanguageid } }, { name="moviehash", value={ string=openSub.file.hash } }, { name="moviebytesize", value={ double=openSub.file.bytesize } } }}}}}}} } end, callback = function(resp) openSub.itemStore = resp.data end }, SearchSubtitles = { methodName = "SearchSubtitles", params = function() openSub.actionLabel = lang["action_search"] setMessage(openSub.actionLabel..": ".. progressBarContent(0)) local member = { { name="sublanguageid", value={ string=openSub.movie.sublanguageid } }, { name="query", value={ string=openSub.movie.title } } } if openSub.movie.seasonNumber ~= nil then table.insert(member, { name="season", value={ string=openSub.movie.seasonNumber } }) end if openSub.movie.episodeNumber ~= nil then table.insert(member, { name="episode", value={ string=openSub.movie.episodeNumber } }) end return { { value={ string=openSub.session.token } }, { value={ array={ data={ value={ struct={ member=member }}}}}} } end, callback = function(resp) openSub.itemStore = resp.data end } }, getInputItem = function() return vlc.item or vlc.input.item() end, getFileInfo = function() -- Get video file path, name, extension from input uri local item = openSub.getInputItem() local file = openSub.file if not item then file.hasInput = false; file.cleanName = nil; file.uri = nil; else vlc.msg.dbg("[VLSub] Video URI: "..item:uri()) file.uri = item:uri() local filePath = vlc.strings.make_path(file.uri) if not filePath then filePath = vlc.strings.decode_uri(file.uri) filePath = string.match(filePath, "^.*[".. slash .."]([^" .. slash .. "]-).?[%a%d]*$") else file.dir, file.name = string.match(filePath, "^(.*[".. slash .."])([^" .. slash .. "]-).?[%a%d]*$") end if not file.name then file.name = filePath end -- Corrections file.hasInput = true; file.cleanName = string.gsub( file.name, "[%._]", " ") vlc.msg.dbg("[VLSub] file info "..(dump_xml(file))) end collectgarbage() end, getMovieInfo = function() -- Clean video file name and check for season/episode pattern in title if not openSub.file.name then openSub.movie.title = "" openSub.movie.seasonNumber = "" openSub.movie.episodeNumber = "" return false end local showName, seasonNumber, episodeNumber = string.match( openSub.file.cleanName or "", "(.+)[sS](%d%d)[eE](%d%d).*") if not showName then showName, seasonNumber, episodeNumber = string.match( openSub.file.cleanName or "", "(.+)(%d)[xX](%d%d).*") end if showName then openSub.movie.title = showName openSub.movie.seasonNumber = seasonNumber openSub.movie.episodeNumber = episodeNumber else openSub.movie.title = openSub.file.cleanName openSub.movie.seasonNumber = "" openSub.movie.episodeNumber = "" end collectgarbage() end, getMovieHash = function() -- Calculate movie hash openSub.actionLabel = lang["action_hash"] setMessage(openSub.actionLabel..": ".. progressBarContent(0)) local item = openSub.getInputItem() if not item then setError(lang["mess_no_input"]) return false end openSub.getFileInfo() local data_start = "" local data_end = "" local size local chunk_size = 65536 local ok local err -- Get data for hash calculation vlc.msg.dbg("[VLSub] Read hash data from stream") local file = vlc.stream(openSub.file.uri) data_start = file:read(chunk_size) ok, size = pcall(file.getsize, file) if not size then vlc.msg.warn("[VLSub] Failed to get stream size: " .. size ) setError(lang["mess_err_hash"]) return false end ok, err = pcall(file.seek, file, size - chunk_size) if not ok then vlc.msg.warn("[VLSub] Failed to seek to the end of the stream: " .. err) setError(lang["mess_err_hash"]) return false end data_end = file:read(chunk_size) -- Hash calculation local lo = size local hi = 0 local o,a,b,c,d,e,f,g,h local hash_data = data_start..data_end local max_size = 4294967296 local overflow for i = 1, #hash_data, 8 do a,b,c,d,e,f,g,h = hash_data:byte(i,i+7) lo = lo + a + b*256 + c*65536 + d*16777216 hi = hi + e + f*256 + g*65536 + h*16777216 if lo > max_size then overflow = math.floor(lo/max_size) lo = lo-(overflow*max_size) hi = hi+overflow end if hi > max_size then overflow = math.floor(hi/max_size) hi = hi-(overflow*max_size) end end openSub.file.bytesize = size openSub.file.hash = string.format("%08x%08x", hi,lo) vlc.msg.dbg("[VLSub] Video hash: "..openSub.file.hash) vlc.msg.dbg("[VLSub] Video bytesize: "..size) collectgarbage() return true end, checkSession = function() if openSub.session.token == "" then openSub.request("LogIn") else openSub.request("NoOperation") end end } function searchHash() local sel = input_table["language"]:get_value() if sel == 0 then openSub.movie.sublanguageid = 'all' else openSub.movie.sublanguageid = openSub.conf.languages[sel][1] end openSub.getMovieHash() if openSub.file.hash then openSub.checkSession() openSub.request("SearchSubtitlesByHash") display_subtitles() else setError(lang["mess_err_hash"]) end end function searchIMBD() openSub.movie.title = trim(input_table["title"]:get_text()) openSub.movie.seasonNumber = tonumber( input_table["seasonNumber"]:get_text()) openSub.movie.episodeNumber = tonumber( input_table["episodeNumber"]:get_text()) local sel = input_table["language"]:get_value() if sel == 0 then openSub.movie.sublanguageid = 'all' else openSub.movie.sublanguageid = openSub.conf.languages[sel][1] end if openSub.movie.title ~= "" then openSub.checkSession() openSub.request("SearchSubtitles") display_subtitles() end end function display_subtitles() local mainlist = input_table["mainlist"] mainlist:clear() if not openSub.itemStore then return end if openSub.itemStore ~= "0" then local nbRes = 0 for i, item in ipairs(openSub.itemStore) do if next(item) then mainlist:add_value( item.SubFileName.. " ["..item.SubLanguageID.."]".. " ("..item.SubSumCD.." CD)", i) nbRes = nbRes + 1 end end if nbRes > 0 then setMessage(""..lang["mess_complete"]..": ".. #(openSub.itemStore).." "..lang["mess_res"]) return end end mainlist:add_value(lang["mess_no_res"], 1) setMessage(""..lang["mess_complete"]..": ".. lang["mess_no_res"]) end function get_first_sel(list) local selection = list:get_selection() for index, name in pairs(selection) do return index end return 0 end function find_subtitle_in_archive(archivePath, subfileExt) local archive = vlc.directory_stream(vlc.strings.make_uri(archivePath)) local items = archive:readdir() if not items then return nil end subfileExt = "." .. subfileExt for _, item in pairs(items) do if string.sub(item:uri(), -string.len(subfileExt)) == subfileExt then return item:uri() end end return nil end function download_subtitles() local index = get_first_sel(input_table["mainlist"]) if index == 0 then setMessage(lang["mess_no_selection"]) return false end openSub.actionLabel = lang["mess_downloading"] display_subtitles() -- reset selection local item = openSub.itemStore[index] if openSub.option.downloadBehaviour == 'manual' or not openSub.file.hasInput then local link = "" link = link..""..lang["mess_dowload_link"]..":" link = link.."  " link = link.."  " link = link..item.MovieReleaseName.."" setMessage(link) return false end local message = "" local subfileName = openSub.file.name or "" if openSub.option.langExt then subfileName = subfileName.."."..item.SubLanguageID end subfileName = subfileName.."."..item.SubFormat local tmp_dir = vlc.config.cachedir() -- create the cache directory if it doesn't already exist local separator = "" local current_dir = "" if package.config:sub(1, 1):match("/") then -- unix based systems separator = "/" current_dir = "/" else -- windows systems separator = "\\" end for dir in tmp_dir:gmatch("[^"..separator.."]+") do current_dir = current_dir..dir..separator local vars = vlc.io.mkdir(current_dir, "0700") end local file_target_access = true local tmpFileName = dump_zip( item.ZipDownloadLink, tmp_dir, item.SubFileName) if not tmpFileName then setError(lang["mess_save_fail"].."  ".. "".. lang["mess_click_link"].."") return false end vlc.msg.dbg("[VLsub] tmpFileName: "..tmpFileName) local subtitleMrl = find_subtitle_in_archive(tmpFileName, item.SubFormat) if not subtitleMrl then setMessage( lang['mess_not_load'] ) return false end -- Determine if the path to the video file is accessible for writing local target if openSub.file.dir then target = openSub.file.dir..subfileName end if not target or not file_touch(target) then if openSub.conf.dirPath then target = openSub.conf.dirPath..slash..subfileName message = "
".. warn_tag(lang["mess_save_warn"].."  ".. "".. lang["mess_click_link"].."") else setError(lang["mess_save_fail"].."  ".. "".. lang["mess_click_link"].."") return false end end vlc.msg.dbg("[VLsub] Subtitles files: "..target) -- Unzipped data into file target local stream = vlc.stream(subtitleMrl) local data = "" local subfile = vlc.io.open(target, "wb") while data do subfile:write(data) data = stream:read(65536) end subfile:flush() subfile:close() stream = nil collectgarbage() if not vlc.io.unlink(tmpFileName) then vlc.msg.err("[VLsub] Unable to remove temp: "..tmpFileName) end -- load subtitles if add_sub(target) then message = success_tag(lang["mess_loaded"]) .. message else message = error_tag(lang["mess_not_load"]) .. message end setMessage(message) end function dump_zip(url, dir, subfileName) -- Dump zipped data in a temporary file setMessage(openSub.actionLabel..": "..progressBarContent(0)) local resp = get(url) if not resp then setError(lang["mess_no_response"]) return false end local tmpFileName = dir..slash..subfileName..".gz" local tmpFile = vlc.io.open(tmpFileName, "wb") if tmpFile == nil then return false end tmpFile:write(resp) tmpFile:flush() tmpFile = nil collectgarbage() return tmpFileName end function add_sub(subPath) if vlc.item or vlc.input.item() then vlc.msg.dbg("[VLsub] Adding subtitle :" .. subPath) return vlc.input.add_subtitle(subPath, true) end return false end --[[ Interface helpers]]-- function progressBarContent(pct) local accomplished = math.ceil( openSub.option.progressBarSize*pct/100) local left = openSub.option.progressBarSize - accomplished local content = "".. string.rep ("-", accomplished).."".. "".. string.rep ("-", left).. "" return content end function setMessage(str) if input_table["message"] then input_table["message"]:set_text(str) dlg:update() end end function setError(mess) setMessage(error_tag(mess)) end function success_tag(str) return "".. lang["mess_success"]..": "..str.."" end function error_tag(str) return "".. lang["mess_error"]..": "..str.."" end function warn_tag(str) return "".. lang["mess_warn"]..": "..str.."" end --[[ Network utils]]-- function get(url) local host, path = parse_url(url) local header = { "GET "..path.." HTTP/1.0", "Host: "..host, "User-Agent: "..openSub.conf.userAgentHTTP, "", "" } 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, status, response end end function http_req(host, port, request) local fd = vlc.net.connect_tcp(host, port) if not fd then return false end local pollfds = {} pollfds[fd] = vlc.net.POLLIN vlc.net.send(fd, request) vlc.net.poll(pollfds) local chunk = vlc.net.recv(fd, 2048) local response = "" local headerStr, header, body local contentLength, status local pct = 0 while chunk do response = response..chunk if not header then headerStr, body = response:match("(.-\r?\n)\r?\n(.*)") if headerStr then response = body header = parse_header(headerStr) contentLength = tonumber(header["Content-Length"]) status = tonumber(header["statuscode"]) end end if contentLength then bodyLenght = #response pct = bodyLenght / contentLength * 100 setMessage(openSub.actionLabel..": "..progressBarContent(pct)) if bodyLenght >= contentLength then break end end vlc.net.poll(pollfds) chunk = vlc.net.recv(fd, 1024) end vlc.net.close(fd) if status == 301 and header["Location"] then local host, path = parse_url(trim(header["Location"])) request = request :gsub("^([^%s]+ )([^%s]+)", "%1"..path) :gsub("(Host: )([^\n]*)", "%1"..host) return http_req(host, port, request) end return status, response end function parse_header(data) local header = {} for name, s, val in string.gmatch( 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.strings.url_parse(url) return url_parsed["host"], url_parsed["path"], url_parsed["option"] end --[[ XML utils]]-- function parse_xml(data) local tree = {} local stack = {} local tmp = {} local level = 0 local op, tag, p, empty, val table.insert(stack, tree) local resolve_xml = vlc.strings.resolve_xml_special_chars for op, tag, p, empty, val in string.gmatch( data, "[%s\r\n\t]*<(%/?)([%w:_]+)(.-)(%/?)>".. "[%s\r\n\t]*([^<]*)[%s\r\n\t]*" ) do if op=="/" then if level>0 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] = resolve_xml(val) table.insert(stack, {}) end if empty ~= "" then stack[level][tag] = "" level = level - 1 table.remove(stack) end end end collectgarbage() return tree end function parse_xmlrpc(data) local tree = {} local stack = {} local tmp = {} local tmpTag = "" local level = 0 local op, tag, p, empty, val local resolve_xml = vlc.strings.resolve_xml_special_chars 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 = resolve_xml(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] = resolve_xml(val) end end collectgarbage() return tree end function dump_xml(data) local level = 0 local stack = {} local dump = "" local convert_xml = vlc.strings.convert_xml_special_chars local function parse(data, stack) local data_index = {} local k local v local i local tb for k,v in pairs(data) do table.insert(data_index, {k, v}) table.sort(data_index, function(a, b) return a[1] < b[1] end) end for i,tb in pairs(data_index) do k = tb[1] v = tb[2] if type(k)=="string" then 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..(convert_xml(v) or v) elseif type(v)=="number" then dump = dump..v else dump = dump..tostring(v) end if type(k)=="string" then if type(v)=="table" then dump = dump.."\r\n"..string.rep( " ", level-1).. "" else dump = dump.."" 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).. "" else dump = dump.."" end end end end parse(data, stack) collectgarbage() return dump end --[[ Misc utils]]-- function file_touch(name) -- test write ability if not name or trim(name) == "" then return false end local f=vlc.io.open(name, "w") if f~=nil then return true else return false end end function file_exist(name) -- test readability if not name or trim(name) == "" then return false end local f=vlc.io.open(name, "r") if f~=nil then return true else return false end end function list_dir(path) if not path or trim(path) == "" then return false end local dir_list_cmd local list = {} dir_list_cmd = vlc.io.readdir(path) if dir_list_cmd then for _, entry in dir_list_cmd do if string.match(entry, "^[^%s]+.+$") then table.insert(list, entry.path .. slash .. entry.filename) end end return list else return false end end function is_window_path(path) return string.match(path, "^(%a:.+)$") end function is_win_safe(path) if not path or trim(path) == "" or not is_window_path(path) then return false end return string.match(path, "^%a?%:?[\\%w%p%s§¤]+$") end function trim(str) if not str then return "" end return string.gsub(str, "^[\r\n%s]*(.-)[\r\n%s]*$", "%1") end function remove_tag(str) return string.gsub(str, "{[^}]+}", "") end