init
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
local P = {}
|
||||
|
||||
local TimeStamp = {}
|
||||
local TimeStamp_mt = { __index = TimeStamp }
|
||||
function TimeStamp:new(hours, minutes, seconds)
|
||||
local new = {}
|
||||
new.hours = hours
|
||||
new.minutes = minutes
|
||||
new.seconds = seconds
|
||||
return setmetatable(new, TimeStamp_mt)
|
||||
end
|
||||
|
||||
function TimeStamp.toTimeStamp(seconds)
|
||||
local diff, h, m, s = seconds, 0, 0, 0
|
||||
h = math.floor(diff / 3600)
|
||||
diff = diff - (h * 3600)
|
||||
m = math.floor(diff / 60)
|
||||
diff = diff - (m * 60)
|
||||
s = diff
|
||||
return TimeStamp:new(h, m, s)
|
||||
end
|
||||
|
||||
function TimeStamp:toSeconds()
|
||||
return (3600 * self.hours) + (60 * self.minutes) + self.seconds
|
||||
end
|
||||
|
||||
function TimeStamp:adjustTime(seconds)
|
||||
return self.toTimeStamp(self:toSeconds() + seconds)
|
||||
end
|
||||
|
||||
function TimeStamp:toString(decimal_symbol)
|
||||
local seconds_fmt = string.format("%06.3f", self.seconds):gsub("%.", decimal_symbol)
|
||||
return string.format("%02d:%02d:%s", self.hours, self.minutes, seconds_fmt)
|
||||
end
|
||||
|
||||
function TimeStamp.to_seconds(seconds, milliseconds)
|
||||
return tonumber(string.format("%s.%s", seconds, milliseconds))
|
||||
end
|
||||
|
||||
local AbstractSubtitle = {}
|
||||
local AbstractSubtitle_mt = { __index = AbstractSubtitle }
|
||||
|
||||
function AbstractSubtitle:create()
|
||||
local new = {}
|
||||
return setmetatable(new, AbstractSubtitle_mt)
|
||||
end
|
||||
|
||||
function AbstractSubtitle:save()
|
||||
print(string.format("Writing '%s' to file..", self.filename))
|
||||
local f = io.open(self.filename, 'w')
|
||||
f:write(self:toString())
|
||||
f:close()
|
||||
end
|
||||
|
||||
-- strip Byte Order Mark from file, if it's present
|
||||
function AbstractSubtitle:sanitize(line)
|
||||
local bom_table = { 0xEF, 0xBB, 0xBF } -- TODO maybe add other ones (like UTF-16)
|
||||
local function has_bom()
|
||||
for i = 1, #bom_table do
|
||||
if i > #line then return false end
|
||||
local ch, byte = line:sub(i, i), line:byte(i, i)
|
||||
if byte ~= bom_table[i] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
return has_bom() and string.sub(line, #bom_table + 1) or line
|
||||
end
|
||||
|
||||
local function trim(s)
|
||||
return s:match "^%s*(.-)%s*$"
|
||||
end
|
||||
|
||||
function AbstractSubtitle:parse_file(filename)
|
||||
local lines = {}
|
||||
for line in io.lines(filename) do
|
||||
if #lines == 0 then line = self:sanitize(line) end
|
||||
line = line:gsub('\r\n?', '') -- make sure there's no carriage return
|
||||
line = trim(line)
|
||||
table.insert(lines, line)
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
function AbstractSubtitle:shift_timing(diff_seconds)
|
||||
for _, entry in pairs(self.entries) do
|
||||
if self.valid_entry(entry) then
|
||||
entry.start_time = entry.start_time:adjustTime(diff_seconds)
|
||||
entry.end_time = entry.end_time:adjustTime(diff_seconds)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function AbstractSubtitle.valid_entry(entry)
|
||||
return entry ~= nil
|
||||
end
|
||||
|
||||
local function inheritsFrom (baseClass)
|
||||
local new_class = {}
|
||||
local class_mt = { __index = new_class }
|
||||
|
||||
function new_class:create(filename)
|
||||
local instance = {
|
||||
filename = filename,
|
||||
language = nil,
|
||||
header = nil, -- will be empty for srt, some stuff for ass
|
||||
entries = {} -- list of entries
|
||||
}
|
||||
setmetatable(instance, class_mt)
|
||||
return instance
|
||||
end
|
||||
|
||||
if baseClass then
|
||||
setmetatable(new_class, { __index = baseClass })
|
||||
end
|
||||
return new_class
|
||||
end
|
||||
|
||||
local SRT = inheritsFrom(AbstractSubtitle)
|
||||
function SRT.entry()
|
||||
return { index = nil, start_time = nil, end_time = nil, text = {} }
|
||||
end
|
||||
|
||||
function SRT:populate(filename)
|
||||
local timestamp_fmt = "^(%d+):(%d+):(%d+),(%d+) %-%-> (%d+):(%d+):(%d+),(%d+)$"
|
||||
local function parse_timestamp(timestamp)
|
||||
local function to_seconds(seconds, milliseconds)
|
||||
return tonumber(string.format("%s.%s", seconds, milliseconds))
|
||||
end
|
||||
local _, _, from_h, from_m, from_s, from_ms, to_h, to_m, to_s, to_ms = timestamp:find(timestamp_fmt)
|
||||
return TimeStamp:new(from_h, from_m, to_seconds(from_s, from_ms)), TimeStamp:new(to_h, to_m, to_seconds(to_s, to_ms))
|
||||
end
|
||||
|
||||
local new = self:create(filename)
|
||||
local entry = self.entry()
|
||||
local f_idx, idx = 1, 1
|
||||
for _, line in pairs(self:parse_file(filename)) do
|
||||
if idx == 1 and #line > 0 then
|
||||
assert(line:match("^%d+$"), string.format("SRT FORMAT ERROR (line %d): expected a number but got '%s'", f_idx, line))
|
||||
entry.index = line
|
||||
elseif idx == 2 then
|
||||
assert(line:match("^%d+:%d+:%d+,%d+ %-%-> %d+:%d+:%d+,%d+$"), string.format("SRT FORMAT ERROR (line %d): expected a timecode string but got '%s'", f_idx, line))
|
||||
local t_start, t_end = parse_timestamp(line)
|
||||
entry.start_time, entry.end_time = t_start, t_end
|
||||
else
|
||||
if #line == 0 then
|
||||
-- end of text
|
||||
if entry.index ~= nil then
|
||||
table.insert(new.entries, entry)
|
||||
end
|
||||
entry = SRT.entry()
|
||||
idx = 0
|
||||
else
|
||||
table.insert(entry.text, line)
|
||||
end
|
||||
end
|
||||
idx = idx + 1
|
||||
f_idx = f_idx + 1
|
||||
end
|
||||
return new
|
||||
end
|
||||
|
||||
function SRT:toString()
|
||||
local stringbuilder = {}
|
||||
local function append(s)
|
||||
table.insert(stringbuilder, s)
|
||||
end
|
||||
for _, entry in pairs(self.entries) do
|
||||
append(entry.index)
|
||||
local timestamp_string = string.format("%s --> %s", entry.start_time:toString(","), entry.end_time:toString(","))
|
||||
append(timestamp_string)
|
||||
if type(entry.text) == 'table' then
|
||||
append(table.concat(entry.text, "\n"))
|
||||
else append(entry.text) end
|
||||
append('')
|
||||
end
|
||||
return table.concat(stringbuilder, '\n')
|
||||
end
|
||||
|
||||
local ASS = inheritsFrom(AbstractSubtitle)
|
||||
ASS.header_mapper = { ["Start"] = "start_time", ["End"] = "end_time" }
|
||||
|
||||
function ASS.valid_entry(entry)
|
||||
return entry['type'] ~= nil
|
||||
end
|
||||
|
||||
function ASS:toString()
|
||||
local stringbuilder = {}
|
||||
local function append(s) table.insert(stringbuilder, s) end
|
||||
append(self.header)
|
||||
append('[Events]')
|
||||
for i = 1, #self.entries do
|
||||
if i == 1 then
|
||||
-- stringbuilder for events header
|
||||
local event_sb = {};
|
||||
for _, v in pairs(self.event_header) do table.insert(event_sb, v) end
|
||||
append(string.format("Format: %s", table.concat(event_sb, ", ")))
|
||||
end
|
||||
local entry = self.entries[i]
|
||||
local entry_sb = {}
|
||||
for _, col in pairs(self.event_header) do
|
||||
local value = entry[col]
|
||||
local timestamp_entry_column = self.header_mapper[col]
|
||||
if timestamp_entry_column then
|
||||
value = entry[timestamp_entry_column]:toString(".")
|
||||
end
|
||||
table.insert(entry_sb, value)
|
||||
end
|
||||
append(string.format("%s: %s", entry['type'], table.concat(entry_sb, ",")))
|
||||
end
|
||||
return table.concat(stringbuilder, '\n')
|
||||
end
|
||||
|
||||
function ASS:populate(filename, language)
|
||||
local header, events, parser = {}, {}, nil
|
||||
for _, line in pairs(self:parse_file(filename)) do
|
||||
local _, _, event = string.find(line, "^%[([^%]]+)%]%s*$")
|
||||
if event then
|
||||
if event == "Events" then
|
||||
parser = function(x) table.insert(events, x) end
|
||||
else
|
||||
parser = function(x) table.insert(header, x) end
|
||||
parser(line)
|
||||
end
|
||||
else
|
||||
parser(line)
|
||||
end
|
||||
end
|
||||
-- create subtitle instance
|
||||
local ev_regex = "^(%a+):%s(.+)$"
|
||||
local function parse_event(header_columns, ev)
|
||||
local function create_timestamp(timestamp_str)
|
||||
local timestamp_fmt = "^(%d+):(%d+):(%d+).(%d+)"
|
||||
local _, _, h, m, s, ms = timestamp_str:find(timestamp_fmt)
|
||||
return TimeStamp:new(h, m, TimeStamp.to_seconds(s, ms))
|
||||
end
|
||||
local new_event = {}
|
||||
local _, _, ev_type, ev_values = string.find(ev, ev_regex)
|
||||
new_event['type'] = ev_type
|
||||
-- skipping last column, since that's the text, which can contain commas
|
||||
local last_idx = 0;
|
||||
for i = 1, #header_columns - 1 do
|
||||
local col = header_columns[i]
|
||||
local idx = string.find(ev_values, ",", last_idx + 1)
|
||||
local val = ev_values:sub(last_idx + 1, idx - 1)
|
||||
local timestamp_entry_column = self.header_mapper[col]
|
||||
if timestamp_entry_column then
|
||||
new_event[timestamp_entry_column] = create_timestamp(val)
|
||||
else
|
||||
new_event[col] = val
|
||||
end
|
||||
last_idx = idx
|
||||
end
|
||||
new_event[header_columns[#header_columns]] = ev_values:sub(last_idx + 1)
|
||||
return new_event
|
||||
end
|
||||
|
||||
local sub = self:create(filename)
|
||||
sub.header = table.concat(header, "\n")
|
||||
sub.language = language
|
||||
-- remove and process first entry in events, which is a header
|
||||
local _, _, colstring = string.find(table.remove(events, 1), "^%a+:%s(.+)$")
|
||||
local columns = {};
|
||||
for i in colstring:gmatch("[^%,%s]+") do table.insert(columns, i) end
|
||||
sub.event_header = columns
|
||||
for _, event in pairs(events) do
|
||||
if #event > 0 then
|
||||
table.insert(sub.entries, parse_event(columns, event))
|
||||
end
|
||||
end
|
||||
return sub
|
||||
end
|
||||
|
||||
P.AbstractSubtitle = AbstractSubtitle
|
||||
P.ASS = ASS
|
||||
P.SRT = SRT
|
||||
return P
|
||||
Reference in New Issue
Block a user