This commit is contained in:
2026-04-03 11:33:51 +02:00
parent 64922e1ae3
commit 0ed904319d
57 changed files with 2935 additions and 1377 deletions
+13 -11
View File
@@ -1,3 +1,5 @@
local unpack = unpack or table.unpack
-- Clean up media name
local function clean_name(name)
return name:gsub("^%[.-%]", " ")
@@ -19,12 +21,6 @@ local formatters = {
return clean_name(name) .. " S" .. season .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*第%s*(%d+)%s*[季部]+%s*[_%-%.%s]%s*[eEpP]+[_%-%.%s]?(%d+[%.v]?%d*)",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*第([一二三四五六七八九十]+)[季部]+%s*[_%-%.%s]%s*第%s*(%d+[%.v]?%d*)%s*[话集回]",
format = function(name, season, episode)
@@ -32,7 +28,13 @@ local formatters = {
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*第([一二三四五六七八九十]+)[季部]+%s*[_%-%.%s]%s*[eEpP]+[_%-%.%s]?(%d+[%.v]?%d*)",
regex = "^(.-)%s*[_%-%.%s]%s*第%s*(%d+)%s*[季部]+%s*[_%-%.%s]%s*[^%ddD][eEpP]+(%d+[%.v]?%d*)",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*第([一二三四五六七八九十]+)[季部]+%s*[_%-%.%s]%s*[^%ddD][eEpP]+(%d+[%.v]?%d*)",
format = function(name, season, episode)
return clean_name(name) .. " S" .. chinese_to_number(season) .. "E" .. episode:gsub("v%d+$","")
end
@@ -54,7 +56,7 @@ local formatters = {
end
},
{
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)%s*[_%.%s]%s*[eEpP]+[_%-%.%s]?(%d+%.?%d*)",
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)%s*[_%.%s]%s*[^%ddD][eEpP]+(%d+%.?%d*)",
format = function(name, year, episode)
return clean_name(name) .. " (" .. year .. ") E" .. episode
end
@@ -78,13 +80,13 @@ local formatters = {
end
},
{
regex = "^(.-)%s*[^dD][eEpP]+[_%-%.%s]?(%d+[%.v]?%d*)[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
regex = "^(.-)%s*[^%ddD][eEpP]+(%d+[%.v]?%d*)[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
format = function(name, episode, year)
return clean_name(name) .. " (" .. year .. ") E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[^dD][eEpP]+[_%-%.%s]?(%d+%.?%d*)",
regex = "^(.-)%s*[^%ddD][eEpP]+(%d+%.?%d*)",
format = function(name, episode)
return clean_name(name) .. " E" .. episode
end
@@ -154,4 +156,4 @@ function format_filename(title)
return title
end
end
end
end
+1 -1
View File
@@ -189,4 +189,4 @@ local function sha256(message)
return result
end
return sha256
return sha256
+611 -80
View File
@@ -1,8 +1,10 @@
local msg = require('mp.msg')
local utils = require("mp.utils")
local unpack = unpack or table.unpack
input_loaded, input = pcall(require, "mp.input")
uosc_available = false
latest_menu_anime = {}
-- 打开番剧数据匹配菜单
function get_animes(query)
@@ -69,10 +71,11 @@ function get_animes(query)
end
if uosc_available then
update_menu_uosc(menu_type, menu_title, items, footnote, menu_cmd, query)
latest_menu_anime = update_menu_uosc(menu_type, menu_title, items, footnote, menu_cmd, query)
elseif input_loaded then
show_message("", 0)
mp.add_timeout(0.1, function()
latest_menu_anime = utils.format_json(items)
open_menu_select(items)
end)
end
@@ -124,6 +127,13 @@ function get_episodes(animeTitle, bangumiId)
return
end
table.insert(items, {
title = "← 返回搜索结果",
value = { "script-message-to", mp.get_script_name(), "open-latest-menu-anime", latest_menu_anime },
keep_open = false,
selectable = true,
})
for _, episode in ipairs(response.bangumi.episodes) do
table.insert(items, {
title = episode.episodeTitle,
@@ -136,6 +146,7 @@ function get_episodes(animeTitle, bangumiId)
end
if uosc_available then
footnote = mp.get_property("filename")
update_menu_uosc(menu_type, menu_title, items, footnote)
elseif input_loaded then
mp.add_timeout(0.1, function()
@@ -154,6 +165,7 @@ function update_menu_uosc(menu_type, menu_title, menu_item, menu_footnote, menu_
keep_open = true,
selectable = false,
align = "center",
icon = "spinner",
})
else
items = menu_item
@@ -171,6 +183,8 @@ function update_menu_uosc(menu_type, menu_title, menu_item, menu_footnote, menu_
}
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
return json_props
end
function open_menu_select(menu_items, is_time)
@@ -182,10 +196,16 @@ function open_menu_select(menu_items, is_time)
end
mp.commandv('script-message-to', 'console', 'disable')
input.select({
prompt = '筛选:',
prompt = is_time and '筛选:' or '选择:',
items = item_titles,
submit = function(id)
mp.commandv(unpack(item_values[id]))
input.terminate()
local v = item_values[id]
if type(v) == 'table' then
mp.commandv(unpack(v))
elseif type(v) == 'string' then
mp.command(v)
end
end,
})
end
@@ -250,12 +270,114 @@ end
-- 打开弹幕源添加管理菜单
function open_add_menu_get()
mp.commandv('script-message-to', 'console', 'disable')
local menu_log, deal_value = {}, {}
-- 重建菜单内容函数
local function rebuild_menu_log(select_num)
deal_value = {}
menu_log = {
{ text = "【既有弹幕源】", style = "{\\c&H00CCFF&\\b1}" },
{ text = "----------------------------", style = "{\\c&H888888&}" }
}
local serial = 0
for url, source in pairs(DANMAKU.sources) do
if source.data then
serial = serial + 1
local action, text
if source.from == "api_server" then
action = source.blocked and "unblock" or "block"
text = string.format(" [%02d] %s [来源:弹幕服务器%s] ", serial, url,
source.blocked and "(已屏蔽)" or "(未屏蔽)")
else
action = "delete"
text = string.format(" [%02d] %s [来源:用户添加] ", serial, url)
end
local style = (tonumber(select_num) == serial) and
"{\\c&HFFDE7F&\\b1}" or (action == "unblock" and "{\\c&H4C4CC3&\\b0}" or "{\\c&HCCCCCC&\\b0}")
deal_value[serial] = {value = url, action = action}
table.insert(menu_log, {text = text, style = style})
end
end
if serial == 0 then
table.insert(menu_log, { text = "", style = "" })
end
end
-- 显示菜单
local function show_menu(extra_lines, select_num)
rebuild_menu_log(select_num)
local display = {}
for _, item in ipairs(menu_log) do table.insert(display, item) end
table.insert(display, { text = "----------------------------", style = "{\\c&H888888&}" })
if extra_lines then
if #extra_lines < 2 then table.insert(display, { text = "\n", style = "" }) end
for _, line in ipairs(extra_lines) do table.insert(display, line) end
else
table.insert(display, { text = "\n", style = "" })
table.insert(display, {
text = "提示: 输入【选项数字】可屏蔽或删除既有弹幕源",
style = "{\\c&H999999&}"
})
end
input.set_log(display)
end
-- 获取操作提示
local function get_hint(action)
local hints = {
block = "按回车执行,屏蔽该弹幕源",
unblock = "按回车执行,解除该弹幕源的屏蔽",
delete = "按回车执行,删除该弹幕源"
}
return hints[action] or "按回车执行获取输入源地址url的弹幕"
end
input.get({
prompt = 'Input url:',
keep_open = true,
prompt = "请在此输入源地址url: ",
opened = function() show_menu() end,
edited = function(text)
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then
show_menu()
return
end
local num = tonumber(text)
local event = num and deal_value[num]
local hint = get_hint(event and event.action)
show_menu({
{ text = string.format("已输入: %s", text), style = "{\\c&HCCCCCC&}" },
{ text = hint, style = "{\\c&H999999&}" }
}, text)
end,
submit = function(text)
input.terminate()
mp.commandv("script-message-to", mp.get_script_name(), "add-source-event", text)
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then return end
local num = tonumber(text)
local event = num and deal_value[num]
if event then
local args = string.format('{"type":"activate","value":"%s","action":"%s"}',
string.gsub(event.value, '\\', '\\\\'), event.action)
mp.commandv("script-message-to", mp.get_script_name(), "setup-danmaku-source", args)
else
input.terminate()
mp.commandv("script-message-to", mp.get_script_name(), "add-source-event", text)
end
mp.add_timeout(0.1, show_menu)
end
})
end
@@ -263,19 +385,19 @@ end
function open_add_menu_uosc()
local sources = {}
for url, source in pairs(DANMAKU.sources) do
if source.fname then
if source.data then
local item = {title = url, value = url, keep_open = true,}
if source.from == "api_server" then
if source.blocked then
item.hint = "来源:弹幕服务器(已屏蔽)"
item.actions = {{icon = "check", name = "unblock"},}
item.actions = {{icon = "check", name = "unblock", label = "解除屏蔽"},}
else
item.hint = "来源:弹幕服务器(未屏蔽)"
item.actions = {{icon = "not_interested", name = "block"},}
item.actions = {{icon = "not_interested", name = "block", label = "屏蔽"},}
end
else
item.hint = "来源:用户添加"
item.actions = {{icon = "delete", name = "delete"},}
item.actions = {{icon = "delete", name = "delete", label = "删除"},}
end
table.insert(sources, item)
end
@@ -312,13 +434,36 @@ function open_content_menu(pos)
if COMMENTS ~= nil then
for _, event in ipairs(COMMENTS) do
local text = event.clean_text:gsub("^m%s[mbl%s%-%d%.]+$", ""):gsub("^%s*(.-)%s*$", "%1")
local delay = get_delay_for_time(DELAYS, event.start_time)
local start_time = event.start_time + delay
local end_time = event.end_time + delay
local delay = event.delay
local start_time = event.start_time
local end_time = event.end_time
if text and text ~= "" and start_time >= 0 and start_time <= duration then
local delay_label_suffix = nil
local delay_num = delay and tonumber(delay)
if delay_num and math.abs(delay_num) > 0 then
delay_label_suffix = string.format("已存在延迟: %+0.1fs", delay_num)
end
local adjust_label = '调整弹幕延迟'
if delay_label_suffix then
adjust_label = adjust_label .. '' .. delay_label_suffix .. ''
end
table.insert(items, {
title = abbr_str(text, 60),
hint = seconds_to_time(start_time),
hint = seconds_to_time(start_time) .. " (" .. utf8_sub(remove_query(event.source), 1, 70) .. ")",
actions = {
{
name = 'block_source',
icon = 'block',
label = '屏蔽对应弹幕源'
},
{
name = 'adjust_delay',
icon = 'more_time',
label = adjust_label,
},
},
value = { "seek", start_time, "absolute" },
active = time_pos >= start_time and time_pos <= end_time,
})
@@ -330,7 +475,9 @@ function open_content_menu(pos)
type = "menu_content",
title = "弹幕内容",
footnote = "使用 / 打开搜索",
items = items
items = items,
item_actions_place = "outside",
callback = {mp.get_script_name(), 'handle-danmaku-content-action'},
}
local json_props = utils.format_json(menu_props)
@@ -361,12 +508,134 @@ local menu_items_config = {
local ordered_keys = {"bold", "fontsize", "outline", "shadow", "scrolltime", "opacity", "displayarea"}
-- 设置弹幕样式菜单
function add_danmaku_setup(actived, status)
if not uosc_available then
show_message("无uosc UI框架不支持使用该功能", 2)
return
function open_style_menu_get(query, indicator)
mp.commandv('script-message-to', 'console', 'disable')
local menu_log = {}
local select_num = 0
local select_query = nil
if query then
if tonumber(query) ~= nil then
select_num = tonumber(query)
else
for i, v in ipairs(ordered_keys) do
if v == query then
select_num = i
end
end
end
select_query = ordered_keys[select_num]
end
local function build_menu(source)
menu_log = {
{ text = "【弹幕样式菜单】", style = "{\\c&H00CCFF&\\b1}" },
{ text = ("-"):rep(33), style = "{\\c&H888888&}" }
}
local serial = 0
for _, key in ipairs(ordered_keys) do
serial = serial + 1
local config = menu_items_config[key]
local text = string.format(" [%02d] %s [目前:%s] ", serial, config.title, config.hint)
text = config.hint ~= config.original and text .. "" or text
local style = serial == select_num and "{\\c&HFFDE7F&}" or "{\\c&HCCCCCC&}"
local item_config = { text = text, style = style }
table.insert(menu_log, item_config)
end
table.insert(menu_log, { text = ("-"):rep(33), style = "{\\c&H888888&}" })
if select_num == 0 then
table.insert(menu_log, {
text = "注: 样式更改仅在本次播放生效",
style = "{\\c&HFFDE7F&}"
})
table.insert(menu_log, {
text = "提示: 输入【w】可上移选项【s】可下移选项",
style = "{\\c&H999999&}"
})
else
local input_text = source and source or ""
local config = menu_items_config[select_query]
local suffix = ""
if config and config.hint ~= config.original then
suffix = "(输入\\r恢复默认配置"
end
input_text = string.format("已输入%s: %s", suffix, input_text)
local scope = config and config.footnote or ""
local hint_text = select_query == "bold" and "提示: 输入【y】切换状态" or "提示: " .. scope
local hint_style = "{\\c&H999999&}"
if source and source:lower() == "\\r" then
hint_text = string.format("提示: 回车将恢复默认配置 < %s >", config.original)
end
if indicator == "refresh" or indicator == "updata" then
indicator = ""
hint_text = "提示: 样式更改成功"
hint_style = "{\\c&HFFDE7F&}"
mp.add_timeout(1.5, build_menu)
elseif indicator == "error" then
indicator = ""
hint_text = "提示: 输入非数字字符或范围出错"
hint_style = "{\\c&H4C4CC3&}"
mp.add_timeout(1.5, build_menu)
end
table.insert(menu_log, { text = input_text, style = "{\\c&HCCCCCC&}" })
table.insert(menu_log, { text = hint_text, style = hint_style })
end
input.set_log(menu_log)
end
input.get({
keep_open = true,
prompt = "请在此输入操作w/s|上移/下移): ",
opened = function() build_menu() end,
edited = function(text)
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then
build_menu()
return
end
if text:lower() == "w" or text:lower() == "s" then
input.terminate()
select_num = text:lower() == "w" and select_num - 1 or select_num + 1
select_num = (select_num > #ordered_keys) and 1 or (select_num <= 0 and #ordered_keys or select_num)
mp.add_timeout(0.01, function()
open_style_menu_get(select_num)
end)
return
end
build_menu(text)
end,
submit = function(text)
if select_query == nil then return end
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then return end
if text:lower() == "\\r" then
input.terminate()
local args = string.format('{"type":"activate","action":"%s","index":%d}', select_query, select_num)
mp.commandv("script-message-to", mp.get_script_name(), "setup-danmaku-style", args)
else
if menu_items_config[select_query]["scope"] ~= nil then
input.terminate()
mp.commandv("script-message-to", mp.get_script_name(), "setup-danmaku-style", select_query, text)
elseif text:lower() == "y" and select_query == "bold" then
input.terminate()
local args = string.format('{"type":"activate","index":%d}', select_num)
mp.commandv("script-message-to", mp.get_script_name(), "setup-danmaku-style", args)
end
end
return
end
})
end
function open_style_menu_uosc(actived, status)
local items = {}
for _, key in ipairs(ordered_keys) do
local config = menu_items_config[key]
@@ -407,7 +676,7 @@ function add_danmaku_setup(actived, status)
elseif status == "error" then
menu_props.title = "输入非数字字符或范围出错"
-- 创建一个定时器在1秒后触发回调函数删除搜索栏错误信息
mp.add_timeout(1.0, function() add_danmaku_setup(actived, "updata") end)
mp.add_timeout(1.0, function() open_style_menu_uosc(actived, "updata") end)
end
menu_props.search_style = "palette"
menu_props.search_debounce = "submit"
@@ -419,8 +688,216 @@ function add_danmaku_setup(actived, status)
mp.commandv("script-message-to", "uosc", actions, json_props)
end
function open_style_menu(actived, status)
if uosc_available then
open_style_menu_uosc(actived, status)
elseif input_loaded then
mp.add_timeout(0.01, function()
open_style_menu_get(actived, status)
end)
else
show_message("无支持可用的 UI框架不支持使用该功能", 3)
end
end
-- 打开以指定时间为起点的延迟菜单
function open_delay_from_time_get(source, time, status)
mp.commandv('script-message-to', 'console', 'disable')
local menu_log = {}
local function build_menu(query, input_text)
menu_log = {
{ text = "【从该时间起调整弹幕延迟】", style = "{\\c&H00CCFF&\\b1}" },
{ text = ("-"):rep(33), style = "{\\c&H888888&}" }
}
table.insert(menu_log, { text = "\n", style = "" })
local hint_text = "提示:请输入数字,单位(秒)/ 或者按照形如\"14m15s\"的格式输入分钟数加秒数"
local hint_style = "{\\c&H999999&}"
if status == "error" then
hint_text = "提示: 输入非数字字符或范围出错"
hint_style = "{\\c&H4C4CC3&}"
end
table.insert(menu_log, { text = input_text and ("已输入:" .. input_text) or "", style = "{\\c&HCCCCCC&}" })
table.insert(menu_log, { text = hint_text, style = hint_style })
input.set_log(menu_log)
end
input.get({
keep_open = true,
prompt = "请输入要设置的延迟(秒或 XmYs: ",
opened = function() build_menu() end,
edited = function(text)
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then
build_menu()
return
end
build_menu(text)
end,
submit = function(text)
text = text and text:gsub("^%s*(.-)%s*$", "%1") or ""
if text == "" then return end
input.terminate()
local parsed = parse_delay_input(text)
if parsed ~= nil then
mp.commandv("script-message", "danmaku-delay", tostring(parsed), tostring(time), tostring(source))
else
open_delay_from_time(time, "error")
end
end
})
end
function open_delay_from_time_uosc(source, time, status)
if not uosc_available then
show_message("无uosc UI框架不支持使用该功能", 2)
return
end
local menu_props = {
type = "menu_delay_from_time",
title = "从该时间起调整弹幕延迟",
search_style = "palette",
search_debounce = "submit",
footnote = "请输入数字,单位(秒)/ 或者按照形如\"14m15s\"的格式输入分钟数加秒数",
items = {},
on_search = { "script-message-to", mp.get_script_name(), "setup-content-delay", tostring(time), tostring(source) },
}
if status == "error" then
menu_props.title = "输入非数字字符或范围出错"
mp.add_timeout(1.0, function() open_delay_from_time_uosc(source, time) end)
end
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
end
function open_delay_from_time(source, time, status)
if uosc_available then
open_delay_from_time_uosc(source, time, status)
elseif input_loaded then
mp.add_timeout(0.01, function()
open_delay_from_time_get(source, time, status)
end)
else
show_message("无支持可用的 UI框架不支持使用该功能", 3)
end
end
-- 设置弹幕源延迟菜单
function danmaku_delay_setup(source_url)
function open_delay_menu_get(source, status)
mp.commandv('script-message-to', 'console', 'disable')
local menu_log = {}
local serial = 0
local select_num = 0
if source and tonumber(source) ~= nil then
select_num = tonumber(source)
end
local select_url = nil
local function build_menu(query, text)
menu_log = {
{ text = "【弹幕源延迟菜单】", style = "{\\c&H00CCFF&\\b1}" },
{ text = ("-"):rep(33), style = "{\\c&H888888&}" }
}
serial, select_num = 0, 0
for url, src in pairs(DANMAKU.sources) do
if src.data and not src.blocked then
local delay = 0
serial = serial + 1
select_num = (url == source) and serial or select_num
if src.delay_segments then
for _, seg in ipairs(src.delay_segments) do
if seg.start == 0 then
delay = seg.delay or 0
break
end
end
end
local hint = "当前弹幕源延迟: " .. string.format("%.1f", delay + 1e-10) .. ""
local text = string.format(" [%02d] %s [%s] ", serial, url, hint)
local style = (serial == select_num) and "{\\c&HFFDE7F&}" or "{\\c&HCCCCCC&}"
table.insert(menu_log, { text = text, style = style })
select_url = serial == select_num and url or select_url
end
end
if serial == 0 then
table.insert(menu_log, { text = "", style = "" })
end
table.insert(menu_log, { text = ("-"):rep(33), style = "{\\c&H888888&}" })
if select_num == 0 then
table.insert(menu_log, { text = "\n", style = "" })
table.insert(menu_log, {
text = "提示: 输入【w】可上移选项【s】可下移选项",
style = "{\\c&H999999&}"
})
else
local input_text = "已输入:" .. (text ~= nil and text or "")
local hint_text = "提示:请输入数字,单位(秒)/ 或者按照形如\"14m15s\"的格式输入分钟数加秒数"
local hint_style = "{\\c&H999999&}"
if status == "refresh" then
status = ""
hint_text = "提示: 样式更改成功"
hint_style = "{\\c&HFFDE7F&}"
mp.add_timeout(1.5, build_menu)
elseif status == "error" then
status = ""
hint_text = "提示: 输入非数字字符或范围出错"
hint_style = "{\\c&H4C4CC3&}"
mp.add_timeout(1.5, build_menu)
end
-- table.insert(menu_log, { text = input_text, style = "{\\c&HCCCCCC&}" })
table.insert(menu_log, { text = input_text, style = "{\\c&HCCCCCC&}" })
table.insert(menu_log, { text = hint_text, style = hint_style })
end
input.set_log(menu_log)
end
input.get({
keep_open = true,
prompt = "请在此输入操作w/s|上移/下移): ",
opened = function() build_menu() end,
edited = function(text)
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then
build_menu()
return
end
if text:lower() == "w" or text:lower() == "s" then
input.terminate()
select_num = text:lower() == "w" and select_num - 1 or select_num + 1
select_num = (select_num > serial) and 1 or (select_num <= 0 and serial or select_num)
mp.add_timeout(0.01, function()
open_delay_menu_get(select_num)
end)
return
end
build_menu(select_num, text)
end,
submit = function(text)
if select_url == nil then return end
text = text:gsub("^%s*(.-)%s*$", "%1")
if text == "" then return end
input.terminate()
mp.commandv("script-message-to", mp.get_script_name(), "setup-source-delay", select_url, text)
return
end
})
end
function open_delay_menu_uosc(source_url, status)
if not uosc_available then
show_message("无uosc UI框架不支持使用该功能", 2)
return
@@ -428,7 +905,7 @@ function danmaku_delay_setup(source_url)
local sources = {}
for url, source in pairs(DANMAKU.sources) do
if source.fname and not source.blocked then
if source.data and not source.blocked then
local delay = 0
if source.delay_segments then
for _, seg in ipairs(source.delay_segments) do
@@ -453,7 +930,13 @@ function danmaku_delay_setup(source_url)
callback = {mp.get_script_name(), 'setup-source-delay'},
}
if source_url ~= nil then
menu_props.title = "请输入数字,单位(秒)/ 或者按照形如\"14m15s\"的格式输入分钟数加秒数"
if status == "error" then
menu_props.title = "输入非数字字符或范围出错"
-- 创建一个定时器在1秒后触发回调函数删除搜索栏错误信息
mp.add_timeout(1.0, function() open_delay_menu_uosc(source_url) end)
else
menu_props.title = "请输入数字,单位(秒)/ 或者按照形如\"14m15s\"的格式输入分钟数加秒数"
end
menu_props.search_style = "palette"
menu_props.search_debounce = "submit"
menu_props.on_search = { "script-message-to", mp.get_script_name(), "setup-source-delay", source_url }
@@ -463,18 +946,29 @@ function danmaku_delay_setup(source_url)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
end
function open_delay_menu(source, status)
if uosc_available then
open_delay_menu_uosc(source, status)
elseif input_loaded then
mp.add_timeout(0.01, function()
open_delay_menu_get(source, status)
end)
else
show_message("无支持可用的 UI框架不支持使用该功能", 3)
end
end
-- 总集合弹幕菜单
local total_menu_items_config = {
{ title = "弹幕搜索", action = "open_search_danmaku_menu" },
{ title = "从源添加弹幕", action = "open_add_source_menu" },
{ title = "弹幕源延迟设置", action = "open_source_delay_menu" },
{ title = "弹幕样式", action = "open_danmaku_style_menu" },
{ title = "弹幕内容", action = "open_content_danmaku_menu" },
}
function open_add_total_menu_uosc()
local items = {}
local total_menu_items_config = {
{ title = "弹幕搜索", action = "open_search_danmaku_menu" },
{ title = "从源添加弹幕", action = "open_add_source_menu" },
{ title = "弹幕源延迟设置", action = "open_source_delay_menu" },
{ title = "弹幕样式", action = "open_setup_danmaku_menu" },
{ title = "弹幕内容", action = "open_content_danmaku_menu" },
}
if DANMAKU.anime and DANMAKU.episode then
local episode = DANMAKU.episode:gsub("%s.-$","")
@@ -509,11 +1003,6 @@ end
function open_add_total_menu_select()
local item_titles, item_values = {}, {}
local total_menu_items_config = {
{ title = "弹幕搜索", action = "open_search_danmaku_menu" },
{ title = "从源添加弹幕", action = "open_add_source_menu" },
{ title = "弹幕内容", action = "open_content_danmaku_menu" },
}
for i, config in ipairs(total_menu_items_config) do
item_titles[i] = config.title
item_values[i] = { "script-message-to", mp.get_script_name(), config.action }
@@ -537,7 +1026,7 @@ function open_add_total_menu()
end
end
-- 添加 uosc 菜单栏按钮
mp.commandv(
"script-message-to",
"uosc",
@@ -570,7 +1059,7 @@ mp.commandv(
utils.format_json({
icon = "palette",
tooltip = "弹幕样式",
command = "script-message open_setup_danmaku_menu",
command = "script-message open_danmaku_style_menu",
})
)
@@ -611,8 +1100,8 @@ mp.register_script_message("set", function(prop, value)
if value == "on" then
ENABLED = true
set_danmaku_visibility(true)
if COMMENTS == nil then
set_danmaku_visibility(true)
local path = mp.get_property("path")
init(path)
else
@@ -622,7 +1111,6 @@ mp.register_script_message("set", function(prop, value)
else
show_message("关闭弹幕", 2)
ENABLED = false
set_danmaku_visibility(false)
hide_danmaku_func()
end
@@ -664,12 +1152,13 @@ mp.register_script_message("add-source-event", function(query)
add_danmaku_source(query, true)
end)
mp.register_script_message("open_setup_danmaku_menu", function()
mp.register_script_message("open_danmaku_style_menu", function()
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_total")
end
add_danmaku_setup()
open_style_menu()
end)
mp.register_script_message("open_content_danmaku_menu", function()
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_total")
@@ -677,6 +1166,17 @@ mp.register_script_message("open_content_danmaku_menu", function()
open_content_menu()
end)
mp.register_script_message("open-latest-menu-anime", function ()
if uosc_available then
mp.commandv("script-message-to", "uosc", "open-menu", latest_menu_anime)
elseif input_loaded then
show_message("", 0)
mp.add_timeout(0.1, function()
open_menu_select(utils.parse_json(latest_menu_anime))
end)
end
end)
mp.register_script_message("setup-danmaku-style", function(query, text)
local event = utils.parse_json(query)
if event ~= nil then
@@ -688,13 +1188,13 @@ mp.register_script_message("setup-danmaku-style", function(query, text)
menu_items_config.bold.hint = options.bold and "true" or "false"
end
-- "updata" 模式会保留输入框文字
add_danmaku_setup(ordered_keys[event.index], "updata")
open_style_menu(ordered_keys[event.index], "updata")
return
else
-- msg.info("event.action" .. event.action)
options[event.action] = menu_items_config[event.action]["original"]
menu_items_config[event.action]["hint"] = options[event.action]
add_danmaku_setup(event.action, "updata")
open_style_menu(event.action, "updata")
if event.action == "fontsize" or event.action == "scrolltime" then
load_danmaku(true)
end
@@ -718,14 +1218,14 @@ mp.register_script_message("setup-danmaku-style", function(query, text)
options[query] = tostring(num)
menu_items_config[query]["hint"] = options[query]
-- "refresh" 模式会清除输入框文字
add_danmaku_setup(query, "refresh")
open_style_menu(query, "refresh")
if query == "fontsize" or query == "scrolltime" then
load_danmaku(true, true)
end
return
end
end
add_danmaku_setup(query, "error")
open_style_menu(query, "error")
end
end)
@@ -734,14 +1234,10 @@ mp.register_script_message('setup-danmaku-source', function(json)
if event.type == 'activate' then
if event.action == "delete" then
local rm = DANMAKU.sources[event.value]["fname"]
if rm and file_exists(rm) and DANMAKU.sources[event.value]["from"] ~= "user_local" then
os.remove(rm)
end
DANMAKU.sources[event.value] = nil
remove_source_from_history(event.value)
mp.commandv("script-message-to", "uosc", "close-menu", "menu_source")
open_add_menu_uosc()
open_add_menu()
load_danmaku(true)
end
@@ -749,7 +1245,7 @@ mp.register_script_message('setup-danmaku-source', function(json)
DANMAKU.sources[event.value]["blocked"] = true
add_source_to_history(event.value, DANMAKU.sources[event.value])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_source")
open_add_menu_uosc()
open_add_menu()
load_danmaku(true)
end
@@ -757,7 +1253,7 @@ mp.register_script_message('setup-danmaku-source', function(json)
DANMAKU.sources[event.value]["blocked"] = false
add_source_to_history(event.value, DANMAKU.sources[event.value])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_source")
open_add_menu_uosc()
open_add_menu()
load_danmaku(true)
end
end
@@ -768,39 +1264,74 @@ mp.register_script_message("setup-source-delay", function(query, text)
if event ~= nil then
-- item点击
if event.type == "activate" then
danmaku_delay_setup(event.value)
open_delay_menu(event.value)
end
else
-- 数值输入
if text == nil or text == "" then
return
end
local newText, _ = text:gsub("%s", "") -- 移除所有空白字符
local num = tonumber(newText)
local delay_segments = shallow_copy(DANMAKU.sources[query]["delay_segments"] or {})
for i = #delay_segments, 1, -1 do
if delay_segments[i].start == 0 then
table.remove(delay_segments, i)
end
local delay = parse_delay_input(text)
if delay ~= nil then
mp.commandv("script-message", "danmaku-delay", tostring(delay), "0")
mp.commandv("script-message-to", "uosc", "close-menu", "menu_delay")
mp.add_timeout(0.1, function()
open_delay_menu(query, "refresh")
end)
else
open_delay_menu(query, "error")
end
if num ~= nil then
table.insert(delay_segments, 1, { start = 0, delay = tonumber(num) })
DANMAKU.sources[query]["delay_segments"] = delay_segments
add_source_to_history(query, DANMAKU.sources[query])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_delay")
danmaku_delay_setup(query)
load_danmaku(true, true)
elseif newText:match("^%-?%d+m%d+s$") then
local minutes, seconds = string.match(newText, "^(%-?%d+)m(%d+)s$")
minutes = tonumber(minutes)
seconds = tonumber(seconds)
if minutes < 0 then seconds = -seconds end
table.insert(delay_segments, 1, { start = 0, delay = 60 * minutes + seconds })
DANMAKU.sources[query]["delay_segments"] = delay_segments
add_source_to_history(query, DANMAKU.sources[query])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_delay")
danmaku_delay_setup(query)
load_danmaku(true, true)
end
end)
mp.register_script_message('handle-danmaku-content-action', function(json)
local event = utils.parse_json(json)
if not event or event.type ~= 'activate' then return end
if event.action then
local d = COMMENTS[event.index]
if not d or not d.source then return end
if event.action == "block_source" then
DANMAKU.sources[d.source]["blocked"] = true
add_source_to_history(d.source, DANMAKU.sources[d.source])
mp.commandv("script-message-to", "uosc", "close-menu", "menu_content")
load_danmaku(true)
elseif event.action == "adjust_delay" then
-- 打开以该弹幕时间为起点的延迟菜单(该延迟将作用于该时间点及之后的弹幕),仅针对该条弹幕的 source
mp.commandv("script-message", "open_content_delay_menu", d.source, tostring(d.start_time))
end
else
if event.value then
if type(event.value) == "table" then
mp.commandv(unpack(event.value))
else
mp.command(event.value)
end
mp.commandv("script-message-to", "uosc", "close-menu", "menu_content")
end
end
end)
mp.register_script_message("open_content_delay_menu", function(source, time)
open_delay_from_time(source, tonumber(time))
end)
mp.register_script_message("setup-content-delay", function(...)
local args = {...}
if #args == 1 then
return
end
if #args >= 2 then
local time = tonumber(args[1])
local source = args[2]
local delay_str = args[3]
local delay = parse_delay_input(delay_str)
if delay ~= nil then
mp.commandv("script-message", "danmaku-delay", tostring(delay), tostring(time), tostring(source))
mp.commandv("script-message-to", "uosc", "close-menu", "menu_delay_from_time")
else
open_delay_from_time(source, tonumber(time), "error")
end
end
end)
+5 -4
View File
@@ -5,19 +5,20 @@ options = {
-- 指定弹幕服务器地址,自定义服务需兼容 dandanplay 的 api
api_server = "https://api.dandanplay.net",
-- 指定 b 站和爱腾优的弹幕获取的兜底服务器地址,主要用于获取非动画弹幕
-- 服务器可以自托管https://github.com/lyz05/danmaku
fallback_server = "https://fc.lyz05.cn",
-- 可用: https://api.danmu.icuhttps://dmku.hls.one
fallback_server = "https://api.danmu.icu",
-- 设置 tmdb 的 API Key用于获取非动画条目的中文信息(当搜索内容非中文时)
-- 可以在 https://www.themoviedb.org 注册后去个人账号设置界面获取
-- 注意:自定义此参数时还需要对获取到的 API Key 进行 base64 编码
tmdb_api_key = "NmJmYjIxOTZkNzIyN2UyMTIzMGM3Y2YzZjQ4MDNkZGM=",
load_more_danmaku = false,
auto_load = false,
autoload_local_danmaku = false,
autoload_for_url = false,
save_danmaku = false,
user_agent = "mpv_danmaku/1.0",
proxy = "",
-- 可选:向 HTTP 请求传递 cookie.txt 文件路径
cookie_file = "",
-- 使用 fps 视频滤镜,大幅提升弹幕平滑度。默认禁用
vf_fps = false,
-- 设置要使用的 fps 滤镜参数
@@ -73,4 +74,4 @@ options = {
]],
}
opt.read_options(options, mp.get_script_name(), function() end)
opt.read_options(options, mp.get_script_name(), function() end)
+303 -238
View File
@@ -40,10 +40,50 @@ local function load_blacklist_patterns(filepath)
return patterns
end
for line in file:lines() do
line = line:match("^%s*(.-)%s*$")
if line ~= "" then
table.insert(patterns, line)
if string.match(filepath, "%.xml$") then
-- xml文件格式示例
--<?xml version="1.0" encoding="utf-8"?>
--<filters>
-- <item enabled="true">t=卡在</item>
-- <item enabled="true">t=进度条</item>
--</filters>
print("加载黑名单文件: " .. filepath)
for line in file:lines() do
local pattern = line:match('<item%s+enabled="true">t=(.-)</item>')
if pattern then
print("加载黑名单模式: " .. pattern)
table.insert(patterns, pattern)
end
end
end
if string.match(filepath, "%.json$") then
-- json文件格式示例
-- [{"type":0,"filter":"开门","opened":true,"id":15628936}
-- ,{"type":0,"filter":"tony","opened":true,"id":15628939}
-- ,{"type":1,"filter":"0+.1","opened":true,"id":15628951}]
local content = read_file(filepath)
if content then
local json = utils.parse_json(content)
if json and type(json) == "table" then
for _, entry in ipairs(json) do
if entry.opened and entry.filter and entry.type == 0 then
table.insert(patterns, entry.filter)
end
end
end
end
end
if string.match(filepath, "%.txt$") then
-- 文本文件格式示例
-- 卡在
-- 进度条
for line in file:lines() do
line = line:match("^%s*(.-)%s*$")
if line ~= "" then
table.insert(patterns, line)
end
end
end
@@ -114,50 +154,62 @@ local function merge_duplicate_danmaku(danmakus, threshold)
local groups = {}
for _, d in ipairs(danmakus) do
local key = d.type .. "|" .. d.color .. "|" .. d.text
if not groups[key] then groups[key] = {} end
table.insert(groups[key], d)
local tkey = tostring(d.type or "")
local ckey = tostring(d.color or "")
local text = d.text or ""
groups[tkey] = groups[tkey] or {}
groups[tkey][ckey] = groups[tkey][ckey] or {}
groups[tkey][ckey][text] = groups[tkey][ckey][text] or {}
table.insert(groups[tkey][ckey][text], d)
end
local merged = {}
local abs = math.abs
for _, group in pairs(groups) do
table.sort(group, function(a, b) return a.time < b.time end)
for _, bytype in pairs(groups) do
for _, bycolor in pairs(bytype) do
for _, group in pairs(bycolor) do
table.sort(group, function(a, b) return a.time < b.time end)
local i = 1
while i <= #group do
local base = group[i]
local times = { base.time }
local count = 1
local j = i + 1
local i = 1
while i <= #group do
local base = group[i]
local times = { base.time }
local count = 1
local j = i + 1
while j <= #group and math.abs(group[j].time - base.time) <= threshold do
table.insert(times, group[j].time)
count = count + 1
j = j + 1
end
while j <= #group and abs(group[j].time - base.time) <= threshold do
times[#times+1] = group[j].time
count = count + 1
j = j + 1
end
local same_time = true
for k = 2, #times do
if times[k] ~= times[1] then
same_time = false
break
local same_time = true
for k = 2, #times do
if times[k] ~= times[1] then
same_time = false
break
end
end
local danmaku = {
time = base.time,
type = base.type,
size = base.size,
color = base.color,
text = base.text,
source = base.source,
orig_time = base.orig_time,
}
if count > 2 or not same_time then
danmaku.text = danmaku.text .. string.format("x%d", count)
end
table.insert(merged, danmaku)
i = j
end
end
local danmaku = {
time = base.time,
type = base.type,
size = base.size,
color = base.color,
text = base.text,
}
if count > 2 or not same_time then
danmaku.text = danmaku.text .. string.format("x%d", count)
end
table.insert(merged, danmaku)
i = j
end
end
@@ -207,9 +259,11 @@ local function limit_danmaku(danmakus, limit)
end
-- 解析 XML 弹幕
local function parse_xml_danmaku(xml_string, delay_segments)
local function parse_xml_danmaku(xml_string)
local danmakus = {}
for p_attr, text in xml_string:gmatch('<d p="([^"]+)">([^<]+)</d>') do
-- [^>]* 匹配其他 attributes
-- %f[^%s] 确保 p= 前面是空白字符
for p_attr, text in xml_string:gmatch('<d%s+[^>]*%f[^%s]p="([^"]+)"[^>]*>([^<]+)</d>') do
local params = {}
local i = 1
for val in p_attr:gmatch("([^,]+)") do
@@ -218,10 +272,8 @@ local function parse_xml_danmaku(xml_string, delay_segments)
end
if params[1] and params[2] and params[3] and params[4] then
local base_time = params[1]
local delay = get_delay_for_time(delay_segments, base_time)
table.insert(danmakus, {
time = base_time + delay,
time = params[1],
type = params[2] or 1,
size = params[3] or 25,
color = params[4] or 0xFFFFFF,
@@ -235,7 +287,7 @@ local function parse_xml_danmaku(xml_string, delay_segments)
end
-- 解析 JSON 弹幕
local function parse_json_danmaku(json_string, delay_segments)
local function parse_json_danmaku(json_string)
local danmakus = {}
if json_string:sub(1, 3) == "\239\187\191" then
json_string = json_string:sub(4)
@@ -259,10 +311,8 @@ local function parse_json_danmaku(json_string, delay_segments)
end
if params[1] and params[2] and params[3] and params[4] then
local base_time = params[1]
local delay = get_delay_for_time(delay_segments, base_time)
table.insert(danmakus, {
time = base_time + delay,
time = params[1],
color = params[2] or 0xFFFFFF,
type = params[3] or 1,
size = params[4] or 25,
@@ -277,64 +327,39 @@ local function parse_json_danmaku(json_string, delay_segments)
end
-- 解析弹幕文件
function parse_danmaku_files(danmaku_input, delays)
local DANMAKU_PATHs = {}
if type(danmaku_input) == "string" then
DANMAKU_PATHs = { danmaku_input }
else
for i, input in ipairs(danmaku_input) do
DANMAKU_PATHs[#DANMAKU_PATHs + 1] = input
end
end
function parse_danmaku_file(danmaku_input)
local danmakus = {}
local all_danmaku = {}
if file_exists(danmaku_input) then
local content = read_file(danmaku_input)
if content then
local parsed = {}
if danmaku_input:match("%.xml$") then
parsed = parse_xml_danmaku(content)
elseif danmaku_input:match("%.json$") then
parsed = parse_json_danmaku(content)
end
for i, DANMAKU_PATH in ipairs(DANMAKU_PATHs) do
if file_exists(DANMAKU_PATH) then
local content = read_file(DANMAKU_PATH)
if content then
local parsed = {}
local delay_segments = delays and delays[i] or {}
if DANMAKU_PATH:match("%.xml$") then
parsed = parse_xml_danmaku(content, delay_segments)
elseif DANMAKU_PATH:match("%.json$") then
parsed = parse_json_danmaku(content, delay_segments)
end
for _, d in ipairs(parsed) do
local matched, pattern = is_blacklisted(d.text, black_patterns)
if not matched then
d.text = ch_convert_cached(d.text)
table.insert(all_danmaku, d)
else
-- msg.debug("命中黑名单: " .. pattern)
end
end
else
msg.info("无法读取文件内容: " .. DANMAKU_PATH)
for _, d in ipairs(parsed) do
table.insert(danmakus, d)
end
else
msg.info("文件不存在: " .. DANMAKU_PATH)
msg.info("无法读取文件内容: " .. danmaku_input)
end
else
msg.info("文件不存在: " .. danmaku_input)
end
if #all_danmaku == 0 then
for _, d in ipairs(danmakus) do
if d.orig_time == nil then d.orig_time = d.time end
end
if #danmakus == 0 then
msg.info("未能解析任何弹幕")
return nil
end
if options.max_screen_danmaku > 0 and options.merge_tolerance <= 0 then
options.merge_tolerance = options.scrolltime
end
-- 按时间排序
table.sort(all_danmaku, function(a, b)
return a.time < b.time
end)
all_danmaku = merge_duplicate_danmaku(all_danmaku, options.merge_tolerance)
return all_danmaku
return danmakus
end
--# 弹幕数组与布局算法 (Danmaku Array & Layout Algorithms)
@@ -349,7 +374,7 @@ function DanmakuArray:new(res_x, res_y, font_size)
time_length_array = {}
}
for i = 1, obj.rows do
obj.time_length_array[i] = { time = -1, length = 0 }
obj.time_length_array[i] = { time = 0, length = 0, empty = true }
end
setmetatable(obj, self)
return obj
@@ -357,7 +382,7 @@ end
function DanmakuArray:set_time_length(row, time, length)
if row > 0 and row <= self.rows then
self.time_length_array[row] = { time = time, length = length }
self.time_length_array[row] = { time = time, length = length, empty = false }
end
end
@@ -375,15 +400,20 @@ function DanmakuArray:get_length(row)
return 0
end
function DanmakuArray:is_empty(row)
if row > 0 and row <= self.rows then
return self.time_length_array[row].empty
end
return false
end
-- 滚动弹幕 Y 坐标算法
function get_position_y(font_size, appear_time, text_length, resolution_x, roll_time, array)
local velocity = (text_length + resolution_x) / roll_time
local best_row = 0
local best_bias = -math.huge
for i = 1, array.rows do
local previous_appear_time = array:get_time(i)
if array:get_time(i) < 0 then
if array:is_empty(i) then
array:set_time_length(i, appear_time, text_length)
return 1 + (i - 1) * font_size
end
@@ -391,7 +421,7 @@ function get_position_y(font_size, appear_time, text_length, resolution_x, roll_
local previous_length = array:get_length(i)
local previous_velocity = (previous_length + resolution_x) / roll_time
local delta_velocity = velocity - previous_velocity
local delta_x = (appear_time - previous_appear_time) * previous_velocity - (previous_length + text_length) / 2
local delta_x = (appear_time - previous_appear_time) * previous_velocity - previous_length
if delta_x >= 0 then
if delta_velocity <= 0 then
@@ -400,22 +430,10 @@ function get_position_y(font_size, appear_time, text_length, resolution_x, roll_
end
local delta_time = delta_x / delta_velocity
local bias = appear_time - previous_appear_time - delta_time
-- 判断:追及点是否在屏幕之外
local t_catch = previous_appear_time + delta_time
local distance_prev = previous_velocity * (t_catch - previous_appear_time)
if distance_prev > resolution_x then
-- 追及发生在屏幕之外,允许放置
if delta_time >= roll_time then
array:set_time_length(i, appear_time, text_length)
return 1 + (i - 1) * font_size
end
if bias > 0 then
array:set_time_length(i, appear_time, text_length)
return 1 + (i - 1) * font_size
elseif bias > best_bias then
best_bias = bias
best_row = i
end
end
end
-- 所有行都被占用,放弃渲染
@@ -424,8 +442,6 @@ end
-- 固定弹幕 Y 坐标算法
function get_fixed_y(font_size, appear_time, fixtime, array, from_top)
local best_row = 0
local best_bias = -1
local row_start, row_end, row_step
if from_top then
row_start, row_end, row_step = 1, array.rows, 1
@@ -435,7 +451,7 @@ function get_fixed_y(font_size, appear_time, fixtime, array, from_top)
for i = row_start, row_end, row_step do
local previous_appear_time = array:get_time(i)
if previous_appear_time < 0 then
if array:is_empty(i) then
array:set_time_length(i, appear_time, 0)
return (i - 1) * font_size + 1
else
@@ -443,9 +459,6 @@ function get_fixed_y(font_size, appear_time, fixtime, array, from_top)
if delta_time > fixtime then
array:set_time_length(i, appear_time, 0)
return (i - 1) * font_size + 1
elseif delta_time > best_bias then
best_bias = delta_time
best_row = i
end
end
end
@@ -453,21 +466,155 @@ function get_fixed_y(font_size, appear_time, fixtime, array, from_top)
return nil
end
-- 将弹幕转换为 ASS 格式
function convert_danmaku_to_ass(all_danmaku, danmaku_file)
if #all_danmaku == 0 then
msg.info("弹幕文件为空或解析失败")
-- 将弹幕转换为 XML 格式
function convert_danmaku_to_xml(danmaku_out)
local danmakus = {}
for _, source in pairs(DANMAKU.sources) do
if not source.blocked and source.data then
for _, d in ipairs(source.data) do
table.insert(danmakus, d)
end
end
end
if #danmakus == 0 then
show_message("弹幕内容为空,无法保存", 3)
msg.verbose("弹幕内容为空,无法保存")
COMMENTS = {}
return false
end
msg.info("已解析 " .. #all_danmaku .. " 条弹幕")
local alpha = string.format("%02X", (1 - tonumber(options.opacity)) * 255)
local bold = options.bold and "1" or "0"
-- 拼接为 XML 内容
local xml = { '<?xml version="1.0" encoding="UTF-8"?><i>\n' }
for _, d in ipairs(danmakus) do
local time = d.time
local type = d.type or 1
local size = d.size or 25
local color = d.color or 0xFFFFFF
local text = d.text or ""
text = text:gsub("&", "&amp;")
:gsub("<", "&lt;")
:gsub(">", "&gt;")
:gsub("\"", "&quot;")
:gsub("'", "&apos;")
table.insert(xml, string.format('<d p="%s,%s,%s,%s">%s</d>\n', time, type, size, color, text))
end
table.insert(xml, '</i>')
-- 写入 XML 文件
local file = io.open(danmaku_out, "w")
if not file then
show_message("无法写入目标 XML 文件", 3)
msg.info("无法写入目标 XML 文件: " .. danmaku_out)
return false
end
file:write(table.concat(xml))
file:close()
show_message("转换 XML 弹幕成功: " .. danmaku_out, 3)
msg.info("转换 XML 弹幕成功: " .. danmaku_out)
return true
end
function convert_danmaku_to_ass_events(force)
local per_source_lists = {}
for url, source in pairs(DANMAKU.sources) do
if not source.blocked and source.data then
local segments = nil
local prefix = nil
if source.delay_segments and #source.delay_segments > 0 then
segments = {}
for i, v in ipairs(source.delay_segments) do segments[i] = v end
table.sort(segments, function(a, b) return (a.start or 0) < (b.start or 0) end)
prefix = {}
local s = 0
for i, v in ipairs(segments) do
s = s + (v.delay or 0)
prefix[i] = s
end
end
local function get_cached_delay(t)
local segs = segments or {}
local pre = prefix or {}
if #segs == 0 then return 0 end
local idx = binary_search(segs, t, function(s) return (s and s.start) or 0 end)
local target = idx - 1
if target < 1 then return 0 end
return pre[target] or 0
end
local list = {}
for _, d in ipairs(source.data) do
local base_time = d.orig_time or d.time
if d.orig_time == nil then d.orig_time = base_time end
local adjusted_time = base_time + get_cached_delay(base_time)
local entry = {
orig_time = d.orig_time,
time = adjusted_time,
type = d.type,
size = d.size,
color = d.color,
text = d.text,
source = url,
}
if not is_blacklisted(d.text, black_patterns) then
table.insert(list, entry)
end
end
if #list > 0 then
table.sort(list, function(a, b) return a.time < b.time end)
per_source_lists[#per_source_lists + 1] = list
end
end
end
local danmakus = {}
local heap = new_min_heap()
for li = 1, #per_source_lists do
local lst = per_source_lists[li]
if lst and #lst > 0 then
heap.push({ time = lst[1].time, list_idx = li, pos = 1, entry = lst[1] })
end
end
while true do
local node = heap.pop()
if not node then break end
table.insert(danmakus, node.entry)
local li = node.list_idx
local next_pos = node.pos + 1
local lst = per_source_lists[li]
if lst and lst[next_pos] then
heap.push({ time = lst[next_pos].time, list_idx = li, pos = next_pos, entry = lst[next_pos] })
end
end
if options.max_screen_danmaku > 0 and options.merge_tolerance <= 0 then
options.merge_tolerance = options.scrolltime
end
danmakus = merge_duplicate_danmaku(danmakus, options.merge_tolerance)
if #danmakus == 0 then
if not force then
show_message("该集弹幕内容为空,结束加载", 3)
msg.verbose("该集弹幕内容为空,结束加载")
end
COMMENTS = {}
return
end
if not force then
msg.info("已解析 " .. #danmakus .. " 条弹幕")
end
local fontsize = tonumber(options.fontsize) or 50
local scrolltime = tonumber(options.scrolltime) or 15
local fixtime = tonumber(options.fixtime) or 5
local outline = tonumber(options.outline) or 1.0
local shadow = tonumber(options.shadow) or 0.0
local res_x = 1920
local res_y = 1080
@@ -475,33 +622,11 @@ function convert_danmaku_to_ass(all_danmaku, danmaku_file)
local roll_array = DanmakuArray:new(res_x, res_y, fontsize)
local top_array = DanmakuArray:new(res_x, res_y, fontsize)
local ass_header = string.format([[
[Script Info]
Title: DanmakuConvert for mpv
ScriptType: v4.00+
Collisions: Normal
PlayResX: %d
PlayResY: %d
Timer: 100.0000
WrapStyle: 2
ScaledBorderAndShadow: yes
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: R2L,%s,%d,&H%sFFFFFF,&H00FFFFFF,&H00000000,&H%s000000,%d,0,0,0,100,100,0,0,1,%.1f,%.1f,7,0,0,0,1
Style: TOP,%s,%d,&H%sFFFFFF,&H00FFFFFF,&H00000000,&H%s000000,%d,0,0,0,100,100,0,0,1,%.1f,%.1f,8,0,0,0,1
Style: BTM,%s,%d,&H%sFFFFFF,&H00FFFFFF,&H00000000,&H%s000000,%d,0,0,0,100,100,0,0,1,%.1f,%.1f,2,0,0,0,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
]], res_x, res_y, options.fontname, fontsize, alpha, alpha, bold, outline, shadow,
options.fontname, fontsize, alpha, alpha, bold, outline, shadow,
options.fontname, fontsize, alpha, alpha, bold, outline, shadow)
-- 预处理弹幕,先计算时间段以便进行数量限制
local pre_events = {}
for _, d in ipairs(all_danmaku) do
for _, d in ipairs(danmakus) do
local time = d.type == 1 and math.floor(d.time + 0.5) or d.time
local orig_time = d.type == 1 and math.floor(d.orig_time + 0.5) or d.orig_time
local appear_time = time
local danmaku_type = d.type
@@ -513,7 +638,7 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
end
if end_time then
table.insert(pre_events, {start_time = appear_time, end_time = end_time, danmaku = d})
table.insert(pre_events, {orig_time = orig_time, start_time = appear_time, end_time = end_time, danmaku = d})
end
end
@@ -526,7 +651,8 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
local d = ev.danmaku
local appear_time = ev.start_time
local danmaku_type = d.type
local text = ass_escape(decode_html_entities(d.text))
local clean_text = ch_convert_cached(decode_html_entities(d.text))
local text = ass_escape(clean_text)
:gsub("x(%d+)$", "{\\b1\\i1}x%1")
-- 颜色从十进制转为 BGR Hex
@@ -537,13 +663,11 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
local b = string.sub(color_hex, 5, 6)
local color_text = string.format("{\\c&H%s%s%s&}", b, g, r)
local start_time_str = seconds_to_time(appear_time)
local layer, end_time_str, style, effect
local style, effect
local pos, move = nil, nil
-- 滚动弹幕 (类型 1, 2, 3)
if danmaku_type >= 1 and danmaku_type <= 3 then
layer = 0
end_time_str = seconds_to_time(ev.end_time)
style = "R2L"
local text_length = get_str_width(text, fontsize)
local x1 = res_x + text_length / 2
@@ -551,105 +675,46 @@ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
local y = get_position_y(fontsize, appear_time, text_length, res_x, scrolltime, roll_array)
if y then
effect = string.format("{\\move(%d, %d, %d, %d)}", x1, y, x2, y)
move = {x1, y, x2, y}
end
-- 顶部弹幕 (类型 5)
elseif danmaku_type == 5 then
layer = 1
end_time_str = seconds_to_time(ev.end_time)
style = "TOP"
local x = res_x / 2
local y = get_fixed_y(fontsize, appear_time, fixtime, top_array, true)
if y then
effect = string.format("{\\pos(%d, %d)}", x, y)
pos = {x, y}
end
-- 底部弹幕 (类型 4)
elseif danmaku_type == 4 then
layer = 1
end_time_str = seconds_to_time(ev.end_time)
style = "BTM"
local x = res_x / 2
local y = get_fixed_y(fontsize, appear_time, fixtime, top_array, false)
if y then
effect = string.format("{\\pos(%d, %d)}", x, y)
pos = {x, y}
end
end
if style then
local line = nil
if effect then
line = string.format("Dialogue: %d,%s,%s,%s,,0,0,0,,%s%s%s", layer, start_time_str, end_time_str, style, effect, color_text, text)
else
line = string.format("Comment: %d,%s,%s,%s,,0,0,0,,%s%s", layer, start_time_str, end_time_str, style, color_text, text)
end
table.insert(ass_events, line)
if style and effect then
text = effect .. color_text .. text
local event = {
orig_time = ev.orig_time,
start_time = ev.start_time,
end_time = ev.end_time,
delay = ev.start_time - (ev.orig_time or ev.start_time),
style = style,
text = text,
clean_text = clean_text,
pos = pos,
move = move,
source = d.source,
}
table.insert(ass_events, event)
COMMENTS = ass_events
end
end
local final_ass = ass_header .. table.concat(ass_events, "\n")
local ass_file = io.open(danmaku_file, "w")
if not ass_file then
msg.info("错误: 无法写入 ASS 弹幕文件")
return false
end
ass_file:write(final_ass)
ass_file:close()
msg.debug("已成功转换并写入 ASS" .. danmaku_file)
return true
end
-- 将弹幕转换为 XML 格式
function convert_danmaku_to_xml(danmaku_input, danmaku_out, delays)
local all_danmaku = parse_danmaku_files(danmaku_input, delays)
if not all_danmaku then
show_message("转换 XML 弹幕失败", 3)
msg.info("转换 XML 弹幕失败")
return
end
-- 拼接为 XML 内容
local xml = { '<?xml version="1.0" encoding="UTF-8"?><i>\n' }
for _, d in ipairs(all_danmaku) do
local time = d.time
local type = d.type or 1
local size = d.size or 25
local color = d.color or 0xFFFFFF
local text = d.text or ""
text = text:gsub("&", "&amp;")
:gsub("<", "&lt;")
:gsub(">", "&gt;")
:gsub("\"", "&quot;")
:gsub("'", "&apos;")
table.insert(xml, string.format('<d p="%s,%s,%s,%s">%s</d>\n', time, type, size, color, text))
end
table.insert(xml, '</i>')
-- 写入 XML 文件
local file = io.open(danmaku_out, "w")
if not file then
show_message("无法写入目标 XML 文件", 3)
msg.info("无法写入目标 XML 文件: " .. danmaku_out)
return false
end
file:write(table.concat(xml))
file:close()
show_message("转换 XML 弹幕成功: " .. danmaku_out, 3)
msg.info("转换 XML 弹幕成功: " .. danmaku_out)
return true
end
-- 解析和转换弹幕
function convert_danmaku_format(danmaku_input, danmaku_file, delays)
local all_danmaku = parse_danmaku_files(danmaku_input, delays)
if all_danmaku then
convert_danmaku_to_ass(all_danmaku, danmaku_file)
else
msg.info("未能解析对应的 .xml 或 .json 弹幕文件")
return false
end
end
end
+89 -126
View File
@@ -1,25 +1,15 @@
-- modified from https://github.com/rkscv/danmaku/blob/main/danmaku.lua
local msg = require('mp.msg')
local utils = require("mp.utils")
local unpack = unpack or table.unpack
local INTERVAL = options.vf_fps and 0.01 or 0.001
local osd_width, osd_height, pause = 0, 0, true
local time_pos_observer_active = false
local overlay = mp.create_osd_overlay('ass-events')
-- 提取 \move 参数 (x1, y1, x2, y2) 并返回
local function parse_move_tag(text)
-- 匹配包括小数和负数在内的坐标值
local x1, y1, x2, y2 = text:match("\\move%((%-?[%d%.]+),%s*(%-?[%d%.]+),%s*(%-?[%d%.]+),%s*(%-?[%d%.]+).*%)")
if x1 and y1 and x2 and y2 then
return tonumber(x1), tonumber(y1), tonumber(x2), tonumber(y2)
end
return nil
end
local function parse_comment(event, pos, height, delay)
local x1, y1, x2, y2 = parse_move_tag(event.text)
local displayarea = tonumber(height * options.displayarea)
if not x1 then
local current_x, current_y = event.text:match("\\pos%((%-?[%d%.]+),%s*(%-?[%d%.]+).*%)")
local function realtime_position_text(event, pos, displayarea)
if not event.move then
local _, current_y = unpack(event.pos)
if not current_y or tonumber(current_y) > displayarea then return end
if event.style ~= "SP" and event.style ~= "MSG" then
return string.format("{\\an8}%s", event.text)
@@ -28,9 +18,10 @@ local function parse_comment(event, pos, height, delay)
end
end
local x1, y1, x2, y2 = unpack(event.move)
-- 计算移动的时间范围
local duration = event.end_time - event.start_time --mean: options.scrolltime
local progress = (pos - event.start_time - delay) / duration -- 移动进度 [0, 1]
local progress = (pos - event.start_time) / duration -- 移动进度 [0, 1]
-- 计算当前坐标
local current_x = tonumber(x1 + (x2 - x1) * progress)
@@ -46,60 +37,28 @@ local function parse_comment(event, pos, height, delay)
end
end
-- 从 ASS 文件中解析样式和事件
local function parse_ass_events(ass_path, callback)
local ass_file = io.open(ass_path, "r")
if not ass_file then
callback("无法打开 ASS 文件")
function render(pos_arg)
if COMMENTS == nil then return end
local pos, err
if pos_arg == nil then
pos, err = mp.get_property_number('time-pos')
if err ~= nil then
return msg.error(err)
end
else
pos = pos_arg
end
if not pos then
overlay:remove()
return
end
local events = {}
local time_tolerance = options.merge_tolerance
for line in ass_file:lines() do
if line:match("^Dialogue:") then
local start_time, end_time, style, text = line:match("Dialogue:%s*[^,]*,%s*([^,]*),%s*([^,]*),%s*([^,]*),[^,]*,[^,]*,[^,]*,[^,]*,[^,]*,(.*)")
if start_time and end_time and text then
local event = {
start_time = time_to_seconds(start_time),
end_time = time_to_seconds(end_time),
style = style,
text = text:gsub("%s+$", ""),
clean_text = text:gsub("\\h+", " "):gsub("{[\\=].-}", ""):gsub("^%s*(.-)%s*$", "%1"),
pos = text:match("\\pos"),
move = text:match("\\move"),
}
table.insert(events, event)
end
end
end
table.sort(events, function(a, b)
return a.start_time < b.start_time
end)
ass_file:close()
callback(nil, events)
end
local overlay = mp.create_osd_overlay('ass-events')
function render()
if COMMENTS == nil then return end
local pos, err = mp.get_property_number('time-pos')
if err ~= nil then
return msg.error(err)
end
local delay = get_delay_for_time(DELAYS, pos)
local fontname = options.fontname
local fontsize = options.fontsize
local alpha = string.format("%02X", (1 - tonumber(options.opacity)) * 255)
local opacity = tonumber(options.opacity)
local alpha = string.format("%02X", (1 - (opacity or 0)) * 255)
local width, height = 1920, 1080
local ratio = osd_width / osd_height
@@ -109,23 +68,37 @@ function render()
end
local ass_events = {}
local max_display = math.max(options.scrolltime, options.fixtime)
local window_start = pos - max_display
for _, event in ipairs(COMMENTS) do
if pos >= event.start_time + delay and pos <= event.end_time + delay then
local text = parse_comment(event, pos, height, delay)
-- 跳过已结束的弹幕
local lo = binary_search(COMMENTS, window_start, function(item) return item.start_time end)
local re_entity = "&#%d+;"
local re_fs = "\\fs(%d+)"
local ass_prefix = string.format("{\\rDefault\\fn%s\\fs%d\\c&HFFFFFF&\\alpha&H%s\\bord%s\\shad%s\\b%s\\q2}",
fontname, fontsize, alpha, options.outline, options.shadow, options.bold and "1" or "0")
for i = lo, #COMMENTS do
local event = COMMENTS[i]
if not event then break end
if event.start_time > pos then break end -- 后续弹幕提前退出
if event.end_time >= pos then
local text = realtime_position_text(event, pos, height * options.displayarea)
if text then
text = text:gsub("&#%d+;","")
text = text:gsub(re_entity, "")
end
if text and text:match("\\fs%d+") then
text = text:gsub("\\fs(%d+)", function(size)
return string.format("\\fs%d", size * 1.5)
if text and text:match(re_fs) then
text = text:gsub(re_fs, function(size)
local n = tonumber(size) or 0
return string.format("\\fs%d", math.floor(n * 1.5))
end)
end
-- 构建 ASS 字符串
local ass_text = text and string.format("{\\rDefault\\fn%s\\fs%d\\c&HFFFFFF&\\alpha&H%s\\bord%s\\shad%s\\b%s\\q2}%s",
fontname, fontsize, alpha, options.outline, options.shadow, options.bold and "1" or "0", text)
local ass_text = text and (ass_prefix .. text)
table.insert(ass_events, ass_text)
end
@@ -137,27 +110,39 @@ function render()
overlay:update()
end
local timer = mp.add_periodic_timer(INTERVAL, render, true)
local function time_pos_callback(_, time_pos)
if time_pos then
render(time_pos)
else
overlay:remove()
end
end
function parse_danmaku(ass_file_path, from_menu, no_osd)
parse_ass_events(ass_file_path, function(err, events)
COMMENTS = events
if err then
msg.error("ASS 解析错误: " .. err)
return
end
local function start_time_observer()
if not time_pos_observer_active then
mp.observe_property('time-pos', 'number', time_pos_callback)
time_pos_observer_active = true
end
end
if ENABLED and (from_menu or get_danmaku_visibility()) then
if not no_osd then
show_loaded(true)
end
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on")
show_danmaku_func()
else
show_message("")
hide_danmaku_func()
local function stop_time_observer()
if time_pos_observer_active then
mp.unobserve_property(time_pos_callback)
time_pos_observer_active = false
end
end
function render_danmaku(from_menu, no_osd)
if ENABLED and (from_menu or get_danmaku_visibility()) then
if not no_osd then
show_loaded(true)
end
end)
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on")
show_danmaku_func()
else
show_message("")
hide_danmaku_func()
end
end
local function filter_state(label, name)
@@ -172,9 +157,11 @@ local function filter_state(label, name)
end
function show_danmaku_func()
mp.set_property_bool(HAS_DANMAKU, true)
set_danmaku_visibility(true)
render()
if not pause then
timer:resume()
start_time_observer()
end
if options.vf_fps then
local display_fps = mp.get_property_number('display-fps')
@@ -189,7 +176,9 @@ function show_danmaku_func()
end
function hide_danmaku_func()
timer:kill()
stop_time_observer()
mp.set_property_bool(HAS_DANMAKU, false)
set_danmaku_visibility(false)
overlay:remove()
if filter_state("danmaku") then
mp.commandv("vf", "remove", "@danmaku")
@@ -221,33 +210,15 @@ end
mp.observe_property('osd-width', 'number', function(_, value) osd_width = value or osd_width end)
mp.observe_property('osd-height', 'number', function(_, value) osd_height = value or osd_height end)
mp.observe_property('display-fps', 'number', function(_, value)
if value ~= nil then
local interval = 1 / value / 10
if interval > INTERVAL then
timer:kill()
timer = mp.add_periodic_timer(interval, render, true)
if ENABLED then
timer:resume()
end
else
timer:kill()
timer = mp.add_periodic_timer(INTERVAL, render, true)
if ENABLED then
timer:resume()
end
end
end
end)
mp.observe_property('pause', 'bool', function(_, value)
if value ~= nil then
pause = value
end
if ENABLED then
if pause then
timer:kill()
stop_time_observer()
elseif COMMENTS ~= nil then
timer:resume()
start_time_observer()
end
end
end)
@@ -263,7 +234,7 @@ end)
mp.add_hook("on_unload", 50, function()
COMMENTS, DELAY = nil, 0
timer:kill()
stop_time_observer()
overlay:remove()
mp.set_property_native(DELAY_PROPERTY, 0)
if filter_state("danmaku") then
@@ -271,13 +242,10 @@ mp.add_hook("on_unload", 50, function()
end
local files_to_remove = {
file1 = utils.join_path(DANMAKU_PATH, "danmaku-" .. PID .. ".json"),
file2 = utils.join_path(DANMAKU_PATH, "danmaku-" .. PID .. ".ass"),
file3 = utils.join_path(DANMAKU_PATH, "temp-" .. PID .. ".mp4"),
file4 = utils.join_path(DANMAKU_PATH, "bahamut-" .. PID .. ".json")
file1 = utils.join_path(DANMAKU_PATH, "temp-" .. PID .. ".mp4"),
}
if options.save_danmaku and file_exists(files_to_remove.file2) then
if options.save_danmaku then
save_danmaku(true)
end
@@ -287,10 +255,5 @@ mp.add_hook("on_unload", 50, function()
end
end
for _, source in pairs(DANMAKU.sources) do
if source.fname and source.from and source.from ~= "user_local" and file_exists(source.fname) then
os.remove(source.fname)
end
end
DANMAKU = {sources = {}, count = 1}
end)
+126 -12
View File
@@ -2,8 +2,10 @@ local msg = require('mp.msg')
local utils = require("mp.utils")
local repo = "Tony15246/uosc_danmaku"
local zip_file = utils.join_path(os.getenv("TEMP") or "/tmp/", "uosc_danmaku.zip")
local local_version = VERSION or "0.0.0"
local platform = mp.get_property("platform")
local function version_greater(v1, v2)
local function parse(ver)
@@ -29,25 +31,137 @@ local function get_latest_release(repo)
})
if not res or res.status ~= 0 then return nil end
local tag = res.stdout:match([["tag_name"%s*:%s*"([^"]+)"]])
return tag
local zip_url = res.stdout:match([["browser_download_url"%s*:%s*"([^"]+%.zip)"]])
return tag, zip_url
end
local function escape_ps(str)
return tostring(str):gsub("'", "''")
end
local function unzip_overwrite(zip_file)
local outpath = mp.get_script_directory()
-- 定义临时目录路径,用于安全更新
local tmpdir = utils.join_path(
(platform == "windows" and (os.getenv("TEMP") or "C:\\Windows\\Temp") or "/tmp"),
"uosc_update_" .. tostring(os.time())
)
local cmd_unzip = {}
msg.info("创建临时目录并解压: " .. tmpdir)
if platform == "windows" then
-- PowerShell: Expand-Archive (会自动创建目标目录)
local ps_script = string.format(
"Expand-Archive -LiteralPath '%s' -DestinationPath '%s' -Force",
escape_ps(zip_file),
escape_ps(tmpdir)
)
cmd_unzip = { "powershell", "-NoProfile", "-Command", ps_script }
else
-- Unix: unzip
cmd_unzip = { "unzip", "-o", zip_file, "-d", tmpdir }
end
local res = mp.command_native({
name = "subprocess",
args = cmd_unzip,
capture_stdout = true,
capture_stderr = true,
playback_only = false,
})
if not res or res.status ~= 0 then
msg.error("❌ 解压失败:\n" .. (res and (res.stdout .. res.stderr) or "未知错误"))
-- 清理残留的临时目录
if platform == "windows" then
mp.command_native({
name = "subprocess",
args = {"powershell", "-NoProfile", "-Command", "Remove-Item -LiteralPath '"..escape_ps(tmpdir).."' -Recurse -Force"}
})
else
os.execute("rm -rf \"" .. tmpdir .. "\"")
end
return false
end
msg.info("解压成功,准备替换旧目录...")
local cmd_swap = {}
if platform == "windows" then
-- Windows: 在一个 PowerShell 实例中执行删除和移动
local ps_swap = string.format(
"Remove-Item -LiteralPath '%s' -Recurse -Force -ErrorAction SilentlyContinue; Move-Item -LiteralPath '%s' -Destination '%s' -Force",
escape_ps(outpath),
escape_ps(tmpdir),
escape_ps(outpath)
)
cmd_swap = { "powershell", "-NoProfile", "-Command", ps_swap }
else
-- Unix: rm && mv
cmd_swap = { "sh", "-c", string.format("rm -rf \"%s\" && mv \"%s\" \"%s\"", outpath, tmpdir, outpath) }
end
local res_swap = mp.command_native({
name = "subprocess",
args = cmd_swap,
capture_stdout = true,
capture_stderr = true,
playback_only = false,
})
if not res_swap or res_swap.status ~= 0 then
msg.error("❌ 替换目录失败:\n" .. (res_swap and (res_swap.stdout .. res_swap.stderr) or ""))
return false
end
msg.info("更新完成")
return true
end
-- 仅检查并提示新版本,不自动下载/覆盖(避免 rm -rf 破坏配置)
function check_for_update()
local latest_version = get_latest_release(repo)
if not latest_version then
show_message("无法获取最新版本信息")
msg.warn("无法获取最新版本信息")
local latest_version, download_url = get_latest_release(repo)
if not latest_version or not download_url then
show_message("无法获取最新版本信息")
msg.warn("无法获取最新版本信息")
return
end
if not version_greater(latest_version, local_version) then
show_message("uosc_danmaku 已是最新版本 (" .. local_version .. ")")
msg.info("uosc_danmaku 已是最新版本 (" .. local_version .. ")")
show_message(" 已是最新版本 ("..local_version..")")
msg.info("✅ 已是最新版本")
return
end
local update_url = "https://github.com/" .. repo .. "/releases/tag/" .. latest_version
show_message("uosc_danmaku 有新版本: " .. latest_version .. " (当前: " .. local_version .. ")\n请手动更新: " .. update_url)
msg.info("uosc_danmaku 新版本: " .. latest_version .. " 下载地址: " .. update_url)
end
show_message("⬇️ 发现新版本: " .. latest_version .. ",正在下载...")
msg.info("⬇️ 发现新版本: " .. latest_version .. ",地址: " .. download_url)
local cmd = { "curl", "-L", "-o", zip_file, download_url }
local res = mp.command_native({
name = "subprocess",
args = cmd,
capture_stdout = true,
capture_stderr = true,
playback_only = false,
})
if not res or res.status ~= 0 then
show_message("❌ 下载失败!")
msg.warn("❌ 下载失败!")
return
end
show_message("📦 下载完成,开始解压覆盖...")
msg.info("📦 下载完成,开始解压覆盖...")
if unzip_overwrite(zip_file) then
os.remove(zip_file)
show_message("✅ 更新成功!请重启 mpv 以应用更新,当前版本为:" .. latest_version)
msg.info("✅ 更新成功,当前版本为:" .. latest_version)
else
os.remove(zip_file)
show_message("❌ 解压失败!请查看控制台日志")
msg.warn("❌ 解压失败!")
end
end
+127 -36
View File
@@ -1,4 +1,5 @@
local utils = require("mp.utils")
local unpack = unpack or table.unpack
-- from http://lua-users.org/wiki/LuaUnicode
local UTF8_PATTERN = '[%z\1-\127\194-\244][\128-\191]*'
@@ -210,6 +211,31 @@ function hex_to_char(x)
return string.char(tonumber(x, 16))
end
function hex_to_int_color(hex_color)
-- 移除颜色代码中的'#'字符
hex_color = hex_color:sub(2) -- 只保留颜色代码部分
-- 提取R, G, B的十六进制值并转为整数
local r = tonumber(hex_color:sub(1, 2), 16)
local g = tonumber(hex_color:sub(3, 4), 16)
local b = tonumber(hex_color:sub(5, 6), 16)
-- 计算32位整数值
local color_int = (r * 256 * 256) + (g * 256) + b
return color_int
end
local function get_type_from_position(position)
if position == 0 then
return 1
end
if position == 1 then
return 4
end
return 5
end
-- url编码转换
function url_encode(str)
-- 将非安全字符转换为百分号编码
@@ -318,6 +344,67 @@ function file_exists(path)
return false
end
function binary_search(tbl, target, key)
if not tbl or #tbl == 0 then return 1 end
key = key or function(x) return x end
local lo, hi = 1, #tbl
local res = #tbl + 1
while lo <= hi do
local mid = math.floor((lo + hi) / 2)
local v = tbl[mid]
local val = key(v)
if val >= target then
res = mid
hi = mid - 1
else
lo = mid + 1
end
end
return res
end
function new_min_heap()
local h = {}
local function swap(i, j)
h[i], h[j] = h[j], h[i]
end
local function up(i)
while i > 1 do
local p = math.floor(i/2)
if h[p].time <= h[i].time then break end
swap(p, i)
i = p
end
end
local function down(i)
local n = #h
while true do
local l = i * 2
local r = l + 1
local smallest = i
if l <= n and h[l].time < h[smallest].time then smallest = l end
if r <= n and h[r].time < h[smallest].time then smallest = r end
if smallest == i then break end
swap(i, smallest)
i = smallest
end
end
local function push(node)
h[#h + 1] = node
up(#h)
end
local function pop()
if #h == 0 then return nil end
local root = h[1]
if #h == 1 then h[1] = nil; return root end
h[1] = h[#h]
h[#h] = nil
down(1)
return root
end
return { push = push, pop = pop, size = function() return #h end }
end
function is_writable(path)
local file = io.open(path, "w")
if file then
@@ -386,10 +473,19 @@ local function split_by_numbers(filename)
return parts
end
-- 识别匹配前后剧集
local function compare_filenames(fname1, fname2)
-- 识别匹配前后剧集并提取集数
local function get_series_episodes(fname1, fname2)
local parts1 = split_by_numbers(fname1)
local parts2 = split_by_numbers(fname2)
local title1 = format_filename(fname1)
local title2 = format_filename(fname2)
if title1 and title2 then
local media_title1, season1, episode1 = title1:match("^(.-)%s*[sS](%d+)[eE](%d+)")
local media_title2, season2, episode2 = title2:match("^(.-)%s*[sS](%d+)[eE](%d+)")
if season1 and season2 and season1 ~= season2 then
return nil, nil
end
end
local min_len = math.min(#parts1, #parts2)
@@ -400,7 +496,7 @@ local function compare_filenames(fname1, fname2)
-- 比较数字前的字符是否相同
if part1.pre ~= part2.pre then
return false
return nil, nil
end
-- 比较数字部分
@@ -410,11 +506,36 @@ local function compare_filenames(fname1, fname2)
-- 比较数字后的字符是否相同
if part1.post ~= part2.post then
return false
return nil, nil
end
end
return false
return nil, nil
end
-- 获取当前文件名所包含的集数
function get_episode_number(filename, fname)
-- 尝试对比记录文件名来获取当前集数
if fname then
return get_series_episodes(fname, filename)
end
local thin_space = string.char(0xE2, 0x80, 0x89)
filename = filename:gsub(thin_space, " ")
local title = format_filename(filename)
if title then
local media_title, season, episode = title:match("^(.-)%s*[sS](%d+)[eE](%d+)")
if season then
return tonumber(episode)
else
local media_title, episode = title:match("^(.-)%s*[eE](%d+)")
if episode then
return tonumber(episode)
end
end
end
return nil
end
-- 规范化路径
@@ -520,36 +641,6 @@ function parse_title()
return title_replace(title), season, episode
end
-- 获取当前文件名所包含的集数
function get_episode_number(filename, fname)
-- 尝试对比记录文件名来获取当前集数
if fname then
local episode_num1, episode_num2 = compare_filenames(fname, filename)
if episode_num1 and episode_num2 then
return episode_num1, episode_num2
else
return nil, nil
end
end
local thin_space = string.char(0xE2, 0x80, 0x89)
filename = filename:gsub(thin_space, " ")
local title = format_filename(filename)
if title then
local media_title, season, episode = title:match("^(.-)%s*[sS](%d+)[eE](%d+)")
if season then
return tonumber(episode)
else
local media_title, episode = title:match("^(.-)%s*[eE](%d+)")
if episode then
return tonumber(episode)
end
end
end
return nil
end
local CHINESE_NUM_MAP = {
[""] = 0, [""] = 1, [""] = 2, [""] = 3, [""] = 4,
[""] = 5, [""] = 6, [""] = 7, [""] = 8, [""] = 9,
@@ -644,7 +735,7 @@ function call_cmd_async(args, callback)
name = 'subprocess',
capture_stderr = true,
capture_stdout = true,
playback_only = false,
playback_only = true,
args = args,
}, function(success, result, error)
if not success or not result or result.status ~= 0 then