This commit is contained in:
2026-03-27 07:06:16 +01:00
commit 1541961403
340 changed files with 151916 additions and 0 deletions
+929
View File
@@ -0,0 +1,929 @@
VERSION = "2.0.0"
mp.commandv('script-message', 'uosc_danmaku-version', VERSION)
local msg = require('mp.msg')
local utils = require("mp.utils")
AES = require("modules/aes")
Base64 = require("modules/base64")
MD5 = require("modules/md5")
Sha256 = require("modules/hash")
require("modules/options")
require("modules/utils")
require("modules/parse")
require("modules/guess")
require('modules/render')
require('modules/menu')
require("modules/update")
require("apis/dandanplay")
require('apis/extra')
DANMAKU_PATH = os.getenv("TEMP") or "/tmp/"
HISTORY_PATH = mp.command_native({"expand-path", options.history_path})
PID = utils.getpid()
DANMAKU = {sources = {}, count = 1}
DELAYS = {}
ENABLED, COMMENTS, DELAY = false, nil, 0
DELAY_PROPERTY = string.format("user-data/%s/danmaku-delay", mp.get_script_name())
mp.set_property_native(DELAY_PROPERTY, 0)
KEY = table_to_zero_indexed({
0x00,0x01,0x02,0x03,0x04,
0x05,0x06,0x07,0x08,0x09,
0x0a,0x0b,0x0c,0x0d,0x0e,
0x0f,0x10,0x11,0x12,0x13,
0x14,0x15,0x16,0x17,0x18,
0x19,0x1a,0x1b,0x1c,0x1d,
0x1e,0x1f
})
PLATFORM = (function()
local platform = mp.get_property_native("platform")
if platform then
if itable_index_of({ "windows", "darwin" }, platform) then
return platform
end
else
if os.getenv("windir") ~= nil then
return "windows"
end
local homedir = os.getenv("HOME")
if homedir ~= nil and string.sub(homedir, 1, 6) == "/Users" then
return "darwin"
end
end
return "linux"
end)()
function get_danmaku_visibility()
local history_json = read_file(HISTORY_PATH)
local history
if history_json ~= nil then
history = utils.parse_json(history_json) or {}
local flag = history["show_danmaku"]
if flag == nil then
history["show_danmaku"] = false
write_json_file(HISTORY_PATH, history)
else
return flag
end
else
history = {}
history["show_danmaku"] = false
write_json_file(HISTORY_PATH, history)
end
return false
end
function set_danmaku_visibility(flag)
local history = {}
local history_json = read_file(HISTORY_PATH)
if history_json ~= nil then
history = utils.parse_json(history_json) or {}
end
history["show_danmaku"] = flag
write_json_file(HISTORY_PATH, history)
end
function set_danmaku_button()
if get_danmaku_visibility() then
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on")
end
end
function show_loaded(init)
if DANMAKU.anime and DANMAKU.episode then
show_message("匹配内容:" .. DANMAKU.anime .. "-" .. DANMAKU.episode .. "\\N弹幕加载成功共计" .. #COMMENTS .. "条弹幕", 3)
if init then
msg.info(DANMAKU.anime .. "-" .. DANMAKU.episode .. " 弹幕加载成功,共计" .. #COMMENTS .. "条弹幕")
end
else
show_message("弹幕加载成功,共计" .. #COMMENTS .. "条弹幕", 3)
end
end
local function get_cid()
local cid, danmaku_id = nil, nil
local tracks = mp.get_property_native("track-list")
for _, track in ipairs(tracks) do
if track["lang"] == "danmaku" then
cid = track["external-filename"]:match("/(%d-)%.xml$")
danmaku_id = track["id"]
break
end
end
return cid, danmaku_id
end
local function extract_between_colons(input_string)
local start_index = 0
local end_index = 0
local count = 0
for i = 1, #input_string do
if input_string:sub(i, i) == ":" then
count = count + 1
if count == 2 then
start_index = i
elseif count == 3 then
end_index = i
break
end
end
end
if start_index > 0 and end_index > 0 then
return input_string:sub(start_index + 1, end_index - 1)
else
return nil
end
end
local 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
-- 获取指定时间的延迟
-- 返回该时间点之前所有延迟段的总和
function get_delay_for_time(delay_segments, time)
if not delay_segments or #delay_segments == 0 then return 0 end
table.sort(delay_segments, function(a, b) return a.start < b.start end)
local applied_delay = 0
for i = 1, #delay_segments do
local seg = delay_segments[i]
local delay = tonumber(seg.delay)
if time >= seg.start and delay then
applied_delay = applied_delay + delay
else
break
end
end
return applied_delay
end
local function merge_delay_segments(segments)
if not segments or #segments == 0 then return {} end
local NEAREST_THRESHOLD = 10 -- 最邻近段合并阈值
local MERGE_THRESHOLD = 30 -- 跨段合并阈值
local EPSILON = 1e-6 -- 判断接近 0 的阈值
table.sort(segments, function(a, b) return a.start < b.start end)
local partially_merged = {}
local i = 1
while i <= #segments do
local cur = segments[i]
local next_seg = segments[i + 1]
if next_seg and (next_seg.start - cur.start) <= NEAREST_THRESHOLD then
local combined_delay = tonumber(cur.delay) + tonumber(next_seg.delay)
if math.abs(combined_delay) > EPSILON then
table.insert(partially_merged, {
start = cur.start,
delay = combined_delay
})
end
i = i + 2
else
if math.abs(tonumber(cur.delay)) > EPSILON then
table.insert(partially_merged, cur)
end
i = i + 1
end
end
local merged = {}
for _, seg in ipairs(partially_merged) do
local merged_flag = false
for idx, m in ipairs(merged) do
if math.abs(seg.start - m.start) <= MERGE_THRESHOLD then
m.delay = tonumber(m.delay) + tonumber(seg.delay)
if math.abs(m.delay) <= EPSILON then
table.remove(merged, idx)
end
merged_flag = true
break
end
end
if not merged_flag then
if math.abs(tonumber(seg.delay)) > EPSILON then
table.insert(merged, {
start = seg.start,
delay = seg.delay
})
end
end
end
table.sort(merged, function(a, b) return a.start < b.start end)
return merged
end
local function set_danmaku_delay(dly, time)
for url, source in pairs(DANMAKU.sources) do
if source.fname and not source.blocked then
source.delay_segments = source.delay_segments or {}
if dly == 0 then
source.delay_segments = {}
elseif time then
table.insert(source.delay_segments, {start = time, delay = dly})
else
table.insert(source.delay_segments, {start = 0, delay = dly})
end
source.delay = nil
table.sort(source.delay_segments, function(a, b) return a.start < b.start end)
add_source_to_history(url, source)
end
end
if time then
table.insert(DELAYS, {start = time, delay = dly})
else
table.insert(DELAYS, {start = 0, delay = dly})
end
if dly == 0 then
DELAY = 0
DELAYS = {}
else
DELAY = DELAY + dly
end
DELAYS = merge_delay_segments(DELAYS)
if ENABLED and COMMENTS ~= nil then
render()
end
show_message('设置弹幕延迟: ' .. string.format("%.1f", DELAY + 1e-10) .. ' s')
mp.set_property_native(DELAY_PROPERTY, DELAY)
end
local function clear_source()
local path = mp.get_property("path")
local history_json = read_file(HISTORY_PATH)
if not path or not history_json then return end
local history = utils.parse_json(history_json) or {}
if history[path] == nil then return end
history[path] = nil
write_json_file(HISTORY_PATH, history)
for url, source in pairs(DANMAKU.sources) do
if source.from == "user_custom" then
if source.fname and file_exists(source.fname) then
os.remove(source.fname)
end
DANMAKU.sources[url] = nil
end
end
load_danmaku(false)
show_message("已重置当前视频所有弹幕源更改", 3)
msg.verbose("已重置当前视频所有弹幕源更改")
end
function write_history(episodeid)
local history = {}
local path = mp.get_property("path")
local dir = get_parent_directory(path)
local fname = mp.get_property('filename/no-ext')
local episodeNumber = 0
if episodeid then
episodeNumber = tonumber(episodeid) % 1000
elseif DANMAKU.extra then
episodeNumber = DANMAKU.extra.episodenum
end
if is_protocol(path) then
local title, season_num, episod_num = parse_title()
if title and episod_num then
if season_num then
dir = title .." Season".. season_num
else
dir = title
end
fname = url_decode(mp.get_property("media-title"))
episodeNumber = episod_num
end
end
if dir ~= nil then
local history_json = read_file(HISTORY_PATH)
if history_json ~= nil then
history = utils.parse_json(history_json) or {}
end
history[dir] = {}
history[dir].fname = fname
history[dir].source = DANMAKU.source
history[dir].animeTitle = DANMAKU.anime
history[dir].episodeTitle = DANMAKU.episode
history[dir].episodeNumber = episodeNumber
if episodeid then
history[dir].episodeId = episodeid
elseif DANMAKU.extra then
history[dir].extra = DANMAKU.extra
end
write_json_file(HISTORY_PATH, history)
end
end
function remove_source_from_history(rm_source)
local history_json = read_file(HISTORY_PATH)
local path = mp.get_property("path")
if is_protocol(path) then
path = remove_query(path)
end
if history_json then
local history = utils.parse_json(history_json) or {}
if history[path] ~= nil and history[path]["sources"] ~= nil then
for source in pairs(history[path]["sources"]) do
if source == rm_source then
history[path]["sources"][source] = nil
break
end
end
end
write_json_file(HISTORY_PATH, history)
end
end
function add_source_to_history(add_url, add_source)
local history_json = read_file(HISTORY_PATH)
local path = mp.get_property("path")
if is_protocol(path) then
path = remove_query(path)
end
local history = {}
if history_json then
history = utils.parse_json(history_json) or {}
end
history[path] = history[path] or {}
history[path]["sources"] = history[path]["sources"] or {}
history[path]["sources"][add_url] = history[path]["sources"][add_url] or {}
local record = history[path]["sources"][add_url]
record.from = add_source.from or "user_custom"
record.blocked = add_source.blocked or false
local delay_segments = shallow_copy(add_source.delay_segments or {})
if #delay_segments > 0 then
record.delay_segments = merge_delay_segments(delay_segments)
if #record.delay_segments == 0 then
record.delay_segments = nil
end
else
record.delay_segments = nil
end
record.delay = nil
write_json_file(HISTORY_PATH, history)
end
function read_danmaku_source_record(path)
if is_protocol(path) then
path = remove_query(path)
end
local history_json = read_file(HISTORY_PATH)
if not history_json then return end
local history = utils.parse_json(history_json) or {}
local record = history[path]
if not record or not record.sources then return end
local sources = record.sources
local upgraded_sources = {}
if is_nested_table(sources) then
for source, data in pairs(sources) do
local from = data.from or "user_custom"
local blocked = data.blocked or false
local delay_segments = shallow_copy(data.delay_segments or {})
if data.delay ~= nil then
for i = #delay_segments, 1, -1 do
if delay_segments[i].start == 0 then
table.remove(delay_segments, i)
end
end
table.insert(delay_segments, 1, { start = 0, delay = tonumber(data.delay) })
end
if #delay_segments > 0 then
delay_segments = merge_delay_segments(delay_segments)
else
delay_segments = nil
end
DANMAKU.sources[source] = {
from = from,
blocked = blocked,
delay_segments = delay_segments,
from_history = true,
}
end
else
for _, raw in ipairs(sources) do
local source = raw
local blocked = false
local from = raw:match("<(.-)>")
local delay = raw:match("{{(.-)}}")
source = source:gsub("<.->", ""):gsub("{{.-}}", "")
if source:match("^%-") then
source = source:sub(2)
blocked = true
from = from or "api_server"
end
local delay_segments = nil
if delay ~= nil then
delay_segments = {
{ start = 0, delay = tonumber(delay) }
}
end
DANMAKU.sources[source] = {
from = from or "user_custom",
blocked = blocked,
delay_segments = delay_segments,
from_history = true,
}
upgraded_sources[source] = shallow_copy(DANMAKU.sources[source])
end
if next(upgraded_sources) then
record.sources = upgraded_sources
write_json_file(HISTORY_PATH, history)
end
end
end
-- 收集现有的弹幕文件和延迟记录
local function collect_danmaku_sources()
local danmaku_input = {}
local delays = {}
for _, source in pairs(DANMAKU.sources) do
if not source.blocked and source.fname then
if not file_exists(source.fname) then
show_message("未找到弹幕文件", 3)
msg.info("未找到弹幕文件")
return
end
table.insert(danmaku_input, source.fname)
if source.delay_segments and #source.delay_segments > 0 then
table.insert(delays, source.delay_segments)
end
end
end
return danmaku_input, delays
end
-- 视频播放时保存弹幕
function save_danmaku(not_forced)
local danmaku_input, delays = collect_danmaku_sources()
if #danmaku_input == 0 then
show_message("弹幕内容为空,无法保存", 3)
msg.verbose("弹幕内容为空,无法保存")
COMMENTS = {}
return
end
local path = mp.get_property("path")
local dir = get_parent_directory(path) or ""
local filename = mp.get_property('filename/no-ext')
local danmaku_out = utils.join_path(dir, filename .. ".xml")
-- 排除网络播放场景
if not path or is_protocol(path) or (not file_exists(danmaku_out)
and not is_writable(danmaku_out)) then
show_message("此弹幕文件不支持保存至本地")
msg.warn("此弹幕文件不支持保存至本地")
else
if not_forced and file_exists(danmaku_out) then
show_message("已存在同名弹幕文件:" .. danmaku_out)
msg.info("已存在同名弹幕文件:" .. danmaku_out)
return
else
convert_danmaku_to_xml(danmaku_input, danmaku_out, delays)
end
end
end
-- 加载弹幕
function load_danmaku(from_menu, no_osd)
if not ENABLED then return end
local temp_file = "danmaku-" .. PID .. ".ass"
local danmaku_file = utils.join_path(DANMAKU_PATH, temp_file)
local danmaku_input, delays = collect_danmaku_sources()
-- 如果没有弹幕文件,退出加载
if #danmaku_input == 0 then
show_message("该集弹幕内容为空,结束加载", 3)
msg.verbose("该集弹幕内容为空,结束加载")
COMMENTS = {}
return
end
convert_danmaku_format(danmaku_input, danmaku_file, delays)
parse_danmaku(danmaku_file, from_menu, no_osd)
end
-- 为 bilibli 网站的视频播放加载弹幕
function load_danmaku_for_bilibili(path)
local cid, danmaku_id = get_cid()
if danmaku_id ~= nil then
mp.commandv('sub-remove', danmaku_id)
end
if cid == nil then
cid = mp.get_opt('cid')
if not cid then
local patterns = {
"bilivideo%.c[nom]+.*/resource/(%d+)%D+.*",
"bilivideo%.c[nom]+.*/(%d+)-%d+-%d+%..*%?",
}
local urls = {
path,
mp.get_property("stream-open-filename", ''),
}
for _, pattern in ipairs(patterns) do
for _, url in ipairs(urls) do
if url:find(pattern) then
cid = url:match(pattern)
break
end
end
end
end
end
if cid == nil and path:match("/video/BV.-") then
if path:match("video/BV.-/.*") then
path = path:gsub("/[^/]+$", "")
end
add_danmaku_source_online(path, true)
return
end
if cid ~= nil then
local url = "https://comment.bilibili.com/" .. cid .. ".xml"
local temp_file = "danmaku-" .. PID .. DANMAKU.count .. ".xml"
local danmaku_xml = utils.join_path(DANMAKU_PATH, temp_file)
DANMAKU.count = DANMAKU.count + 1
local arg = {
"curl",
"-L",
"-s",
"--compressed",
"--user-agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
"--output",
danmaku_xml,
url,
}
call_cmd_async(arg, function(error)
async_running = false
if error then
show_message("HTTP 请求失败,打开控制台查看详情", 5)
msg.error(error)
return
end
if file_exists(danmaku_xml) then
save_danmaku_downloaded(path, danmaku_xml)
load_danmaku(true)
end
end)
end
end
-- 为 bahamut 网站的视频播放加载弹幕
function load_danmaku_for_bahamut(path)
local path = path:gsub('%%(%x%x)', hex_to_char)
local sn = extract_between_colons(path)
if sn == nil then
return
end
local url = "https://ani.gamer.com.tw/ajax/danmuGet.php"
local temp_file = "bahamut-" .. PID .. ".json"
local danmaku_json = utils.join_path(DANMAKU_PATH, temp_file)
local arg = {
"curl",
"-X",
"POST",
"-d",
"sn=" .. sn,
"-L",
"-s",
"--user-agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36",
"--header",
"Origin: https://ani.gamer.com.tw",
"--header",
"Content-Type: application/x-www-form-urlencoded;charset=utf-8",
"--header",
"Accept: application/json",
"--header",
"Authority: ani.gamer.com.tw",
"--output",
danmaku_json,
url,
}
if options.proxy ~= "" then
table.insert(arg, '-x')
table.insert(arg, options.proxy)
end
call_cmd_async(arg, function(error)
async_running = false
if error then
show_message("HTTP 请求失败,打开控制台查看详情", 5)
msg.error(error)
return
end
if not file_exists(danmaku_json) then
url = "https://ani.gamer.com.tw/animeVideo.php?sn=" .. sn
ENABLED = true
add_danmaku_source_online(url, true)
return
end
local comments_json = read_file(danmaku_json)
local comments = utils.parse_json(comments_json)
if not comments then
return
end
temp_file = "danmaku-" .. PID .. DANMAKU.count .. ".json"
local json_filename = utils.join_path(DANMAKU_PATH, temp_file)
DANMAKU.count = DANMAKU.count + 1
local json_file = io.open(json_filename, "w")
if json_file then
json_file:write("[\n")
for _, comment in ipairs(comments) do
local m = comment["text"]
local color = hex_to_int_color(comment["color"])
local mode = get_type_from_position(comment["position"])
local time = tonumber(comment["time"]) / 10
local c = time .. "," .. color .. "," .. mode .. ",25,,,"
-- Write the JSON object as a single line, no spaces or extra formatting
local json_entry = string.format('{"c":"%s","m":"%s"},\n', c, m)
json_file:write(json_entry)
end
json_file:write("]")
json_file:close()
end
if file_exists(json_filename) then
save_danmaku_downloaded(
"https://ani.gamer.com.tw/animeVideo.php?sn=" .. sn,
json_filename)
load_danmaku(true)
end
end)
end
function load_danmaku_for_url(path)
if path:find('bilibili.com') or path:find('bilivideo.c[nom]+') then
load_danmaku_for_bilibili(path)
return
end
if path:find('bahamut.akamaized.net') then
load_danmaku_for_bahamut(path)
return
end
local title, season_num, episod_num = parse_title()
local filename = url_decode(mp.get_property("media-title"))
local episod_number = nil
if title and episod_num then
if season_num then
dir = title .." Season".. season_num
episod_number = episod_num
else
dir = title
end
auto_load_danmaku(path, dir, filename, episod_number)
addon_danmaku(dir, false)
return
end
get_danmaku_with_hash(filename, path)
addon_danmaku()
end
-- 自动加载上次匹配的弹幕
function auto_load_danmaku(path, dir, filename, number)
if dir ~= nil then
local history_json = read_file(HISTORY_PATH)
if history_json ~= nil then
local history = utils.parse_json(history_json) or {}
-- 1.判断父文件名是否存在
local history_dir = history[dir]
if history_dir ~= nil then
--2.如果存在则获取number和id
DANMAKU.anime = history_dir.animeTitle
local episode_number = history_dir.episodeTitle and history_dir.episodeTitle:match("%d+")
local history_number = history_dir.episodeNumber
local history_id = history_dir.episodeId
local history_fname = history_dir.fname
local history_extra = history_dir.extra
local playing_number = nil
if history_fname then
if filename ~= history_fname then
if number then
playing_number = number
else
history_number, playing_number = get_episode_number(filename, history_fname)
end
else
playing_number = history_number
end
else
playing_number = get_episode_number(filename)
end
if playing_number ~= nil then
local x = playing_number - history_number --获取集数差值
DANMAKU.episode = episode_number and string.format("第%s话", episode_number + x) or history_dir.episodeTitle
show_message("自动加载上次匹配的弹幕", 3)
msg.verbose("自动加载上次匹配的弹幕")
if history_id then
local tmp_id = tostring(x + history_id)
set_episode_id(tmp_id)
elseif history_extra then
local episodenum = history_extra.episodenum + x
get_details(history_extra.class, history_extra.id, history_extra.site,
history_extra.title, history_extra.year, history_extra.number, episodenum)
end
else
get_danmaku_with_hash(filename, path)
end
else
get_danmaku_with_hash(filename, path)
end
else
get_danmaku_with_hash(filename, path)
end
end
end
function init(path)
if not path then return end
local dir = get_parent_directory(path)
local filename = mp.get_property('filename/no-ext')
local video = mp.get_property_native("current-tracks/video")
local duration = mp.get_property_number("duration", 0)
if not video or video["image"] or video["albumart"] or duration < 60 then
msg.info("不支持的播放内容(非视频)")
return
end
if is_protocol(path) then
load_danmaku_for_url(path)
end
if dir and filename then
local danmaku_xml = utils.join_path(dir, filename .. ".xml")
if file_exists(danmaku_xml) then
add_danmaku_source_local(danmaku_xml, true)
else
auto_load_danmaku(path, dir, filename)
addon_danmaku(dir, true)
end
end
end
mp.register_event("file-loaded", function()
local path = mp.get_property("path")
local dir = get_parent_directory(path)
local filename = mp.get_property('filename/no-ext')
local video = mp.get_property_native("current-tracks/video")
local fps = mp.get_property_number("container-fps", 0)
local duration = mp.get_property_number("duration", 0)
if not video or video["image"] or video["albumart"] or fps < 23 or duration < 60 then
return
end
read_danmaku_source_record(path)
if not get_danmaku_visibility() then
return
end
if options.autoload_for_url and is_protocol(path) then
ENABLED = true
load_danmaku_for_url(path)
end
if filename == nil or dir == nil then
return
end
local danmaku_xml = utils.join_path(dir, filename .. ".xml")
if options.autoload_local_danmaku then
if file_exists(danmaku_xml) then
ENABLED = true
add_danmaku_source_local(danmaku_xml)
return
end
end
if options.auto_load then
ENABLED = true
auto_load_danmaku(path, dir, filename)
addon_danmaku(dir, false)
return
end
if ENABLED and COMMENTS == nil and not async_running then
init(path)
end
end)
-------------- 键位绑定 --------------
mp.add_key_binding(options.open_search_danmaku_menu_key, "open_search_danmaku_menu", function()
mp.commandv("script-message", "open_search_danmaku_menu")
end)
mp.add_key_binding(options.show_danmaku_keyboard_key, "show_danmaku_keyboard", function()
mp.commandv("script-message", "show_danmaku_keyboard")
end)
mp.register_script_message("danmaku-delay", function(...)
local commands = {...}
local delay_str, time_str = commands[1], commands[2]
local dly = tonumber(delay_str)
local time = time_str and tonumber(time_str)
if type(dly) ~= "number" then
show_message("参数错误:缺少有效的延迟秒数", 3)
return
end
set_danmaku_delay(dly, time)
end)
mp.register_script_message("show_danmaku_keyboard", function()
ENABLED = not ENABLED
if ENABLED then
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on")
set_danmaku_visibility(true)
if COMMENTS == nil then
show_message("加载弹幕初始化...", 3)
local path = mp.get_property("path")
init(path)
else
show_loaded()
show_danmaku_func()
end
else
show_message("关闭弹幕", 2)
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "off")
set_danmaku_visibility(false)
hide_danmaku_func()
end
end)
mp.register_script_message("check-update", check_for_update)
mp.register_script_message("clear-source", clear_source)
mp.register_script_message("immediately_save_danmaku", save_danmaku)
mp.register_script_message("open_source_delay_menu", danmaku_delay_setup)
mp.register_script_message("open_search_danmaku_menu", open_input_menu)
mp.register_script_message("open_add_source_menu", open_add_menu)
mp.register_script_message("open_add_total_menu", open_add_total_menu)