rydesun/.config/mpv/scripts/timeline-marker.lua
2025-11-13 22:36:33 +08:00

238 lines
7.5 KiB
Lua

local mp = require 'mp'
local utils = require 'mp.utils'
local function get_xattr_comment(path)
local p = io.popen(string.format(
"getfattr -n user.timemarks --only-values %q 2>/dev/null", path
))
if not p then return nil end
local out = p:read("*a")
p:close()
return #out > 0 and out or nil
end
local function set_xattr_comment(path, comment)
local cmd
if comment == "" then
cmd = string.format("setfattr -x user.timemarks %q", path)
else
cmd = string.format("setfattr -n user.timemarks -v %q %q", comment, path)
end
os.execute(cmd)
end
local function parse_time(tstr)
local h, m, s
if tstr:match("^%d+:%d+:%d+$") then
h, m, s = tstr:match("^(%d+):(%d+):(%d+)$")
return tonumber(h) * 3600 + tonumber(m) * 60 + tonumber(s)
elseif tstr:match("^%d+:%d+$") then
m, s = tstr:match("^(%d+):(%d+)$")
return tonumber(m) * 60 + tonumber(s)
end
end
local function format_time(sec)
local h = math.floor(sec / 3600)
local m = math.floor((sec % 3600) / 60)
local s = math.floor(sec % 60)
if h > 0 then
return string.format("%d:%02d:%02d", h, m, s)
else
return string.format("%02d:%02d", m, s)
end
end
local function to_timemark(time_str, title)
local sec = parse_time(time_str)
if not sec then return end
return { time = sec, title = (#title > 0) and title or "" }
end
local function get_timemarks()
local path = mp.get_property("path")
if not path then return end
local comment = get_xattr_comment(path)
if not comment then return end
local marks = {}
for time_str, title in comment:gmatch("{(%d+:%d+:%d+)%s*([^:}]*)}") do
local mark = to_timemark(time_str, title)
if mark then table.insert(marks, mark) end
end
for time_str, title in comment:gmatch("{(%d+:%d+)%s*([^:}]*)}") do
local mark = to_timemark(time_str, title)
if mark then table.insert(marks, mark) end
end
table.sort(marks, function(a, b) return a.time < b.time end)
return marks
end
local function create_chapters()
local marks = get_timemarks()
if marks and #marks > 0 then
mp.set_property_native("chapter-list", marks)
end
end
local function get_current_and_next_chapter()
local chapter_list = mp.get_property_native("chapter-list")
if not chapter_list or #chapter_list == 0 then return end
local current_time = mp.get_property_number("time-pos", 0)
local current_chapter, next_chapter
for _, chapter in ipairs(chapter_list) do
if current_time >= chapter.time then
current_chapter = chapter
else
next_chapter = chapter
break
end
end
return current_chapter, next_chapter
end
local function append_current_time()
local path = mp.get_property("path")
if not path then return end
local pos = mp.get_property_number("time-pos")
if not pos then return end
local tstr = format_time(pos)
local comment = get_xattr_comment(path) or ""
comment = comment .. "{" .. tstr .. "}"
set_xattr_comment(path, comment)
mp.osd_message("添加时间标记:" .. tstr)
create_chapters()
end
local function remove_current_time()
local current_chapter = get_current_and_next_chapter()
if not current_chapter then return end
mp.commandv("seek", current_chapter.time, "absolute")
local tstr = format_time(current_chapter.time)
local path = mp.get_property("path")
if not path then return end
local comment = get_xattr_comment(path) or ""
comment = comment:gsub("{" .. tstr .. "%s*[^}]*}", "")
set_xattr_comment(path, comment)
mp.osd_message("删除时间标记:" .. tstr)
local marks = get_timemarks()
if marks and #marks > 0 then
mp.set_property_native("chapter-list", marks)
else
-- 必须清空所有章节信息
mp.set_property_native("chapter-list", {})
end
end
local function modify_current_title()
local current_chapter = get_current_and_next_chapter()
if not current_chapter then return end
local res = utils.subprocess({
args = { "kdialog", "--inputbox", "编辑标题", current_chapter.title },
cancellable = false
})
if res.error or res.status ~= 0 then return end
local input = res.stdout:gsub("^%s+", ""):gsub("%s+$", "")
local path = mp.get_property("path")
if not path then return end
local comment = get_xattr_comment(path)
if not comment then return end
local tstr = format_time(current_chapter.time)
if input ~= "" then
local new_fmt = "{" .. tstr .. " " .. input .. "}"
comment = comment:gsub("{" .. tstr .. "%s*[^}]*}", new_fmt)
mp.osd_message("时间标记的新标题:" .. input)
else
local new_fmt = "{" .. tstr .. "}"
comment = comment:gsub("{" .. tstr .. "%s*[^}]*}", new_fmt)
mp.osd_message("清除时间标记的标题")
end
set_xattr_comment(path, comment)
create_chapters()
end
local mark_combo = {
enabled = false,
timer = nil,
}
mp.register_script_message('set', function(prop, _)
if prop ~= "timemark-combo" then return end
mark_combo.toggle()
end)
function mark_combo.toggle()
mark_combo.enabled = not mark_combo.enabled
if mark_combo.enabled then
local chapter_list = mp.get_property_native("chapter-list")
if #chapter_list == 0 then
mark_combo.stop()
return
end
if mp.get_property_native("pause") then
mp.commandv("set", "pause", "no")
end
mp.commandv('script-message-to', 'uosc', 'set', 'timemark-combo', "yes")
mark_combo.multistep(chapter_list)
else
mark_combo.stop()
end
end
function mark_combo.stop()
mp.commandv('script-message-to', 'uosc', 'set', 'timemark-combo', "no")
mark_combo.enabled = false
if mark_combo.timer then
mark_combo.timer:kill()
mark_combo.timer = nil
end
end
function mark_combo.multistep(chapter_list)
local min_duration = 10
if mp.get_property_native("pause") then
mark_combo.stop()
return
end
local current_time = mp.get_property_number("time-pos", 0)
local duration = min_duration + math.random(min_duration / 2)
local current_chapter, next_chapter = get_current_and_next_chapter()
if current_chapter then
local current_offset = current_time - current_chapter.time
-- 防止相邻的两个时间标记过近导致错过当前标记,需要先播放完当前标记
if current_offset < duration then
-- 当前标记的剩余时长,不可以跳到下个标记
duration = duration - current_offset
else
if next_chapter then
mp.commandv("seek", next_chapter.time, "absolute")
else
-- 从头播放
next_chapter = chapter_list[1]
mp.commandv("seek", next_chapter.time, "absolute")
end
end
elseif next_chapter then
mp.commandv("seek", next_chapter.time, "absolute")
else
-- #chapter_list > 0
end
mark_combo.timer = mp.add_timeout(duration, function()
mark_combo.multistep(chapter_list)
end)
end
mp.register_event("file-loaded", create_chapters)
mp.register_event("file-loaded", mark_combo.stop)
mp.register_script_message("xattr-append-timemark", append_current_time)
mp.register_script_message("xattr-remove-timemark", remove_current_time)
mp.register_script_message("xattr-modify-timemark-title", modify_current_title)
mp.register_script_message("timemark-combo-toggle", mark_combo.toggle)