--- Describes the extension. function descriptor() return { title = "MergeSub", version = "0.1", author = "Christopher Gundler", url = '', shortdesc = "Mix subtitles"; description = "An extension for the VLC Media Player, which allows to mix multiple subtitles and merge them into one file", capabilities = { "menu" } } end --- Activates the extension. function activate() MergeDialog:instance() end --- Closes the extension. function close() -- Remove the merged cache file for the subtitles if it exists local path = get_tmp_file() local f = io.open(path, "r") if f ~= nil then io.close(f) os.remove(path) end vlc.deactivate() end --- Deactivates the extension. function deactivate() end function meta_changed() end --- Returns the elements which are to be shown in the menu. -- @return A map of subtitle descriptions ordered by their id. function menu() return {} end --- Handles a click on the menu. -- @param sub_id The id of the menu entry function trigger_menu(id) end --- Opens user interface, let the user select their subtitles and merge them function merge_subtitles() local dialog = MergeDialog:instance() local subs = dialog:selected_subtitles() local file = get_current_file() if get_table_len(subs) < 2 then vlc.msg.err("Please select at least two subtitles!") elseif dialog:is_waiting() then vlc.msg.err("Please wait!") elseif not file then vlc.msg.err("Please select a valid mkv file!") else -- Disable further interactions dialog:wait() -- Extract the subtitles local files = {} local command = string.format("mkvextract tracks %q", file) for k, v in pairs(subs) do local tmp_file = os.tmpname() files[#files+1] = tmp_file command = string.format("%s %u:%s", command, k, tmp_file) end assert(os.execute(command) ~= 0, "Extraction failed") -- Load and merge the subtitles in RAM local sub = nil for _, file in ipairs(files) do if not sub then sub = SubStationAlpha:load(file) -- Load ... else sub:merge_with(file) -- ... or merge. end os.remove(file) end sub:set_title("Merged") -- Save file and set subtitle local tmp_file = get_tmp_file() assert(sub:write(tmp_file)) vlc.input.add_subtitle(tmp_file) dialog:close() vlc.deactivate() end end --- Returns the path to a temporal file. -- @return The path to a temporal file. function get_tmp_file() if not tmp_file then tmp_file = os.tmpname() end return tmp_file end --- Returns the path of the current MKV file. -- @return The path to the current video. function get_current_file() local item = vlc.input.item() if item then local path = string.match(vlc.strings.decode_uri(item:uri()), "^file://(.+%.mkv)$") if path then return path end end end --- Returns the length of an arbitrary table. -- @return A number. function get_table_len(table) local count = 0 for k,v in pairs(table) do count = count + 1 end return count end --- Returns a list of available SubStation Alpha subtitles. -- @return A table of subtitles. function get_subtitles() sub = {} for k, v in pairs(vlc.input.item():info()) do if(v.Type == "Subtitle" and v.Codec == "SubStation Alpha subtitles (ssa )") then sub[tonumber(string.match(k, "(%d+)"))] = v.Description end end return sub end -- [ Class: MergeDialog ] -- --- A user dialog which allows the selection of multiple subtitles. MergeDialog = {} MergeDialog.__index = MergeDialog --- Returns an instance of the dialog by creating it or returning a handle. -- @return A MergeDialog instance. function MergeDialog:instance() if not dialog_singletone then local d = vlc.dialog( "Mix subtitles" ) d:add_label("Please select subtitles:") local subtitle_list = d:add_list() for k, v in pairs(get_subtitles()) do subtitle_list:add_value(v, k) end local subtitle_button = d:add_button("Merge", merge_subtitles) local this = { dialog = d, subtitles = subtitle_list, button = subtitle_button } setmetatable(this, MergeDialog) this.dialog:show() -- Set the global var dialog_singletone = this end return dialog_singletone end --- Closes the dialog instance. function MergeDialog:close() self.dialog:delete() dialog_singletone = nil end --- Sets the dialog in a state where it does not response to further input. function MergeDialog:wait() self.button:set_text("Please wait...") end --- Checks if the dialog does not response to input currently. -- @return True, if the dialog is inactive. function MergeDialog:is_waiting() return self.button:get_text() == "Please wait..." end --- Returns the selected subtitles. -- @return A table of subtitles. function MergeDialog:selected_subtitles() return self.subtitles:get_selection() end -- [ Class: SubStationAlpha ] -- --- A parser for the SubStation Alpha format. SubStationAlpha = {} SubStationAlpha.__index = SubStationAlpha --- Parses the format out of a file. -- @return The parsed subtitle format in memory. function SubStationAlpha:load(path) local result = {} local current_key = nil for line in io.lines(path) do -- Extract key and value local key, value = string.match(line, "%s*(%g+):%s*(.+)") -- If a value is parsed which has to be added under a existing key add it if value and current_key then -- If the key does not exists create it ... if not result[current_key][key] then result[current_key][key] = value -- ... or add value to a already existing collection else if type(result[current_key][key]) ~= "table" then local current_val = result[current_key][key] result[current_key][key] = {} result[current_key][key][1] = current_val end result[current_key][key][#result[current_key][key] + 1] = value end -- Add a value globally if there is no existing category elseif value and not current_key then result[key] = value -- Create a new category else local category = string.match(line, "%s*%[(.+)%]") if category then result[category] = {} current_key = category end end end local this = { data = result } setmetatable(this, SubStationAlpha) return this end --- Merges two SubStation alpha subtitles. function SubStationAlpha:merge_with(path) local tmp = SubStationAlpha:load(path) for _, dialogue in ipairs(tmp.data["Events"]["Dialogue"]) do self.data["Events"]["Dialogue"][#self.data["Events"]["Dialogue"] + 1] = dialogue end end --- Sets the title of the subtitle format. function SubStationAlpha:set_title(title) self.data["Script Info"]["Title"] = title end --- Writes the subtitle into a file. function SubStationAlpha:write(path) local file = io.open(path, "w+") if file == nil then return false end for key, value in pairs(self.data) do -- Find categories if type(value) == "table" then file:write("[", key, "]\n") -- Print items of category for sub_key, sub_value in pairs(value) do -- Check for dialogue if type(sub_value) == "table" then for _, element in ipairs(sub_value) do file:write(sub_key, ": ", element, "\n") end else file:write(sub_key, ": ", sub_value, "\n") end end else file:write(key, ": ", value, "\n") end end file:close() return true end