Created
September 2, 2011 12:27
-
-
Save ema/1188492 to your computer and use it in GitHub Desktop.
VLC extension to find and download subtitles
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Subtitles | |
Copyright © 2009-2011 VideoLAN and AUTHORS | |
Authors: ale5000 (Based on the script made by Jean-Philippe André) | |
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. | |
--]] | |
--[[ | |
Notes | |
- Because of internal changes in VLC, the minimum version of VLC required is now: 1.1.0 git - 2010-03-21 | |
- VLC support only uncompressed .rar files | |
Know issues | |
- The search fail if you click the button two times consecutively | |
Changelog | |
1.04 | |
- Updated for the latest GIT build (now almost all lua bugs are fixed) | |
- Bumped the minimum version required | |
- Minor changes | |
1.03 | |
- Fixed the duplicated names that were appearing in the search | |
- Fixed the empty names that were appearing in the search | |
- Now there is a message when it doesn't find anything | |
- Minor optimizations | |
- Minor fixes | |
1.02 | |
- Fix for the latest GIT build | |
1.01 | |
- Changed version schema | |
- Now there isn't any dialog showed by default, you can still open them from the menu | |
- Now it is more clear when the script is working | |
- Added some dlg:flush() that were missing (this fix the elements in the dialog that weren't updated) | |
- Now it catch the fail of vlc.stream | |
- Minor fixes | |
0.1.8 | |
- Now you can open a new dialog while the previous is still open | |
- Fixed getting title in the new version of VLC | |
- Now using dlg:flush() where appropriate | |
- Workarounded some bugs in VLC | |
IMPORTANT: Now it no longer work on old versions of VLC | |
0.1.7 | |
- Removed the button "+"; it is useless since the title is updated automatically | |
- The download dialog can now be hidden/showed | |
- Added the "Load from url..." dialog | |
- The code of parse_archive() is now a separate function called by both "Load from url..." and the normal subtitles download dialog. | |
- Minor changes | |
0.1.6 | |
- Some variables made local | |
- Minor fixes | |
- Added .rar archive support ("stream_filter_rar" is incomplete and can only use one file in the archive, need a fix in VLC) | |
Note: VLC support only uncompressed .rar files | |
]] | |
-- Global variables | |
dlg = nil | |
dialog_is_opened = false | |
dialog_is_hidden = false | |
update_title_needed = false | |
-- -Common | |
website = nil | |
language = nil | |
main_text_input = nil | |
search_button = nil | |
load_button = nil | |
-- -Dialog "Download subtitles" | |
subtitles_list = nil | |
subtitles_result = nil | |
-- -Dialog "Load subtitles from url..." | |
type_text_input = nil | |
function descriptor() | |
return { | |
title = "Subtitles"; | |
version = "1.04"; | |
author = "ale5000"; | |
url = 'http://ale5000.altervista.org/vlc/extensions/subtitles-mod.lua'; | |
description = "<center><b>Subtitles</b></center>" | |
.. "Get the subtitles of movies from the internet, currently only from OpenSubtitles.org<br /><br />" | |
.. "<img src='http://static.opensubtitles.org/favicon.ico' /> Subtitles service allowed by <a href='http://www.opensubtitles.org/'>www.OpenSubtitles.org</a><br />" | |
.. "<br /><b>(Based on the script made by Jean-Philippe André)</b>"; | |
shortdesc = "Get the subtitles of movies from the internet, currently only from OpenSubtitles.org"; | |
capabilities = { "menu"; "input-listener"--[[; "meta-listener"]] } | |
} | |
end | |
function menu() | |
if not dialog_is_hidden then | |
return { "Download", "Upload", "Load from url...", "About" } | |
else | |
return { "Show" } | |
end | |
end | |
-- Function triggered when the extension is activated | |
function activate() | |
vlc.msg.dbg(_VERSION) | |
vlc.msg.dbg("[Subtitles] Activating") | |
trigger_menu(1) | |
return true | |
end | |
-- Function triggered when the extension is deactivated | |
function deactivate() | |
if dialog_is_opened then | |
close() | |
else | |
reset_variables() | |
dlg = nil | |
end | |
vlc.msg.dbg("[Subtitles] Deactivated") | |
return true | |
end | |
function reset_variables() | |
update_title_needed = false | |
-- Common | |
website = nil | |
language = nil | |
main_text_input = nil | |
search_button = nil | |
load_button = nil | |
-- Dialog "Download subtitles" | |
subtitles_list = nil | |
subtitles_result = nil | |
-- Dialog "Load subtitles from url..." | |
type_text_input = nil | |
end | |
-- Function triggered when the dialog is closed | |
function close() | |
dialog_is_opened = false | |
dialog_is_hidden = false | |
vlc.msg.dbg("[Subtitles] Closing dialog") | |
reset_variables() | |
if dlg ~= nil then dlg:delete() end | |
dlg = nil | |
return true | |
end | |
-- Current input changed | |
function input_changed() | |
vlc.msg.dbg("[Subtitles] Input is changed") | |
update_title() | |
end | |
-- Current input item meta changed | |
--[[function meta_changed() | |
end]] | |
function update_title() | |
if dialog_is_hidden or not update_title_needed then return true end | |
vlc.msg.dbg("[Subtitles] Updating title") | |
local item = vlc.input.item() | |
if item == nil then return false end | |
local title = item:name() -- It return the internal title or the filename if the first is missing | |
if title ~= nil then | |
title = string.gsub(title, "(.*)%.%w+$", "%1") -- Removes file extension | |
if title ~= "" then | |
main_text_input:set_text(title) | |
dlg:update() | |
return true | |
end | |
end | |
return false | |
end | |
function show_dialog_download() | |
-- column, row, colspan, rowspan | |
dlg:add_label("<right><b>Database: </b></right>", 1, 1, 1, 1) | |
website = dlg:add_dropdown(2, 1, 3, 1) | |
dlg:add_label("<right><b>Language: </b></right>", 1, 2, 1, 1) | |
language = dlg:add_dropdown(2, 2, 3, 1) | |
dlg:add_label("<right><b>Search: </b></right>", 1, 3, 1, 1) | |
main_text_input = dlg:add_text_input("", 2, 3, 1, 1) | |
search_button = dlg:add_button("Search", click_search, 3, 3, 1, 1) | |
dlg:add_button("Hide", hide_dialog, 4, 3, 1, 1) | |
for idx, ws in ipairs(websites) do | |
website:add_value(ws.title, idx) | |
end | |
for idx, ws in ipairs(languages) do | |
language:add_value(ws.title, idx) | |
end | |
update_title_needed = true | |
update_title() | |
dlg:update() | |
return true | |
end | |
function show_dialog_upload() | |
-- column, row, colspan, rowspan | |
dlg:add_label("<right><b>Database: </b></right>", 1, 1, 1, 1) | |
website = dlg:add_dropdown(2, 1, 4, 1) | |
-- FIXME: Link is NOT yet working | |
dlg:add_label("<center><a href='http://www.opensubtitles.org/upload'>Upload to OpenSubtitles.org</a></center>", 1, 2, 2, 1) | |
for idx, ws in ipairs(websites) do | |
website:add_value(ws.title, idx) | |
end | |
dlg:update() | |
return true | |
end | |
function show_dialog_load_url() | |
-- column, row, colspan, rowspan | |
dlg:add_label("<right><b>URL: </b></right>", 1, 1, 1, 1) | |
main_text_input = dlg:add_text_input("", 2, 1, 3, 1) | |
dlg:add_label("<right><b>Type: </b></right>", 1, 2, 1, 1) | |
type_text_input = dlg:add_text_input("", 2, 2, 1, 1) | |
dlg:add_label(" ", 3, 2, 1, 1) -- Spacer | |
load_button = dlg:add_button("Load", click_load_from_url_button, 4, 2, 1, 1) | |
dlg:update() | |
return true | |
end | |
function show_dialog_about() | |
local data = descriptor() | |
-- column, row, colspan, rowspan | |
dlg:add_label("<center><b>" .. data.title .. " " .. data.version .. "</b></center>", 1, 1, 1, 1) | |
dlg:add_html(data.description, 1, 2, 1, 1) | |
dlg:update() | |
return true | |
end | |
function sleep(sec) | |
local t = vlc.misc.mdate() | |
vlc.misc.mwait(t + sec*1000*1000) | |
end | |
function new_dialog(title) | |
dlg = vlc.dialog(title) | |
end | |
function hide_dialog() | |
dialog_is_hidden = true | |
dlg:hide() | |
end | |
-- Function triggered when a element from the menu is selected | |
function trigger_menu(id) | |
if dialog_is_hidden then | |
if dlg == nil then | |
vlc.msg.err("[Subtitles] Dialog pointer lost") | |
close() | |
return false; | |
end | |
dialog_is_hidden = false | |
dlg:show() | |
return true | |
end | |
if(dialog_is_opened) then close() end | |
dialog_is_opened = true | |
if id == 1 then | |
new_dialog("Download subtitles") | |
return show_dialog_download() | |
elseif id == 2 then | |
new_dialog("Upload subtitles") | |
return show_dialog_upload() | |
elseif id == 3 then | |
new_dialog("Load subtitles from url...") | |
return show_dialog_load_url() | |
elseif id == 4 then | |
new_dialog("About") | |
return show_dialog_about() | |
end | |
vlc.msg.err("[Subtitles] Invalid menu id: "..id) | |
return false | |
end | |
function click_search() | |
vlc.msg.dbg("[Subtitles] Clicked search button from \"Download subtitles\" dialog") | |
local search_term = main_text_input:get_text() | |
if(search_term == "") then return false end | |
local old_button_name = search_button:get_text() | |
search_button:set_text("Wait...") | |
if subtitles_list ~= nil then subtitles_list:clear() end | |
dlg:update() | |
subtitles_result = nil | |
local idx = website:get_value() | |
local idx2 = language:get_value() | |
if idx < 1 or idx2 < 1 then vlc.msg.err("[Subtitles] Invalid index in dropdown") search_button:set_text(old_button_name) return false end | |
local ws = websites[idx] | |
local lang = languages[idx2] | |
local url = ws.urlfunc(search_term) | |
-- vlc.msg.info("[Subtitles] Url: '" .. url .. "'") | |
local stream = vlc.stream(url) | |
if stream == nil then vlc.msg.err("[Subtitles] The site of subtitles isn't reachable") search_button:set_text(old_button_name) return false end | |
local reading = "blah" | |
local xmlpage = "" | |
while(reading ~= nil and reading ~= "") do | |
reading = stream:read(65653) | |
if(reading) then | |
xmlpage = xmlpage .. reading | |
end | |
end | |
if xmlpage == "" then search_button:set_text(old_button_name) return false end | |
subtitles_result = ws.parsefunc(xmlpage) | |
if subtitles_list == nil then | |
subtitles_list = dlg:add_list(1, 4, 4, 1) | |
load_button = dlg:add_button("Load selected subtitles", click_load_from_search_button, 1, 5, 4, 1) | |
end | |
if not subtitles_result then | |
subtitles_result = {} | |
subtitles_result[1]= { url = "-1" } | |
subtitles_list:add_value("Nothing found", 1) | |
search_button:set_text(old_button_name) | |
dlg:update() | |
return false | |
end | |
for idx, res in ipairs(subtitles_result) do | |
if(not res.language or lang.tag == "all" or lang.tag == res.language) then | |
subtitles_list:add_value("["..res.language.."] "..res.name, idx) | |
end | |
end | |
search_button:set_text(old_button_name) | |
dlg:update() | |
return true | |
end | |
function load_unknown_subtitles(url, language) | |
vlc.msg.dbg("[Subtitles] Loading "..language.." subtitle: "..url) | |
vlc.input.add_subtitle(url) | |
end | |
function load_subtitles_in_the_archive(dataBuffer, language) | |
local buffer_length = dataBuffer:len() | |
local files_found_in_the_compressed_file = 0 | |
local subtitles_found_in_the_compressed_file = 0 | |
local endIdx = 1 | |
local srturl, extension, download_dialog | |
-- Find subtitles | |
while(endIdx < buffer_length) do | |
_, endIdx, srturl, extension = dataBuffer:find("<location>([^<]+)%.(%a%a%a?)</location>", endIdx) | |
if(srturl == nil ) then break end | |
--vlc.msg.dbg("[Subtitles] File found in the archive: " .. srturl .. extension) | |
files_found_in_the_compressed_file = files_found_in_the_compressed_file + 1 | |
srturl = string.gsub(srturl, "^(%a%a%a)://", "%1://http://") | |
if(extension == "ass" or extension == "ssa" or extension == "srt" or extension == "smi" or extension == "sub" or extension == "rt" or extension == "txt" or extension == "mpl") then | |
subtitles_found_in_the_compressed_file = subtitles_found_in_the_compressed_file + 1 | |
vlc.msg.dbg("[Subtitles] Loading "..language.." subtitle: "..srturl) | |
vlc.input.add_subtitle(srturl.."."..extension) | |
end | |
end | |
vlc.msg.info("[Subtitles] Files found in the compressed file: "..files_found_in_the_compressed_file) | |
vlc.msg.info("[Subtitles] Subtitles found in the compressed file: "..subtitles_found_in_the_compressed_file) | |
if(subtitles_found_in_the_compressed_file > 0) then return true end | |
vlc.msg.warn("[Subtitles] No subtitles found in the compressed file") | |
return false | |
end | |
function parse_archive(url, language) | |
if url == "-1" then vlc.msg.dbg("[Subtitles] Dummy result") return true end | |
local stream = vlc.stream(url) | |
if stream == nil then vlc.msg.err("[Subtitles] The site of subtitles isn't reachable") return false end | |
stream:addfilter("zip,stream_filter_rar") | |
local data = stream:read(2048) | |
if(data == nil or data:find("<?xml version", 1, true) ~= 1) then | |
vlc.msg.info("[Subtitles] Type: RAR or unknown file") | |
load_unknown_subtitles(url, language) | |
else | |
vlc.msg.info("[Subtitles] Type: ZIP file") | |
local dataBuffer = "" | |
while(data ~= nil and data ~= "") do | |
vlc.msg.dbg("Buffering...") | |
dataBuffer = dataBuffer..data | |
data = stream:read(8192) | |
end | |
load_subtitles_in_the_archive(dataBuffer, language) | |
end | |
--vlc.msg.dbg("[Subtitles] Subtitle data: "..dataBuffer) | |
local subsfilename = "/tmp/\""..vlc.input.item():name()..".srt.zip\"" | |
-- http://www.opensubtitles.org/en/subtitleserve/sub/3476865 | |
vlc.msg.info("[Subtitles] Downloading this subs from: "..url.." to "..subsfilename) | |
os.execute("wget "..url.." -O "..subsfilename) | |
return true | |
end | |
function click_load_from_search_button() | |
vlc.msg.dbg("[Subtitles] Clicked load button from \"Download subtitles\" dialog") | |
if(not vlc.input.is_playing()) then | |
vlc.msg.warn("[Subtitles] You cannot load subtitles if you aren't playing any file") | |
return true | |
end | |
local old_button_name = load_button:get_text() | |
load_button:set_text("Wait...") | |
dlg:update() | |
local selection = subtitles_list:get_selection() | |
local index, name | |
for index, name in pairs(selection) do | |
vlc.msg.dbg("[Subtitles] Selected the item "..index.." with the name: "..name) | |
vlc.msg.dbg("[Subtitles] URL: "..subtitles_result[index].url) | |
parse_archive(subtitles_result[index].url, subtitles_result[index].language) -- ZIP, RAR or unknown file | |
end | |
load_button:set_text(old_button_name) | |
dlg:update() | |
return true | |
end | |
function click_load_from_url_button() | |
vlc.msg.dbg("[Subtitles] Clicked load button in \"Load subtitles from url...\" dialog") | |
if(not vlc.input.is_playing()) then | |
vlc.msg.warn("[Subtitles] You cannot load subtitles if you aren't playing any file") | |
return true | |
end | |
local old_button_name = load_button:get_text() | |
load_button:set_text("Wait...") | |
type_text_input:set_text("") | |
dlg:update() | |
local url_to_load = main_text_input:get_text() | |
if(url_to_load == "") then return false end | |
vlc.msg.dbg("[Subtitles] URL: "..url_to_load) | |
local _, ext_pos, extension = url_to_load:find("%.(%a%a%a?)", -4) | |
if(ext_pos == url_to_load:len()) then | |
type_text_input:set_text(extension) | |
if(extension == "ass" or extension == "ssa" or extension == "srt" or extension == "smi" or extension == "sub" or extension == "rt" or extension == "txt" or extension == "mpl") then | |
load_button:set_text(old_button_name) | |
dlg:update() | |
return vlc.input.add_subtitle(url_to_load) | |
end | |
end | |
local result = parse_archive(url_to_load, "") | |
if not result then | |
vlc.msg.info("[Subtitles] Waiting 5 seconds before retry...") | |
sleep(5) | |
result = parse_archive(url_to_load, "") | |
end | |
load_button:set_text(old_button_name) | |
dlg:update() | |
return result | |
end | |
-- XML Parsing | |
function parseargs(s) | |
local arg = {} | |
string.gsub(s, "(%w+)=([\"'])(.-)%2", function (w, _, a) | |
arg[w] = a | |
end) | |
return arg | |
end | |
function collect(s) | |
local stack = {} | |
local top = {} | |
table.insert(stack, top) | |
local ni,c,label,xarg, empty | |
local i, j = 1, 1 | |
while true do | |
ni,j,c,label,xarg, empty = string.find(s, "<(%/?)([%w:]+)(.-)(%/?)>", i) | |
if not ni then break end | |
local text = string.sub(s, i, ni-1) | |
if not string.find(text, "^%s*$") then | |
table.insert(top, text) | |
end | |
if empty == "/" then -- empty element tag | |
table.insert(top, {label=label, xarg=parseargs(xarg), empty=1}) | |
elseif c == "" then -- start tag | |
top = {label=label, xarg=parseargs(xarg)} | |
table.insert(stack, top) -- new level | |
else -- end tag | |
local toclose = table.remove(stack) -- remove top | |
top = stack[#stack] | |
if #stack < 1 then | |
error("nothing to close with "..label) | |
end | |
if toclose.label ~= label then | |
error("trying to close "..toclose.label.." with "..label) | |
end | |
table.insert(top, toclose) | |
end | |
i = j+1 | |
end | |
local text = string.sub(s, i) | |
if not string.find(text, "^%s*$") then | |
table.insert(stack[#stack], text) | |
end | |
if #stack > 1 then | |
error("unclosed "..stack[stack.n].label) | |
end | |
return stack[1] | |
end | |
--[[ | |
Websites configurations | |
]]-- | |
-- OpenSubtitles.org | |
-- This function uses first version of OS API. It will probably fail in | |
-- the future. We'll need a XML-RPC key btw | |
function urlOpenSub(search_term) | |
-- base = "http://api.opensubtitles.org/xml-rpc" | |
-- lang = "eng" | |
base = "http://api.opensubtitles.org/en/search/" | |
-- if(lang) then base = base .. "sublanguageid-" .. lang .. "/" | |
search_term = string.gsub(search_term, "%%", "%%37") | |
search_term = string.gsub(search_term, " ", "%%20") | |
return base .. "moviename-" .. search_term .. "/simplexml" | |
-- http://api.opensubtitles.org/en/search/moviename- .. search_term .. /simplexml | |
end | |
function parseOpenSub(xmltext) | |
vlc.msg.dbg("[Subtitles] Parsing XML data...") | |
local xmltext = string.gsub(xmltext, "<%?xml version=\"1%.0\" encoding=\"utf-8\"%?>", "") | |
local xmldata = collect(xmltext) | |
for a,b in pairs(xmldata) do | |
if type(b) == "table" then | |
if b.label == "search" then | |
xmldata = b | |
break | |
end | |
end | |
end | |
if xmldata == nil then return nil end | |
-- Subtitles information data | |
local subname = {} | |
local sub_movie = {} | |
local suburl = {} | |
local sublang = {} | |
local sub_language = {} | |
local subformat = {} | |
local subfilenum = {} | |
local subnum = 1 | |
local baseurl = "" | |
-- Let's browse iteratively the 'xmldata' tree | |
-- OK, the variables' names aren't explicit enough, but just remember a couple | |
-- a,b contains the index (a) and the data (b) of the table, which might also be a table | |
for a,b in pairs(xmldata) do | |
if type(b) == "table" then | |
if b.label == "results" then | |
for c,d in pairs(b) do | |
if type(d) == "table" then | |
if d.label == "subtitle" then | |
for e,f in pairs(d) do | |
if type(f) == "table" then | |
if f.label == "releasename" then | |
if f[1] ~= nil then subname[subnum] = f[1] | |
else subname[subnum] = "" end | |
elseif f.label == "movie" then | |
if f[1] ~= nil then sub_movie[subnum] = f[1] | |
else sub_movie[subnum] = "" end | |
elseif f.label == "download" then | |
if f[1] ~= nil then suburl[subnum] = f[1] | |
else suburl[subnum] = "" end | |
elseif f.label == "iso639" then -- language | |
if f[1] ~= nil then sublang[subnum] = f[1] | |
else sublang[subnum] = "" end | |
elseif f.label == "language" then -- language -- not use yet | |
if f[1] ~= nil then sub_language[subnum] = f[1] | |
else sub_language[subnum] = "" end | |
elseif f.label == "format" then | |
if f[1] ~= nil then subformat[subnum] = f[1] | |
else subformat[subnum] = "" end | |
end | |
end | |
end | |
subnum = subnum + 1 | |
end | |
end | |
end | |
elseif b.label == "base" then | |
baseurl = b[1] | |
end | |
end | |
end | |
if subnum <= 1 then | |
return nil | |
end | |
ret = {} | |
for i = 1,(subnum - 1) do | |
fullURL = baseurl .. "/" .. suburl[i] | |
realName = string.gsub( subname[i], "<..CDATA.", "" ) | |
realName = string.gsub( realName, "..>", "" ) | |
if realName == "" then | |
realName = string.gsub( sub_movie[i], "<..CDATA.", "" ) | |
realName = string.gsub( realName, "..>", "" ) | |
end | |
ret[i] = { name = realName, | |
url = fullURL, | |
language = sublang[i], | |
extension = ".zip" } | |
vlc.msg.dbg("[Subtitles] Found subtitle " .. i .. ": ") | |
vlc.msg.dbg(realName) | |
vlc.msg.dbg(fullURL) | |
end | |
return ret | |
end | |
-- These tables must be after all function definitions | |
websites = { | |
{ title = "OpenSubtitles.org", | |
urlfunc = urlOpenSub, | |
parsefunc = parseOpenSub } --[[; | |
{ title = "Fake (OS)", | |
urlfunc = url2, | |
parsefunc = parse2 }]] | |
} | |
languages = { | |
{ title = "All", tag = "all" }, | |
{ title = "English", tag = "en" }, | |
{ title = "Chinese", tag = "zh" }, | |
{ title = "Finnish", tag = "fi" }, | |
{ title = "French", tag = "fr" }, | |
{ title = "German", tag = "de" }, | |
{ title = "Italian", tag = "it" }, | |
{ title = "Japanese", tag = "ja" }, | |
{ title = "Polish", tag = "pl" }, | |
{ title = "Portuguese", tag = "pt" }, | |
{ title = "Russian", tag = "ru" }, | |
{ title = "Spanish", tag = "es" } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment