diff --git a/ffmetadata.lua b/ffmetadata.lua new file mode 100644 index 0000000..b0707fc --- /dev/null +++ b/ffmetadata.lua @@ -0,0 +1,397 @@ +--[[ + Filename: ffmetadata.lua + Description: ffmpeg metadata chapter creator + + - Inputs: any VLC file + - Outputs: /.ffm + + Usage: + ffmpeg -i / -i /.ffm -map_metadata 1 -codec copy / + + More Info: https://ffmpeg.org/ffmpeg-formats.html#Metadata-1 + + Copyright 2020 Amrit Panesar [Neo-Desktop] + See LICENSE for full license text +]] + +-- Imports from VLC modules library ------------------------- + +require 'common' + +-- VLC Required Descriptor Function ------------------------- + +function descriptor() + return { + title = "ffmetadata Editor", + version = "0.01", + author = "Amrit Panesar", + url = 'https://github.com/Neo-Desktop/vlc-lua-metadata-editor-script', + shortdesc = "ffmetadata Editor", + description = "Edits metadata for use with ffmpeg", + capabilities = {"playing-listener"} + } +end + +-- Globals ------------------------- + +CURRENT_FILE = nil +VLC_TIMEBASE = 1000000 + +-- Objects ------------------------- + +MetaClass = { } +MetaClass.__index = MetaClass + +function MetaClass:New(filename) + -- out.debug("New Class") + self = self or {} + input = vlc.object.input() + if input ~= nil then + self.FileName = filename + self.Chapters = {} + self.DisplayChapters = {} + self.Dirty = false + self.metadata = vlc.input.item():metas() or { } + self.path = uri_to_path(vlc.input.item():uri()) + self.duration = vlc.input.item():duration() * VLC_TIMEBASE + self:WalkChapters() + self:SetDirty(true) + -- self:Write() + return self + end + return nil +end + +function MetaClass:Write() + --[[ + ["metadata"] = { ["encoded_by"] = "HandBrake 0.10.5 2016021100"; ["filename"] = "The Simpsons S01E02 Bart the Genius.mp4"; ["title"] = "The Simpsons S01E02 Bart the Genius"; }; + ]]-- + + local file = io.open(self.path .. ".ffm", "w") + local writeln = function(...) file:write(... .. "\n") end + + writeln(";FFMETADATA1") + writeln("title=" .. self.metadata["title"] or "") + writeln("") + + --[[ + ["Chapters"] = { [1] = 0; [2] = 90244999; [3] = 179278999; [4] = 628507999; [5] = 1002628999; [6] = 1330483999; }; + ]]-- + + for k, v in pairs(self.Chapters) do + local endDuration = nil + if #self.Chapters >= k+1 then + endDuration = self.Chapters[k+1] + else + endDuration = self.duration + end + + writeln("[CHAPTER]") + writeln("TIMEBASE=1/" .. VLC_TIMEBASE) + writeln("START=" .. v) + writeln("END=" .. endDuration) + writeln("title=Chapter " .. k) + writeln("") + end + + file:flush() + file:close() + self:SetDirty(false) +end + +function MetaClass:AddChapter(time) + table.insert(self.Chapters, time) + local displayString = string.format("%s %s - [%s]", "Chapter", #self.Chapters, common.durationtostring(time/VLC_TIMEBASE)) + table.insert(self.DisplayChapters, displayString) + self:SetDirty(true) +end + +function MetaClass:RemoveChapter(displayString) + for k, v in pairs(self.DisplayChapters) do + if v == displayString then + out.debug("Removing Chapter [" .. v .. ']') + table.remove(self.Chapters, k) + table.remove(self.DisplayChapters, k) + break + end + end +end + +function MetaClass:WalkChapters() + local total, chapterMap = vlc.var.get_list(input, "chapter") + vlc.playlist.pause() + for k,v in pairs(chapterMap) do + vlc.var.set(vlc.object.input(), "chapter", k - 1 ) + sleep(0.001) + local time = vlc.var.get(vlc.object.input(), "time") + out.debug("Key: " .. k .. " | Chapter Index: " .. k-1 .. " | Time: " .. time) + self:AddChapter(time) + end + vlc.var.set(vlc.object.input(), "time", 0) +end + +function MetaClass:SetDirty(dirty) + self.Dirty = dirty + update_Title(dirty) +end + +-- VLC Callbacks ------------------------- + +function activate() + -- this is where extension starts + -- for example activation of extension opens custom dialog box: + create_dialog() +end + +function deactivate() + -- what should be done on deactivation of extension +end + +function close() + -- function triggered on dialog box close event + -- for example to deactivate extension on dialog box close: + if CURRENT_FILE and CURRENT_FILE.dirty then + CURRENT_FILE:Write() + end + vlc.deactivate() +end + +function meta_changed() + -- this really shouldn't be getting called, but here we are +end + +function playing_changed() + -- related to capabilities={"playing-listener"} in descriptor() + -- triggered by Pause/Play madia input event + + -- FIX ME: I legitamately have no idea how to determine if all metadata was loaded + -- this logic is a really bad hack with magic numbers and a state machine + if ( vlc.playlist.status() == "playing" and vlc.var.get(vlc.object.input(), "state") == 2 ) and + ( CURRENT_FILE == nil or vlc.input.item():uri() ~= CURRENT_FILE["uri"] ) then + + CURRENT_FILE = MetaClass:New(vlc.input.item():name()) + update_List() + out.debug(out.show(CURRENT_FILE, "CURRENT_FILE")) + end +end + +-- Dialog box Functions ------------------------- + +function create_dialog() + d = vlc.dialog("FFMpeg Metadata Writer") + w1 = d:add_label("Title:", 1, 1, 3, 1) + w2 = d:add_text_input("", 1, 2, 3, 1) + w3 = d:add_list(1, 4, 4, 1) + w4 = d:add_button("Add", click_Add, 1, 3, 1, 1) + w5 = d:add_button("Remove", click_Remove, 2, 3, 1, 1) + w6 = d:add_button("Clear", click_Clear, 3, 3, 1, 1) + w7 = d:add_button("Save", click_Save, 1, 9, 1, 1) +end + +function click_Add() + if not vlc.object.input() then return nil end + + local time = vlc.var.get(vlc.object.input(), "time") + CURRENT_FILE:AddChapter(time) + update_List() +end + +function click_Clear() + if not CURRENT_FILE then return nil end + + CURRENT_FILE:Clear() + update_List() +end + +function click_Save() + if not CURRENT_FILE then return nil end + + CURRENT_FILE.metadata["title"] = w2:get_text() + CURRENT_FILE:Write() +end + +function click_Remove() + local selection = w3:get_selection() + if (not selection) then return 1 end + + for k, v in pairs(selection) do + CURRENT_FILE:RemoveChapter(v) + end + + update_List() +end + +function update_List() + w2:set_text(CURRENT_FILE.metadata and CURRENT_FILE.metadata["title"] or "") + w3:clear() + for k, v in pairs(CURRENT_FILE.DisplayChapters) do + w3:add_value(v) + end +end + +function update_Title(dirty) + if dirty then + d:set_title("* FFMpeg Metadata Writer") + else + d:set_title("FFMpeg Metadata Writer") + end +end + +-- Debug Library ------------------------- + +out = {} + +function out.debugf(format, ...) + vlc.msg.dbg(string.format("[%s] " .. format, 'ffmetadata', ...)) +end + +function out.debug(...) + vlc.msg.dbg(string.format("[%s] %s", 'ffmetadata', ...)) +end + +function out.debug_dump() + local test = { } + test.InputStatus = vlc.playlist.status() + test.PlaylistItem = vlc.playlist.get(vlc.playlist.current()) + test.URI = vlc.input.item():uri() + test.Name = vlc.input.item():name() + test.InputItem = vlc.input.item():info() + test.Preparsed = vlc.input.item():is_preparsed() + test.InputPlaying = vlc.input.is_playing() + test.InputState = vlc.var.get(vlc.object.input(), "state") + out.debug(out.show(test, "test")) +end + +-- "Internal" Library Functions ------------------------- + +function uri_to_path(uri) + -- "file:///Z:/aesthetictv/media/_animation/The%20Simpsons/The%20Simpsons%20S02E13%20Homer%20vs.%20Lisa%20and%20the%20Eighth%20Commandment.mp4" + uri = decodeURI(uri) + uri = string.gsub(uri, "file:///", "", 1) + return string.gsub(uri, "/", '\\') +end + +function sleep(s) + local ntime = os.time() + s + repeat until os.time() > ntime +end + +-- "External" Library Fuctions ------------------------- + +-- decodeURI +-- https://gist.github.com/cgwxyz/6053d51e8d7134dd2e30 +function decodeURI(s) + if(s) then + s = string.gsub(s, "+", " ") + s = string.gsub(s, '%%(%x%x)', + function (hex) return string.char(tonumber(hex,16)) end ) + end + return s +end + + +--[[ + Author: Julio Manuel Fernandez-Diaz + Date: January 12, 2007 + (For Lua 5.1) + + Modified slightly by RiciLake to avoid the unnecessary table traversal in tablecount() + + Formats tables with cycles recursively to any depth. + The output is returned as a string. + References to other tables are shown as values. + Self references are indicated. + + The string returned is "Lua code", which can be procesed + (in the case in which indent is composed by spaces or "--"). + Userdata and function keys and values are shown as strings, + which logically are exactly not equivalent to the original code. + + This routine can serve for pretty formating tables with + proper indentations, apart from printing them: + + print(table.show(t, "t")) -- a typical use + + Heavily based on "Saving tables with cycles", PIL2, p. 113. + + Arguments: + t is the table. + name is the name of the table (optional) + indent is a first indentation (optional). +--]] +function out.show(t, name, indent) + local cart -- a container + local autoref -- for self references + + --[[ counts the number of elements in a table + local function tablecount(t) + local n = 0 + for _, _ in pairs(t) do n = n+1 end + return n + end + ]] + -- (RiciLake) returns true if the table is empty + local function isemptytable(t) return next(t) == nil end + + local function basicSerialize (o) + local so = tostring(o) + if type(o) == "function" then + local info = debug.getinfo(o, "S") + -- info.name is nil because o is not a calling level + if info.what == "C" then + return string.format("%q", so .. ", C function") + else + -- the information is defined through lines + return string.format("%q", so .. ", defined in (" .. + info.linedefined .. "-" .. info.lastlinedefined .. + ")" .. info.source) + end + elseif type(o) == "number" or type(o) == "boolean" then + return so + else + return string.format("%q", so) + end + end + + local function addtocart (value, name, indent, saved, field) + indent = indent or "" + saved = saved or {} + field = field or name + + cart = cart .. indent .. field + + if type(value) ~= "table" then + cart = cart .. " = " .. basicSerialize(value) .. ";\n" + else + if saved[value] then + cart = cart .. " = {}; -- " .. saved[value] + .. " (self reference)\n" + autoref = autoref .. name .. " = " .. saved[value] .. ";\n" + else + saved[value] = name + --if tablecount(value) == 0 then + if isemptytable(value) then + cart = cart .. " = {};\n" + else + cart = cart .. " = {\n" + for k, v in pairs(value) do + k = basicSerialize(k) + local fname = string.format("%s[%s]", name, k) + field = string.format("[%s]", k) + -- three spaces between levels + addtocart(v, fname, indent .. " ", saved, field) + end + cart = cart .. indent .. "};\n" + end + end + end + end + + name = name or "__unnamed__" + if type(t) ~= "table" then + return name .. " = " .. basicSerialize(t) + end + cart, autoref = "", "" + addtocart(t, name, indent) + return cart .. autoref +end \ No newline at end of file