This commit is contained in:
2026-03-27 07:06:16 +01:00
commit 1541961403
340 changed files with 151916 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 吴南李
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
File diff suppressed because it is too large Load Diff
+735
View File
@@ -0,0 +1,735 @@
local msg = require('mp.msg')
local utils = require("mp.utils")
local function extract_url(url)
local path = url:match("^https?://[^/]+(/[^%?]*)")
return path
end
local function generateXSignature(url, time, appid, app_accept)
local url_path = extract_url(url)
if not url_path then
return nil
end
local dataToHash = string.format("%s%d%s%s", AES.ECB.decrypt(KEY, Base64.decode(appid)),
time, url_path, AES.ECB.decrypt(KEY, Base64.decode(app_accept)))
local hash = Sha256(dataToHash)
local base64Hash = Base64.encode(hex_to_bin(hash))
return base64Hash
end
-- 写入history.json
-- 读取episodeId获取danmaku
function set_episode_id(input, from_menu)
from_menu = from_menu or false
DANMAKU.source = "dandanplay"
for url, source in pairs(DANMAKU.sources) do
if source.from == "api_server" then
if source.fname and file_exists(source.fname) then
os.remove(source.fname)
end
if not source.from_history then
DANMAKU.sources[url] = nil
else
DANMAKU.sources[url]["fname"] = nil
end
end
end
local episodeId = tonumber(input)
write_history(episodeId)
set_danmaku_button()
if options.load_more_danmaku then
fetch_danmaku_all(episodeId, from_menu)
else
fetch_danmaku(episodeId, from_menu)
end
end
-- 回退使用额外的弹幕获取方式
function get_danmaku_fallback(query)
local url = options.fallback_server .. "/?url=" .. query
msg.verbose("尝试获取弹幕:" .. url)
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
if query:find("iqiyi%.com") ~= nil then
DANMAKU.strict = true
end
save_danmaku_downloaded(query, danmaku_xml)
load_danmaku(true)
end
end)
end
-- 返回弹幕请求参数
function make_danmaku_request_args(method, url, headers, body)
local args = {
"curl",
"-L",
"-X",
method,
"-H",
"Accept: application/json",
"-H",
"User-Agent: " .. options.user_agent,
}
if headers then
for k, v in pairs(headers) do
table.insert(args, '-H')
table.insert(args, string.format('%s: %s', k, v))
end
end
if body then
table.insert(args, '-d')
table.insert(args, utils.format_json(body))
table.insert(args, '-H')
table.insert(args, 'Content-Type: application/json')
end
if url:find("api%.dandanplay%.") then
local time = os.time()
local appid = "UgjRIH45lE1BBLNmir1WKw=="
local app_accept = "SzuWlFZAPRMqeWf9qmfp8dcvYr3hvxuSrIRZuAeEfko="
table.insert(args, '-H')
table.insert(args, string.format('X-AppId: %s', AES.ECB.decrypt(KEY, Base64.decode(appid))))
table.insert(args, '-H')
table.insert(args, string.format('X-Signature: %s', generateXSignature(url, time, appid, app_accept)))
table.insert(args, '-H')
table.insert(args, string.format('X-Timestamp: %s', time))
end
table.insert(args, url)
return args
end
-- 尝试通过解析文件名匹配剧集
local function match_episode(animeTitle, bangumiId, episode_num)
local url = options.api_server .. "/api/v2/bangumi/" .. bangumiId
local args = make_danmaku_request_args("GET", url)
if args == nil then
return
end
call_cmd_async(args, function(error, json)
async_running = false
if error then
show_message("HTTP 请求失败,打开控制台查看详情", 5)
msg.error(error)
return
end
local data = utils.parse_json(json)
if not data or not data.bangumi or not data.bangumi.episodes then
msg.info("无结果")
return
end
for _, episode in ipairs(data.bangumi.episodes) do
local ep_num = tonumber(episode.episodeNumber)
if ep_num and ep_num == tonumber(episode_num) then
DANMAKU.anime = animeTitle
DANMAKU.episode = episode.episodeTitle
set_episode_id(episode.episodeId)
break
end
end
end)
end
local function match_anime()
local animes = {}
local anime_type = "tvseries"
local type_count = 0
local title, season_num, episode_num = parse_title()
if not episode_num then
msg.info("无法解析剧集信息")
return
end
if title:match("OVA") or title:match("OAD") then
anime_type = "ova"
end
local encoded_query = url_encode(title)
local url = options.api_server .. "/api/v2/search/anime"
local params = "keyword=" .. encoded_query
local full_url = url .. "?" .. params
local args = make_danmaku_request_args("GET", full_url)
if not args then return end
call_cmd_async(args, function(error, json)
async_running = false
if error then
show_message("HTTP 请求失败,打开控制台查看详情", 5)
msg.error(error)
return
end
local data = utils.parse_json(json)
if not data or not data.animes then
msg.info("无结果")
return
end
for _, anime in ipairs(data.animes) do
if anime.type == anime_type then
type_count = type_count + 1
table.insert(animes, anime)
end
end
if type_count == 1 then
match_episode(animes[1].animeTitle, animes[1].bangumiId, episode_num)
elseif type_count > 1 and season_num then
local best_match, best_score = nil, -1
local target_title = title
if tonumber(season_num) > 1 then
target_title = title .. "" .. number_to_chinese(season_num) .. ""
end
for _, anime in ipairs(animes) do
if anime.animeTitle:match("第一[季部]") and tonumber(season_num) == 1 then
target_title = title .. " 第一季"
end
local score = jaro_winkler(target_title, anime.animeTitle)
msg.debug(("候选: %s -> 相似度 %.3f"):format(anime.animeTitle, score))
if score > best_score then
best_score = score
best_match = anime
end
end
if best_match and best_score >= 0.75 then
msg.info(("模糊匹配选中: %s (score=%.2f)"):format(best_match.animeTitle, best_score))
match_episode(best_match.animeTitle, best_match.bangumiId, episode_num)
else
msg.info("匹配到多个结果,但相似度不足,请手动搜索")
end
else
msg.info("没有找到合适的匹配结果")
end
end)
end
-- 执行哈希匹配获取弹幕
local function match_file(file_path, file_name, callback)
-- 计算文件哈希
local hash = nil
local file_info = utils.file_info(file_path)
if file_info and file_info.size > 16 * 1024 * 1024 then
local file, error = io.open(normalize(file_path), 'rb')
if file and not error then
local m = MD5.new()
for _ = 1, 16 * 1024 do
local content = file:read(1024)
if not content then
break
end
m:update(content)
end
file:close()
hash = m:finish()
end
end
if hash then msg.info('hash:', hash) end
local title, season_num, episode_num = parse_title()
if title and episode_num then
if season_num then
file_name = title .. " S" .. season_num .. "E" .. episode_num
else
file_name = title .. " E" .. episode_num
end
else
file_name = title
end
local url = options.api_server .. "/api/v2/match"
local args = make_danmaku_request_args("POST", url, {
["Content-Type"] = "application/json"
}, {
fileName = file_name,
fileHash = hash or "",
matchMode = "hashAndFileName"
}
)
if not args then return end
call_cmd_async(args, function(error, json)
async_running = false
if error then
show_message("HTTP 请求失败,打开控制台查看详情", 5)
callback(error)
return
end
local data = utils.parse_json(json)
if not data or not data.isMatched or #data.matches > 1 then
callback("没有匹配的剧集")
return
end
DANMAKU.anime = data.matches[1].animeTitle
DANMAKU.episode = data.matches[1].episodeTitle
-- 获取并加载弹幕数据
set_episode_id(data.matches[1].episodeId)
end)
end
-- 异步获取弹幕数据
function fetch_danmaku_data(args, callback)
call_cmd_async(args, function(error, json)
async_running = false
if error then
show_message("获取数据失败", 3)
msg.error("HTTP 请求失败:" .. error)
return
end
local data = utils.parse_json(json)
callback(data)
end)
end
-- 保存弹幕数据
function save_danmaku_data(comments, query, danmaku_source)
local temp_file = "danmaku-" .. PID .. DANMAKU.count .. ".json"
local danmaku_file = utils.join_path(DANMAKU_PATH, temp_file)
DANMAKU.count = DANMAKU.count + 1
local success = save_danmaku_json(comments, danmaku_file)
if success then
if DANMAKU.sources[query] ~= nil then
if DANMAKU.sources[query].fname and file_exists(DANMAKU.sources[query].fname) then
os.remove(DANMAKU.sources[query].fname)
end
DANMAKU.sources[query]["fname"] = danmaku_file
else
DANMAKU.sources[query] = {from = danmaku_source, fname = danmaku_file}
end
end
end
function save_danmaku_downloaded(url, downloaded_file)
if DANMAKU.sources[url] ~= nil then
if DANMAKU.sources[url].fname and file_exists(DANMAKU.sources[url].fname) then
os.remove(DANMAKU.sources[url].fname)
end
DANMAKU.sources[url]["fname"] = downloaded_file
else
DANMAKU.sources[url] = {from = "user_custom", fname = downloaded_file}
end
end
-- 处理弹幕数据
function handle_danmaku_data(query, data, from_menu)
local comments = data["comments"]
local count = data["count"]
-- 如果没有数据,进行重试
if count == 0 then
show_message("服务器无缓存数据,再次尝试请求", 30)
msg.verbose("服务器无缓存数据,再次尝试请求")
-- 等待 2 秒后重试
local start = os.time()
while os.time() - start < 2 do
-- 空循环,等待 2 秒
end
-- 重新发起请求
local url = options.api_server .. "/api/v2/extcomment?url=" .. url_encode(query)
local args = make_danmaku_request_args("GET", url)
if args == nil then
return
end
fetch_danmaku_data(args, function(retry_data)
if not retry_data or not retry_data["comments"] or retry_data["count"] == 0 then
get_danmaku_fallback(query)
return
end
save_danmaku_data(retry_data["comments"], query, "user_custom")
load_danmaku(from_menu)
end)
else
save_danmaku_data(comments, query, "user_custom")
load_danmaku(from_menu)
end
end
-- 处理第三方弹幕数据
function handle_related_danmaku(index, relateds, related, shift, callback)
local url = options.api_server .. "/api/v2/extcomment?url=" .. url_encode(related["url"])
show_message(string.format("正在从第三方库装填弹幕 [%d/%d]", index, #relateds), 30)
msg.verbose("正在从第三方库装填弹幕:" .. url)
local args = make_danmaku_request_args("GET", url)
if args == nil then
return
end
fetch_danmaku_data(args, function(data)
local comments = {}
if data and data["comments"] then
if data["count"] == 0 then
-- 如果没有数据,稍等 2 秒重试
local start = os.time()
while os.time() - start < 2 do
-- 空循环,等待 2 秒
end
fetch_danmaku_data(args, function(data)
for _, comment in ipairs(data["comments"]) do
comment["shift"] = shift
table.insert(comments, comment)
end
callback(comments)
end)
else
for _, comment in ipairs(data["comments"]) do
comment["shift"] = shift
table.insert(comments, comment)
end
callback(comments)
end
else
show_message("无数据", 3)
msg.info("无数据")
callback(comments)
end
end)
end
-- 处理dandan库的弹幕数据
function handle_main_danmaku(url, from_menu)
show_message("正在从弹弹Play库装填弹幕", 30)
msg.verbose("尝试获取弹幕:" .. url)
local args = make_danmaku_request_args("GET", url)
if args == nil then
return
end
fetch_danmaku_data(args, function(data)
if not data or not data["comments"] then
show_message("无数据", 3)
msg.info("无数据")
return
end
local comments = data["comments"]
local count = data["count"]
if count == 0 then
if DANMAKU.sources[url] == nil then
DANMAKU.sources[url] = {from = "api_server"}
end
load_danmaku(from_menu)
return
end
save_danmaku_data(comments, url, "api_server")
load_danmaku(from_menu)
end)
end
-- 处理获取到的数据
function handle_fetched_danmaku(data, url, from_menu)
if data and data["comments"] then
if data["count"] == 0 then
if DANMAKU.sources[url] == nil then
DANMAKU.sources[url] = {from = "api_server"}
end
show_message("该集弹幕内容为空,结束加载", 3)
msg.verbose("该集弹幕内容为空,结束加载")
return
end
save_danmaku_data(data["comments"], url, "api_server")
load_danmaku(from_menu)
else
show_message("无数据", 3)
msg.info("无数据")
end
end
-- 匹配弹幕库 comment, 仅匹配dandan本身弹幕库
-- 通过danmaku apiurl+id获取弹幕
function fetch_danmaku(episodeId, from_menu)
local url = options.api_server .. "/api/v2/comment/" .. episodeId .. "?withRelated=true&chConvert=0"
show_message("弹幕加载中...", 30)
msg.verbose("尝试获取弹幕:" .. url)
local args = make_danmaku_request_args("GET", url)
if args == nil then
return
end
fetch_danmaku_data(args, function(data)
handle_fetched_danmaku(data, url, from_menu)
end)
end
-- 主函数:获取所有相关弹幕
function fetch_danmaku_all(episodeId, from_menu)
local url = options.api_server .. "/api/v2/related/" .. episodeId
show_message("弹幕加载中...", 30)
msg.verbose("尝试获取弹幕:" .. url)
local args = make_danmaku_request_args("GET", url)
if args == nil then
return
end
fetch_danmaku_data(args, function(data)
if not data or not data["relateds"] then
show_message("无数据", 3)
msg.info("无数据")
return
end
-- 处理所有的相关弹幕
local relateds = data["relateds"]
local function process_related(index)
if index > #relateds then
-- 所有相关弹幕加载完成后,开始加载主库弹幕
url = options.api_server .. "/api/v2/comment/" .. episodeId .. "?withRelated=false&chConvert=0"
handle_main_danmaku(url, from_menu)
return
end
local related = relateds[index]
local shift = related["shift"]
-- 处理当前的相关弹幕
handle_related_danmaku(index, relateds, related, shift, function(comments)
if #comments == 0 then
if DANMAKU.sources[related["url"]] == nil then
DANMAKU.sources[related["url"]] = {from = "api_server"}
end
else
save_danmaku_data(comments, related["url"], "api_server")
end
-- 继续处理下一个相关弹幕
process_related(index + 1)
end)
end
-- 从第一个相关库开始请求
process_related(1)
end)
end
-- 从用户添加过的弹幕源添加弹幕
function addon_danmaku(dir, from_menu)
if dir then
local history_json = read_file(HISTORY_PATH)
local history = utils.parse_json(history_json) or {}
if history[dir] and history[dir].extra ~= nil then
return
end
end
for url, source in pairs(DANMAKU.sources) do
if source.from ~= "api_server" then
add_danmaku_source(url, from_menu)
end
end
end
--通过输入源url获取弹幕库
function add_danmaku_source(query, from_menu)
if DANMAKU.sources[query] == nil then
DANMAKU.sources[query] = {from = "user_custom"}
end
from_menu = from_menu or false
if from_menu then
add_source_to_history(query, DANMAKU.sources[query])
end
if is_protocol(query) then
add_danmaku_source_online(query, from_menu)
else
add_danmaku_source_local(query, from_menu)
end
end
function add_danmaku_source_local(query, from_menu)
local path = normalize(query)
if not file_exists(path) then
msg.warn("无效的文件路径")
return
end
if not (string.match(path, "%.xml$") or string.match(path, "%.json$") or string.match(path, "%.ass$")) then
msg.warn("仅支持弹幕文件")
return
end
if DANMAKU.sources[query] ~= nil then
if DANMAKU.sources[query].fname and file_exists(DANMAKU.sources[query].fname) then
os.remove(DANMAKU.sources[query].fname)
end
DANMAKU.sources[query]["from"] = "user_local"
DANMAKU.sources[query]["fname"] = path
else
DANMAKU.sources[query] = {from = "user_local", fname = path}
end
set_danmaku_button()
load_danmaku(from_menu)
end
--通过输入源url获取弹幕库
function add_danmaku_source_online(query, from_menu)
set_danmaku_button()
local url = options.api_server .. "/api/v2/extcomment?url=" .. url_encode(query)
show_message("弹幕加载中...", 30)
msg.verbose("尝试获取弹幕:" .. url)
local args = make_danmaku_request_args("GET", url)
if args == nil then
return
end
fetch_danmaku_data(args, function(data)
if not data or not data["comments"] then
show_message("此源弹幕无法加载", 3)
msg.verbose("此源弹幕无法加载")
return
end
handle_danmaku_data(query, data, from_menu)
end)
end
-- 将弹幕转换为factory可读的json格式
function save_danmaku_json(comments, json_filename)
local temp_file = "danmaku-" .. PID .. ".json"
json_filename = json_filename or utils.join_path(DANMAKU_PATH, temp_file)
local json_file = io.open(json_filename, "w")
if json_file then
json_file:write("[\n")
for _, comment in ipairs(comments) do
local p = comment["p"]
local shift = comment["shift"]
if p then
local fields = split(p, ",")
if shift ~= nil then
fields[1] = tonumber(fields[1]) + tonumber(shift)
end
local c_value = string.format(
"%s,%s,%s,25,,,",
tostring(fields[1]), -- first field of p to first field of c
fields[3], -- third field of p to second field of c
fields[2] -- second field of p to third field of c
)
local m_value = comment["m"]
:gsub("[%z\1-\31]", "")
:gsub("\\", "")
:gsub("\"", "")
-- Write the JSON object as a single line, no spaces or extra formatting
local json_entry = string.format('{"c":"%s","m":"%s"},\n', c_value, m_value)
json_file:write(json_entry)
end
end
json_file:write("]")
json_file:close()
return true
end
return false
end
-- 通过文件前 16M 的 hash 值进行弹幕匹配
function get_danmaku_with_hash(file_name, file_path)
if type(MD5) ~= "table" or not MD5.sum then
msg.warn("MD5 模块不支持 Lua 5.1,回退到文件名匹配")
match_anime()
return
end
if is_protocol(file_path) then
set_danmaku_button()
local temp_file = "temp-" .. PID .. ".mp4"
local arg = {
"curl",
"--connect-timeout",
"10",
"--max-time",
"30",
"--range",
"0-16777215",
"--user-agent",
options.user_agent,
"--output",
utils.join_path(DANMAKU_PATH, temp_file),
"-L",
file_path,
}
if options.proxy ~= "" then
table.insert(arg, '-x')
table.insert(arg, options.proxy)
end
call_cmd_async(arg, function(error)
async_running = false
file_path = utils.join_path(DANMAKU_PATH, temp_file)
match_file(file_path, file_name, function(error)
if error then
msg.error(error)
msg.info("尝试通过解析文件名获取弹幕")
match_anime()
end
end)
end)
else
local dir = get_parent_directory(file_path)
local excluded_path = utils.parse_json(options.excluded_path)
if PLATFORM == "windows" then
for i, path in pairs(excluded_path) do
excluded_path[i] = path:gsub("/", "\\")
end
end
if contains_any(excluded_path, dir) then
match_anime()
return
end
match_file(file_path, file_name, function(error)
if error then
msg.error(error)
msg.info("尝试通过解析文件名获取弹幕")
match_anime()
end
end)
end
end
+347
View File
@@ -0,0 +1,347 @@
local utils = require 'mp.utils'
local msg = require 'mp.msg'
local Source = {
["b 站"] = "bilibili1",
["腾讯"] = "qq",
["爱奇艺"] = "qiyi",
["优酷"] = "youku",
}
local function load_extra_danmaku(url, episode, number, class, id, site, title, year)
local play_url = nil
if url:match("^.-%.html") then
play_url = url:match("^(.-%.html).*")
else
play_url = url:gsub("%?bsource=360ogvys$","")
end
ENABLED = true
DANMAKU.anime = title .. " (" .. year .. ")"
DANMAKU.episode = "" .. episode .. ""
DANMAKU.source = site
DANMAKU.extra = {
id = id,
site = site,
year = year,
class = class,
title = title,
number = tonumber(number),
episodenum = tonumber(episode),
}
write_history()
add_danmaku_source(play_url, true)
end
local function query_tmdb(title, class, menu)
local encoded_title = url_encode(title)
local url = string.format("https://api.themoviedb.org/3/search/%s?api_key=%s&query=%s&language=zh-CN",
class, Base64.decode(options.tmdb_api_key), encoded_title)
local cmd = {
"curl",
"-s",
"-H", "accept: application/json",
url
}
if options.proxy ~= "" then
table.insert(cmd, '-x')
table.insert(cmd, options.proxy)
end
local res = mp.command_native({
name = "subprocess",
args = cmd,
capture_stdout = true,
capture_stderr = true,
})
local data = utils.parse_json(res.stdout)
if not res.status or res.status ~= 0 or not data.results or #data.results == 0 then
local message = "获取 tmdb 中文数据失败"
if uosc_available then
update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, title)
else
show_message(message, 3)
end
msg.error("获取 tmdb 中文数据失败:" .. res.stdout)
else
if class == "tv" then
return data.results[1].name
else
return data.results[1].title
end
end
end
local function get_number(cat, id, site)
local url = string.format("https://api.web.360kan.com/v1/detail?cat=%s&id=%s&site=%s",
cat, id, site)
local cmd = { "curl", "-s", url }
local res = mp.command_native({
name = "subprocess",
args = cmd,
capture_stdout = true,
capture_stderr = true,
})
if not res.status or res.status ~= 0 then
msg.error("Failed to fetch data: " .. (res.stderr or "unknown error"))
return nil
end
local result = utils.parse_json(res.stdout)
if result and result.data and result.data.allupinfo then
return tonumber(result.data.allupinfo[site])
end
return nil
end
function get_details(class, id, site, title, year, number, episodenum)
local message = episodenum and "查询弹幕中..." or "加载数据中..."
local menu_type = "menu_details"
local menu_title = "剧集信息"
local footnote = "使用 / 打开筛选"
if uosc_available and not episodenum then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 3)
end
local cat = 0
if class == "电影" then
cat = 1
elseif class == "电视剧" then
cat = 2
-- elseif class == "综艺" then
-- cat = 3
elseif class == "动漫" then
cat = 4
end
if not number and cat ~= 0 then
number = get_number(cat, id, site)
end
if not number or cat == 0 then
local message = "无结果"
if uosc_available and not episodenum then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 3)
end
msg.verbose("无结果")
return
end
local url = string.format("https://api.web.360kan.com/v1/detail?cat=%s&id=%s&start=1&end=%s&site=%s",
cat, id, number, site)
local cmd = { "curl", "-s", url }
local res = mp.command_native({
name = "subprocess",
args = cmd,
capture_stdout = true,
capture_stderr = true,
})
if not res.status or res.status ~= 0 then
local message = "无结果"
if uosc_available and not episodenum then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 3)
end
msg.verbose("无结果")
return
end
local result = utils.parse_json(res.stdout)
local items = {}
if result and result.data and result.data.allepidetail then
local data = result.data.allepidetail
local playurl, episode = nil, nil
if episodenum then
for _, item in ipairs(data[site]) do
if tonumber(item.playlink_num) == tonumber(episodenum) then
playurl = item.url
episode = item.playlink_num
break
end
end
if playurl then
load_extra_danmaku(playurl, episode, number, class, id, site, title, year)
return
end
end
for _, item in ipairs(data[site]) do
table.insert(items, {
title = "" .. item.playlink_num .. "",
hint = item.playlink_num,
value = {
"script-message-to",
mp.get_script_name(),
"add-extra-event",
item.url, item.playlink_num, number, class, id, site, title, year
},
})
end
end
if #items > 0 then
if uosc_available and not episodenum then
update_menu_uosc(menu_type, menu_title, items, footnote)
elseif not episodenum then
show_message("", 0)
mp.add_timeout(0.1, function()
open_menu_select(items)
end)
end
else
local message = "无结果"
if uosc_available and not episodenum then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 3)
end
msg.verbose("无结果")
end
end
local function search_query(query, class, menu)
local url = string.format("https://api.so.360kan.com/index?force_v=1&kw=%s", query)
if class ~= nil then
url = url .. "&type=" .. class
end
local cmd = { "curl", "-s", url }
local res = mp.command_native({
name = "subprocess",
args = cmd,
capture_stdout = true,
capture_stderr = true,
})
if not res.status or res.status ~= 0 then
local message = "无结果"
if uosc_available then
update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, query)
else
show_message(message, 3)
end
msg.verbose("无结果")
return
end
local result = utils.parse_json(res.stdout)
local items = {}
if result and result.data.longData and result.data.longData.rows then
for _, item in ipairs(result.data.longData.rows) do
if item.playlinks then
for source_name, source_id in pairs(Source) do
if item.playlinks[source_id] then
table.insert(items, {
title = item.titleTxt,
hint = item.cat_name .. " | " .. item.year .. " | 来源:" .. source_name,
value = {
"script-message-to",
mp.get_script_name(),
"get-extra-event",
item.cat_name, item.en_id, item.playlinks[source_id], source_id,
item.titleTxt, item.year,
},
})
end
end
end
end
end
if #items > 0 then
if uosc_available then
update_menu_uosc(menu.type, menu.title, items, menu.footnote, menu.cmd, query)
else
show_message("", 0)
mp.add_timeout(0.1, function()
open_menu_select(items)
end)
end
else
local message = "无结果"
if uosc_available then
update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, query)
else
show_message(message, 3)
end
msg.verbose("无结果")
end
end
function query_extra(name, class)
local name = name:gsub("%s*%(%d-%)%s*$", "")
local title = nil
local class = class and class:lower()
local message = "加载数据中..."
local menu = {
type = "menu_anime",
title = "在此处输入番剧名称",
footnote = "使用enter或ctrl+enter进行搜索"
}
menu.cmd = { "script-message-to", mp.get_script_name(), "search-anime-event" }
if uosc_available then
update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, name)
else
show_message(message, 30)
end
if is_chinese(name) then
search_query(name, class, menu)
return
end
if options.tmdb_api_key == "" or #Base64.decode(options.tmdb_api_key) < 32 then
local message = "请正确设置 tmdb_api_key 或尝试使用中文搜索"
if uosc_available then
update_menu_uosc(menu.type, menu.title, message, menu.footnote, menu.cmd, name)
else
show_message(message, 3)
end
return
end
if class == "dy" then
title = query_tmdb(name, "movie", menu)
else
title = query_tmdb(name, "tv", menu)
end
if title then
search_query(title, class, menu)
end
end
mp.register_script_message("get-extra-event", function(cat, id, playlink, source_id, title, year)
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_anime")
end
if cat == "电影" then
if playlink:match("^.-%.html") then
playlink = playlink:match("^(.-%.html).*")
else
playlink = playlink:gsub("%?bsource=360ogvys$","")
end
DANMAKU.anime = title .. " (" .. year .. ")"
DANMAKU.episode = "电影"
DANMAKU.source = source_id
write_history()
add_danmaku_source(playlink, true)
else
get_details(cat, id, source_id, title, year)
end
end)
mp.register_script_message("add-extra-event", function(url, episode, number, class, id, site, title, year)
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_details")
end
load_extra_danmaku(url, episode, number, class, id, site, title, year)
end)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+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)
+491
View File
@@ -0,0 +1,491 @@
-- modified from https://github.com/idiomic/Lua_AES
--[[
Copyright 2019 Tyler Richard Hoyer
Copyright 2025 dyphire
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
local unpack = unpack or table.unpack
local GF8x2 = {
[0]=0x00,0x02,0x04,0x06,0x08,0x0a,0x0c,0x0e,0x10,0x12,0x14,0x16,0x18,0x1a,0x1c,0x1e,
0x20,0x22,0x24,0x26,0x28,0x2a,0x2c,0x2e,0x30,0x32,0x34,0x36,0x38,0x3a,0x3c,0x3e,
0x40,0x42,0x44,0x46,0x48,0x4a,0x4c,0x4e,0x50,0x52,0x54,0x56,0x58,0x5a,0x5c,0x5e,
0x60,0x62,0x64,0x66,0x68,0x6a,0x6c,0x6e,0x70,0x72,0x74,0x76,0x78,0x7a,0x7c,0x7e,
0x80,0x82,0x84,0x86,0x88,0x8a,0x8c,0x8e,0x90,0x92,0x94,0x96,0x98,0x9a,0x9c,0x9e,
0xa0,0xa2,0xa4,0xa6,0xa8,0xaa,0xac,0xae,0xb0,0xb2,0xb4,0xb6,0xb8,0xba,0xbc,0xbe,
0xc0,0xc2,0xc4,0xc6,0xc8,0xca,0xcc,0xce,0xd0,0xd2,0xd4,0xd6,0xd8,0xda,0xdc,0xde,
0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xee,0xf0,0xf2,0xf4,0xf6,0xf8,0xfa,0xfc,0xfe,
0x1b,0x19,0x1f,0x1d,0x13,0x11,0x17,0x15,0x0b,0x09,0x0f,0x0d,0x03,0x01,0x07,0x05,
0x3b,0x39,0x3f,0x3d,0x33,0x31,0x37,0x35,0x2b,0x29,0x2f,0x2d,0x23,0x21,0x27,0x25,
0x5b,0x59,0x5f,0x5d,0x53,0x51,0x57,0x55,0x4b,0x49,0x4f,0x4d,0x43,0x41,0x47,0x45,
0x7b,0x79,0x7f,0x7d,0x73,0x71,0x77,0x75,0x6b,0x69,0x6f,0x6d,0x63,0x61,0x67,0x65,
0x9b,0x99,0x9f,0x9d,0x93,0x91,0x97,0x95,0x8b,0x89,0x8f,0x8d,0x83,0x81,0x87,0x85,
0xbb,0xb9,0xbf,0xbd,0xb3,0xb1,0xb7,0xb5,0xab,0xa9,0xaf,0xad,0xa3,0xa1,0xa7,0xa5,
0xdb,0xd9,0xdf,0xdd,0xd3,0xd1,0xd7,0xd5,0xcb,0xc9,0xcf,0xcd,0xc3,0xc1,0xc7,0xc5,
0xfb,0xf9,0xff,0xfd,0xf3,0xf1,0xf7,0xf5,0xeb,0xe9,0xef,0xed,0xe3,0xe1,0xe7,0xe5
}
local GF8x3 = {
[0]=0x00,0x03,0x06,0x05,0x0c,0x0f,0x0a,0x09,0x18,0x1b,0x1e,0x1d,0x14,0x17,0x12,0x11,
0x30,0x33,0x36,0x35,0x3c,0x3f,0x3a,0x39,0x28,0x2b,0x2e,0x2d,0x24,0x27,0x22,0x21,
0x60,0x63,0x66,0x65,0x6c,0x6f,0x6a,0x69,0x78,0x7b,0x7e,0x7d,0x74,0x77,0x72,0x71,
0x50,0x53,0x56,0x55,0x5c,0x5f,0x5a,0x59,0x48,0x4b,0x4e,0x4d,0x44,0x47,0x42,0x41,
0xc0,0xc3,0xc6,0xc5,0xcc,0xcf,0xca,0xc9,0xd8,0xdb,0xde,0xdd,0xd4,0xd7,0xd2,0xd1,
0xf0,0xf3,0xf6,0xf5,0xfc,0xff,0xfa,0xf9,0xe8,0xeb,0xee,0xed,0xe4,0xe7,0xe2,0xe1,
0xa0,0xa3,0xa6,0xa5,0xac,0xaf,0xaa,0xa9,0xb8,0xbb,0xbe,0xbd,0xb4,0xb7,0xb2,0xb1,
0x90,0x93,0x96,0x95,0x9c,0x9f,0x9a,0x99,0x88,0x8b,0x8e,0x8d,0x84,0x87,0x82,0x81,
0x9b,0x98,0x9d,0x9e,0x97,0x94,0x91,0x92,0x83,0x80,0x85,0x86,0x8f,0x8c,0x89,0x8a,
0xab,0xa8,0xad,0xae,0xa7,0xa4,0xa1,0xa2,0xb3,0xb0,0xb5,0xb6,0xbf,0xbc,0xb9,0xba,
0xfb,0xf8,0xfd,0xfe,0xf7,0xf4,0xf1,0xf2,0xe3,0xe0,0xe5,0xe6,0xef,0xec,0xe9,0xea,
0xcb,0xc8,0xcd,0xce,0xc7,0xc4,0xc1,0xc2,0xd3,0xd0,0xd5,0xd6,0xdf,0xdc,0xd9,0xda,
0x5b,0x58,0x5d,0x5e,0x57,0x54,0x51,0x52,0x43,0x40,0x45,0x46,0x4f,0x4c,0x49,0x4a,
0x6b,0x68,0x6d,0x6e,0x67,0x64,0x61,0x62,0x73,0x70,0x75,0x76,0x7f,0x7c,0x79,0x7a,
0x3b,0x38,0x3d,0x3e,0x37,0x34,0x31,0x32,0x23,0x20,0x25,0x26,0x2f,0x2c,0x29,0x2a,
0x0b,0x08,0x0d,0x0e,0x07,0x04,0x01,0x02,0x13,0x10,0x15,0x16,0x1f,0x1c,0x19,0x1a
}
local GF8x9 = {
[0]=0x00,0x09,0x12,0x1b,0x24,0x2d,0x36,0x3f,0x48,0x41,0x5a,0x53,0x6c,0x65,0x7e,0x77,
0x90,0x99,0x82,0x8b,0xb4,0xbd,0xa6,0xaf,0xd8,0xd1,0xca,0xc3,0xfc,0xf5,0xee,0xe7,
0x3b,0x32,0x29,0x20,0x1f,0x16,0x0d,0x04,0x73,0x7a,0x61,0x68,0x57,0x5e,0x45,0x4c,
0xab,0xa2,0xb9,0xb0,0x8f,0x86,0x9d,0x94,0xe3,0xea,0xf1,0xf8,0xc7,0xce,0xd5,0xdc,
0x76,0x7f,0x64,0x6d,0x52,0x5b,0x40,0x49,0x3e,0x37,0x2c,0x25,0x1a,0x13,0x08,0x01,
0xe6,0xef,0xf4,0xfd,0xc2,0xcb,0xd0,0xd9,0xae,0xa7,0xbc,0xb5,0x8a,0x83,0x98,0x91,
0x4d,0x44,0x5f,0x56,0x69,0x60,0x7b,0x72,0x05,0x0c,0x17,0x1e,0x21,0x28,0x33,0x3a,
0xdd,0xd4,0xcf,0xc6,0xf9,0xf0,0xeb,0xe2,0x95,0x9c,0x87,0x8e,0xb1,0xb8,0xa3,0xaa,
0xec,0xe5,0xfe,0xf7,0xc8,0xc1,0xda,0xd3,0xa4,0xad,0xb6,0xbf,0x80,0x89,0x92,0x9b,
0x7c,0x75,0x6e,0x67,0x58,0x51,0x4a,0x43,0x34,0x3d,0x26,0x2f,0x10,0x19,0x02,0x0b,
0xd7,0xde,0xc5,0xcc,0xf3,0xfa,0xe1,0xe8,0x9f,0x96,0x8d,0x84,0xbb,0xb2,0xa9,0xa0,
0x47,0x4e,0x55,0x5c,0x63,0x6a,0x71,0x78,0x0f,0x06,0x1d,0x14,0x2b,0x22,0x39,0x30,
0x9a,0x93,0x88,0x81,0xbe,0xb7,0xac,0xa5,0xd2,0xdb,0xc0,0xc9,0xf6,0xff,0xe4,0xed,
0x0a,0x03,0x18,0x11,0x2e,0x27,0x3c,0x35,0x42,0x4b,0x50,0x59,0x66,0x6f,0x74,0x7d,
0xa1,0xa8,0xb3,0xba,0x85,0x8c,0x97,0x9e,0xe9,0xe0,0xfb,0xf2,0xcd,0xc4,0xdf,0xd6,
0x31,0x38,0x23,0x2a,0x15,0x1c,0x07,0x0e,0x79,0x70,0x6b,0x62,0x5d,0x54,0x4f,0x46
}
local GF8x11 = {
[0]=0x00,0x0b,0x16,0x1d,0x2c,0x27,0x3a,0x31,0x58,0x53,0x4e,0x45,0x74,0x7f,0x62,0x69,
0xb0,0xbb,0xa6,0xad,0x9c,0x97,0x8a,0x81,0xe8,0xe3,0xfe,0xf5,0xc4,0xcf,0xd2,0xd9,
0x7b,0x70,0x6d,0x66,0x57,0x5c,0x41,0x4a,0x23,0x28,0x35,0x3e,0x0f,0x04,0x19,0x12,
0xcb,0xc0,0xdd,0xd6,0xe7,0xec,0xf1,0xfa,0x93,0x98,0x85,0x8e,0xbf,0xb4,0xa9,0xa2,
0xf6,0xfd,0xe0,0xeb,0xda,0xd1,0xcc,0xc7,0xae,0xa5,0xb8,0xb3,0x82,0x89,0x94,0x9f,
0x46,0x4d,0x50,0x5b,0x6a,0x61,0x7c,0x77,0x1e,0x15,0x08,0x03,0x32,0x39,0x24,0x2f,
0x8d,0x86,0x9b,0x90,0xa1,0xaa,0xb7,0xbc,0xd5,0xde,0xc3,0xc8,0xf9,0xf2,0xef,0xe4,
0x3d,0x36,0x2b,0x20,0x11,0x1a,0x07,0x0c,0x65,0x6e,0x73,0x78,0x49,0x42,0x5f,0x54,
0xf7,0xfc,0xe1,0xea,0xdb,0xd0,0xcd,0xc6,0xaf,0xa4,0xb9,0xb2,0x83,0x88,0x95,0x9e,
0x47,0x4c,0x51,0x5a,0x6b,0x60,0x7d,0x76,0x1f,0x14,0x09,0x02,0x33,0x38,0x25,0x2e,
0x8c,0x87,0x9a,0x91,0xa0,0xab,0xb6,0xbd,0xd4,0xdf,0xc2,0xc9,0xf8,0xf3,0xee,0xe5,
0x3c,0x37,0x2a,0x21,0x10,0x1b,0x06,0x0d,0x64,0x6f,0x72,0x79,0x48,0x43,0x5e,0x55,
0x01,0x0a,0x17,0x1c,0x2d,0x26,0x3b,0x30,0x59,0x52,0x4f,0x44,0x75,0x7e,0x63,0x68,
0xb1,0xba,0xa7,0xac,0x9d,0x96,0x8b,0x80,0xe9,0xe2,0xff,0xf4,0xc5,0xce,0xd3,0xd8,
0x7a,0x71,0x6c,0x67,0x56,0x5d,0x40,0x4b,0x22,0x29,0x34,0x3f,0x0e,0x05,0x18,0x13,
0xca,0xc1,0xdc,0xd7,0xe6,0xed,0xf0,0xfb,0x92,0x99,0x84,0x8f,0xbe,0xb5,0xa8,0xa3
}
local GF8x13 = {
[0]=0x00,0x0d,0x1a,0x17,0x34,0x39,0x2e,0x23,0x68,0x65,0x72,0x7f,0x5c,0x51,0x46,0x4b,
0xd0,0xdd,0xca,0xc7,0xe4,0xe9,0xfe,0xf3,0xb8,0xb5,0xa2,0xaf,0x8c,0x81,0x96,0x9b,
0xbb,0xb6,0xa1,0xac,0x8f,0x82,0x95,0x98,0xd3,0xde,0xc9,0xc4,0xe7,0xea,0xfd,0xf0,
0x6b,0x66,0x71,0x7c,0x5f,0x52,0x45,0x48,0x03,0x0e,0x19,0x14,0x37,0x3a,0x2d,0x20,
0x6d,0x60,0x77,0x7a,0x59,0x54,0x43,0x4e,0x05,0x08,0x1f,0x12,0x31,0x3c,0x2b,0x26,
0xbd,0xb0,0xa7,0xaa,0x89,0x84,0x93,0x9e,0xd5,0xd8,0xcf,0xc2,0xe1,0xec,0xfb,0xf6,
0xd6,0xdb,0xcc,0xc1,0xe2,0xef,0xf8,0xf5,0xbe,0xb3,0xa4,0xa9,0x8a,0x87,0x90,0x9d,
0x06,0x0b,0x1c,0x11,0x32,0x3f,0x28,0x25,0x6e,0x63,0x74,0x79,0x5a,0x57,0x40,0x4d,
0xda,0xd7,0xc0,0xcd,0xee,0xe3,0xf4,0xf9,0xb2,0xbf,0xa8,0xa5,0x86,0x8b,0x9c,0x91,
0x0a,0x07,0x10,0x1d,0x3e,0x33,0x24,0x29,0x62,0x6f,0x78,0x75,0x56,0x5b,0x4c,0x41,
0x61,0x6c,0x7b,0x76,0x55,0x58,0x4f,0x42,0x09,0x04,0x13,0x1e,0x3d,0x30,0x27,0x2a,
0xb1,0xbc,0xab,0xa6,0x85,0x88,0x9f,0x92,0xd9,0xd4,0xc3,0xce,0xed,0xe0,0xf7,0xfa,
0xb7,0xba,0xad,0xa0,0x83,0x8e,0x99,0x94,0xdf,0xd2,0xc5,0xc8,0xeb,0xe6,0xf1,0xfc,
0x67,0x6a,0x7d,0x70,0x53,0x5e,0x49,0x44,0x0f,0x02,0x15,0x18,0x3b,0x36,0x21,0x2c,
0x0c,0x01,0x16,0x1b,0x38,0x35,0x22,0x2f,0x64,0x69,0x7e,0x73,0x50,0x5d,0x4a,0x47,
0xdc,0xd1,0xc6,0xcb,0xe8,0xe5,0xf2,0xff,0xb4,0xb9,0xae,0xa3,0x80,0x8d,0x9a,0x97
}
local GF8x14 = {
[0]=0x00,0x0e,0x1c,0x12,0x38,0x36,0x24,0x2a,0x70,0x7e,0x6c,0x62,0x48,0x46,0x54,0x5a,
0xe0,0xee,0xfc,0xf2,0xd8,0xd6,0xc4,0xca,0x90,0x9e,0x8c,0x82,0xa8,0xa6,0xb4,0xba,
0xdb,0xd5,0xc7,0xc9,0xe3,0xed,0xff,0xf1,0xab,0xa5,0xb7,0xb9,0x93,0x9d,0x8f,0x81,
0x3b,0x35,0x27,0x29,0x03,0x0d,0x1f,0x11,0x4b,0x45,0x57,0x59,0x73,0x7d,0x6f,0x61,
0xad,0xa3,0xb1,0xbf,0x95,0x9b,0x89,0x87,0xdd,0xd3,0xc1,0xcf,0xe5,0xeb,0xf9,0xf7,
0x4d,0x43,0x51,0x5f,0x75,0x7b,0x69,0x67,0x3d,0x33,0x21,0x2f,0x05,0x0b,0x19,0x17,
0x76,0x78,0x6a,0x64,0x4e,0x40,0x52,0x5c,0x06,0x08,0x1a,0x14,0x3e,0x30,0x22,0x2c,
0x96,0x98,0x8a,0x84,0xae,0xa0,0xb2,0xbc,0xe6,0xe8,0xfa,0xf4,0xde,0xd0,0xc2,0xcc,
0x41,0x4f,0x5d,0x53,0x79,0x77,0x65,0x6b,0x31,0x3f,0x2d,0x23,0x09,0x07,0x15,0x1b,
0xa1,0xaf,0xbd,0xb3,0x99,0x97,0x85,0x8b,0xd1,0xdf,0xcd,0xc3,0xe9,0xe7,0xf5,0xfb,
0x9a,0x94,0x86,0x88,0xa2,0xac,0xbe,0xb0,0xea,0xe4,0xf6,0xf8,0xd2,0xdc,0xce,0xc0,
0x7a,0x74,0x66,0x68,0x42,0x4c,0x5e,0x50,0x0a,0x04,0x16,0x18,0x32,0x3c,0x2e,0x20,
0xec,0xe2,0xf0,0xfe,0xd4,0xda,0xc8,0xc6,0x9c,0x92,0x80,0x8e,0xa4,0xaa,0xb8,0xb6,
0x0c,0x02,0x10,0x1e,0x34,0x3a,0x28,0x26,0x7c,0x72,0x60,0x6e,0x44,0x4a,0x58,0x56,
0x37,0x39,0x2b,0x25,0x0f,0x01,0x13,0x1d,0x47,0x49,0x5b,0x55,0x7f,0x71,0x63,0x6d,
0xd7,0xd9,0xcb,0xc5,0xef,0xe1,0xf3,0xfd,0xa7,0xa9,0xbb,0xb5,0x9f,0x91,0x83,0x8d
}
local s = {
[0]=0x63,0x7C,0x77,0x7B,0xF2,0x6B,0x6F,0xC5,0x30,0x01,0x67,0x2B,0xFE,0xD7,0xAB,0x76,
0xCA,0x82,0xC9,0x7D,0xFA,0x59,0x47,0xF0,0xAD,0xD4,0xA2,0xAF,0x9C,0xA4,0x72,0xC0,
0xB7,0xFD,0x93,0x26,0x36,0x3F,0xF7,0xCC,0x34,0xA5,0xE5,0xF1,0x71,0xD8,0x31,0x15,
0x04,0xC7,0x23,0xC3,0x18,0x96,0x05,0x9A,0x07,0x12,0x80,0xE2,0xEB,0x27,0xB2,0x75,
0x09,0x83,0x2C,0x1A,0x1B,0x6E,0x5A,0xA0,0x52,0x3B,0xD6,0xB3,0x29,0xE3,0x2F,0x84,
0x53,0xD1,0x00,0xED,0x20,0xFC,0xB1,0x5B,0x6A,0xCB,0xBE,0x39,0x4A,0x4C,0x58,0xCF,
0xD0,0xEF,0xAA,0xFB,0x43,0x4D,0x33,0x85,0x45,0xF9,0x02,0x7F,0x50,0x3C,0x9F,0xA8,
0x51,0xA3,0x40,0x8F,0x92,0x9D,0x38,0xF5,0xBC,0xB6,0xDA,0x21,0x10,0xFF,0xF3,0xD2,
0xCD,0x0C,0x13,0xEC,0x5F,0x97,0x44,0x17,0xC4,0xA7,0x7E,0x3D,0x64,0x5D,0x19,0x73,
0x60,0x81,0x4F,0xDC,0x22,0x2A,0x90,0x88,0x46,0xEE,0xB8,0x14,0xDE,0x5E,0x0B,0xDB,
0xE0,0x32,0x3A,0x0A,0x49,0x06,0x24,0x5C,0xC2,0xD3,0xAC,0x62,0x91,0x95,0xE4,0x79,
0xE7,0xC8,0x37,0x6D,0x8D,0xD5,0x4E,0xA9,0x6C,0x56,0xF4,0xEA,0x65,0x7A,0xAE,0x08,
0xBA,0x78,0x25,0x2E,0x1C,0xA6,0xB4,0xC6,0xE8,0xDD,0x74,0x1F,0x4B,0xBD,0x8B,0x8A,
0x70,0x3E,0xB5,0x66,0x48,0x03,0xF6,0x0E,0x61,0x35,0x57,0xB9,0x86,0xC1,0x1D,0x9E,
0xE1,0xF8,0x98,0x11,0x69,0xD9,0x8E,0x94,0x9B,0x1E,0x87,0xE9,0xCE,0x55,0x28,0xDF,
0x8C,0xA1,0x89,0x0D,0xBF,0xE6,0x42,0x68,0x41,0x99,0x2D,0x0F,0xB0,0x54,0xBB,0x16
}
local si = {
[0]=0x52,0x09,0x6A,0xD5,0x30,0x36,0xA5,0x38,0xBF,0x40,0xA3,0x9E,0x81,0xF3,0xD7,0xFB,
0x7C,0xE3,0x39,0x82,0x9B,0x2F,0xFF,0x87,0x34,0x8E,0x43,0x44,0xC4,0xDE,0xE9,0xCB,
0x54,0x7B,0x94,0x32,0xA6,0xC2,0x23,0x3D,0xEE,0x4C,0x95,0x0B,0x42,0xFA,0xC3,0x4E,
0x08,0x2E,0xA1,0x66,0x28,0xD9,0x24,0xB2,0x76,0x5B,0xA2,0x49,0x6D,0x8B,0xD1,0x25,
0x72,0xF8,0xF6,0x64,0x86,0x68,0x98,0x16,0xD4,0xA4,0x5C,0xCC,0x5D,0x65,0xB6,0x92,
0x6C,0x70,0x48,0x50,0xFD,0xED,0xB9,0xDA,0x5E,0x15,0x46,0x57,0xA7,0x8D,0x9D,0x84,
0x90,0xD8,0xAB,0x00,0x8C,0xBC,0xD3,0x0A,0xF7,0xE4,0x58,0x05,0xB8,0xB3,0x45,0x06,
0xD0,0x2C,0x1E,0x8F,0xCA,0x3F,0x0F,0x02,0xC1,0xAF,0xBD,0x03,0x01,0x13,0x8A,0x6B,
0x3A,0x91,0x11,0x41,0x4F,0x67,0xDC,0xEA,0x97,0xF2,0xCF,0xCE,0xF0,0xB4,0xE6,0x73,
0x96,0xAC,0x74,0x22,0xE7,0xAD,0x35,0x85,0xE2,0xF9,0x37,0xE8,0x1C,0x75,0xDF,0x6E,
0x47,0xF1,0x1A,0x71,0x1D,0x29,0xC5,0x89,0x6F,0xB7,0x62,0x0E,0xAA,0x18,0xBE,0x1B,
0xFC,0x56,0x3E,0x4B,0xC6,0xD2,0x79,0x20,0x9A,0xDB,0xC0,0xFE,0x78,0xCD,0x5A,0xF4,
0x1F,0xDD,0xA8,0x33,0x88,0x07,0xC7,0x31,0xB1,0x12,0x10,0x59,0x27,0x80,0xEC,0x5F,
0x60,0x51,0x7F,0xA9,0x19,0xB5,0x4A,0x0D,0x2D,0xE5,0x7A,0x9F,0x93,0xC9,0x9C,0xEF,
0xA0,0xE0,0x3B,0x4D,0xAE,0x2A,0xF5,0xB0,0xC8,0xEB,0xBB,0x3C,0x83,0x53,0x99,0x61,
0x17,0x2B,0x04,0x7E,0xBA,0x77,0xD6,0x26,0xE1,0x69,0x14,0x63,0x55,0x21,0x0C,0x7D
}
local rcon = {
0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,
0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,
0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,
0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,
0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,
0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,
0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,
0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,
0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,
0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,
0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,
0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,
0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,
0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,
0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,
0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d
}
local xor4 = {
[0]=0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,
1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,
2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,
3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,
4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,
5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,
6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,
7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,
8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,
9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,
10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,
11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,
12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,
13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,
14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,
15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,
}
local function xor8(a, b)
local al = a % 16
local bl = b % 16
return 16 * xor4[a - al + (b - bl) / 16] + xor4[16 * al + bl]
end
local function xor_blocks_8(a, b)
local res = {}
for i = 1, 16 do
res[i] = xor8(a[i], b[i])
end
return res
end
local function addRoundKey(state, key)
for i, byte in next, state do
state[i] = xor8(byte, key[i])
end
end
local function subBytes(state, s_box)
for i, byte in next, state do
state[i] = s_box[byte]
end
end
local function shiftRows(state)
state[2], state[6], state[10], state[14] =
state[6], state[10], state[14], state[2]
state[3], state[7], state[11], state[15] =
state[11], state[15], state[3], state[7]
state[4], state[8], state[12], state[16] =
state[16], state[4], state[8], state[12]
end
local function inv_shiftRows(state)
state[2], state[6], state[10], state[14] =
state[14], state[2], state[6], state[10]
state[3], state[7], state[11], state[15] =
state[11], state[15], state[3], state[7]
state[4], state[8], state[12], state[16] =
state[8], state[12], state[16], state[4]
end
local function mixColumns(state)
for i = 0, 3 do
local cur = i*4+1
local a, b, c, d = state[cur], state[cur + 1], state[cur + 2], state[cur + 3]
state[cur + 0] = xor8(xor8(xor8(GF8x2[a], GF8x3[b]), c), d)
state[cur + 1] = xor8(xor8(xor8(a, GF8x2[b]), GF8x3[c]), d)
state[cur + 2] = xor8(xor8(xor8(a, b), GF8x2[c]), GF8x3[d])
state[cur + 3] = xor8(xor8(xor8(GF8x3[a], b), c), GF8x2[d])
end
end
local function inv_mixColumns(state) -- TODO: fix
for i = 0, 3 do
local cur = i*4+1
local a, b, c, d = state[cur], state[cur + 1], state[cur + 2], state[cur + 3]
state[cur + 0] = xor8(xor8(xor8(GF8x14[a], GF8x11[b]), GF8x13[c]), GF8x9[d])
state[cur + 1] = xor8(xor8(xor8(GF8x9[a], GF8x14[b]), GF8x11[c]), GF8x13[d])
state[cur + 2] = xor8(xor8(xor8(GF8x13[a], GF8x9[b]), GF8x14[c]), GF8x11[d])
state[cur + 3] = xor8(xor8(xor8(GF8x11[a], GF8x13[b]), GF8x9[c]), GF8x14[d])
end
end
-- 256-bit key constants
local n = 32 -- number of bytes in the 256-bit encryption key
local b = 240 -- number of bytes in 15 128-bit round keys
local function schedule256(key)
local expanded = {}
for c = 0, n-1 do
expanded[c] = key[c]
end
local i = 1
local c = n
local t1, t2, t3, t4 --t
while c < b do
t1 = expanded[c-4]
t2 = expanded[c-3]
t3 = expanded[c-2]
t4 = expanded[c-1]
if (c % n == 0) then
t1, t2, t3, t4 = xor8(rcon[i+1], s[t2]), s[t3], s[t4], s[t1]
i = i + 1
end
if (c % n == 16) then
t1 = s[t1]
t2 = s[t2]
t3 = s[t3]
t4 = s[t4]
end
t1 = xor8(t1, expanded[c - n])
expanded[c] = t1
c = c + 1
t2 = xor8(t2, expanded[c - n])
expanded[c] = t2
c = c + 1
t3 = xor8(t3, expanded[c - n])
expanded[c] = t3
c = c + 1
t4 = xor8(t4, expanded[c - n])
expanded[c] = t4
c = c + 1
end
local roundKeys = {}
for round = 0, 14 do
local roundKey = {}
for byte = 0, 15 do
roundKey[byte+1] = expanded[round * 16 + byte]
end
roundKeys[round] = roundKey
end
return roundKeys
end
local function chunks(text, i)
local first = i * 16 + 1
if first > #text then
return
end
i = i + 1
local chunk = {text:byte(first, first + 15)}
for j = #chunk + 1, 16 do
chunk[j] = 0
end
return i, chunk
end
local function pkcs7_unpad(str)
local len = #str
if len == 0 then return str end
local pad_len = string.byte(str, len)
if pad_len < 1 or pad_len > 16 then
return nil
end
for i = len - pad_len + 1, len do
if string.byte(str, i) ~= pad_len then
return nil
end
end
return string.sub(str, 1, len - pad_len)
end
local function zero_unpad(str)
local len = #str
while len > 0 and string.byte(str, len) == 0 do
len = len - 1
end
return string.sub(str, 1, len)
end
local function unpad(str)
local unpadded = pkcs7_unpad(str)
if unpadded then
return unpadded
else
return zero_unpad(str)
end
end
local function encrypt(state, roundKeys)
addRoundKey(state, roundKeys[0])
for round = 1, 13 do
subBytes(state, s)
shiftRows(state)
mixColumns(state)
addRoundKey(state, roundKeys[round])
end
subBytes(state, s)
shiftRows(state)
addRoundKey(state, roundKeys[14])
end
local function decrypt(state, roundKeys)
addRoundKey(state, roundKeys[14])
inv_shiftRows(state)
subBytes(state, si)
for round = 13, 1, -1 do
addRoundKey(state, roundKeys[round])
inv_mixColumns(state)
inv_shiftRows(state)
subBytes(state, si)
end
addRoundKey(state, roundKeys[0])
end
local function ECB_encrypt(key, originaltext)
local text = {}
local roundKeys = schedule256(key)
local i = 0
while true do
i, state = chunks(originaltext, i)
if not state then break end
encrypt(state, roundKeys)
text[i] = string.char(unpack(state))
end
return table.concat(text)
end
local function ECB_decrypt(key, ciphertext)
local text = {}
local roundKeys = schedule256(key)
local i = 0
while true do
i, state = chunks(ciphertext, i)
if not state then break end
decrypt(state, roundKeys)
text[i] = string.char(unpack(state))
end
return unpad(table.concat(text))
end
local function CBC_encrypt(key, iv, originaltext)
local roundKeys = schedule256(key)
local text = {}
local prev_block = {unpack(iv)}
local i = 0
while true do
i, block = chunks(originaltext, i)
if not block then break end
local xored = xor_blocks_8(block, prev_block)
encrypt(xored, roundKeys)
text[i] = string.char(unpack(xored))
prev_block = xored
end
return table.concat(text)
end
local function CBC_decrypt(key, iv, ciphertext)
local roundKeys = schedule256(key)
local text = {}
local prev_block = {unpack(iv)}
local i = 0
while true do
i, block = chunks(ciphertext, i)
if not block then break end
local decrypted = {unpack(block)}
decrypt(decrypted, roundKeys)
local plain_block = xor_blocks_8(decrypted, prev_block)
text[i] = string.char(unpack(plain_block))
prev_block = block
end
local result = table.concat(text)
return unpad(result)
end
return {
ECB = {
encrypt = ECB_encrypt;
decrypt = ECB_decrypt;
};
CBC = {
encrypt = CBC_encrypt;
decrypt = CBC_decrypt;
};
}
+203
View File
@@ -0,0 +1,203 @@
--[[
base64 -- v1.5.3 public domain Lua base64 encoder/decoder
no warranty implied; use at your own risk
Needs bit32.extract function. If not present it's implemented using BitOp
or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua
implementation inspired by Rici Lake's post:
http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html
author: Ilya Kolbin (iskolbin@gmail.com)
url: github.com/iskolbin/lbase64
COMPATIBILITY
Lua 5.1+, LuaJIT
LICENSE
See end of file for license information.
--]]
local base64 = {}
local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode
if not extract then
if _G.bit then -- LuaJIT
local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band
extract = function( v, from, width )
return band( shr( v, from ), shl( 1, width ) - 1 )
end
elseif _G._VERSION == "Lua 5.1" then
extract = function( v, from, width )
local w = 0
local flag = 2^from
for i = 0, width-1 do
local flag2 = flag + flag
if v % flag2 >= flag then
w = w + 2^i
end
flag = flag2
end
return w
end
else -- Lua 5.3+
extract = load[[return function( v, from, width )
return ( v >> from ) & ((1 << width) - 1)
end]]()
end
end
function base64.makeencoder( s62, s63, spad )
local encoder = {}
for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J',
'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y',
'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n',
'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2',
'3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do
encoder[b64code] = char:byte()
end
return encoder
end
function base64.makedecoder( s62, s63, spad )
local decoder = {}
for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do
decoder[charcode] = b64code
end
return decoder
end
local DEFAULT_ENCODER = base64.makeencoder()
local DEFAULT_DECODER = base64.makedecoder()
local char, concat = string.char, table.concat
function base64.encode( str, encoder, usecaching )
encoder = encoder or DEFAULT_ENCODER
local t, k, n = {}, 1, #str
local lastn = n % 3
local cache = {}
for i = 1, n-lastn, 3 do
local a, b, c = str:byte( i, i+2 )
local v = a*0x10000 + b*0x100 + c
local s
if usecaching then
s = cache[v]
if not s then
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
cache[v] = s
end
else
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
end
t[k] = s
k = k + 1
end
if lastn == 2 then
local a, b = str:byte( n-1, n )
local v = a*0x10000 + b*0x100
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64])
elseif lastn == 1 then
local v = str:byte( n )*0x10000
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64])
end
return concat( t )
end
function base64.decode( b64, decoder, usecaching, schar1pos, schar2pos )
decoder = decoder or DEFAULT_DECODER
schar1pos = schar1pos or 62
schar2pos = schar2pos or 63
local pattern = '[^%w%+%/%=]'
if decoder then
local s62, s63
for charcode, b64code in pairs( decoder ) do
if b64code == schar1pos then s62 = charcode
elseif b64code == schar2pos then s63 = charcode
end
end
pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) )
end
b64 = b64:gsub( pattern, '' )
local cache = usecaching and {}
local t, k = {}, 1
local n = #b64
local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0
for i = 1, padding > 0 and n-4 or n, 4 do
local a, b, c, d = b64:byte( i, i+3 )
local s
if usecaching then
local v0 = a*0x1000000 + b*0x10000 + c*0x100 + d
s = cache[v0]
if not s then
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
cache[v0] = s
end
else
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
end
t[k] = s
k = k + 1
end
if padding == 1 then
local a, b, c = b64:byte( n-3, n-1 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
t[k] = char( extract(v,16,8), extract(v,8,8))
elseif padding == 2 then
local a, b = b64:byte( n-3, n-2 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000
t[k] = char( extract(v,16,8))
end
return concat( t )
end
return base64
--[[
------------------------------------------------------------------------------
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2018 Ilya Kolbin
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------------------------
--]]
+157
View File
@@ -0,0 +1,157 @@
-- Clean up media name
local function clean_name(name)
return name:gsub("^%[.-%]", " ")
:gsub("^%(.-%)", " ")
:gsub("[_%.%[%]]", " ")
:gsub("第%s*%d+%s*季", "")
:gsub("第%s*%d+%s*部", "")
:gsub("第[一二三四五六七八九十]+季", "")
:gsub("第[一二三四五六七八九十]+部", "")
:gsub("^%s*(.-)%s*$", "%1")
:gsub("[!@#%.%?%+%-%%&*_=,/~`]+$", "")
end
-- Formatters for media titles
local formatters = {
{
regex = "^(.-)%s*[_%-%.%s]%s*第%s*(%d+)%s*[季部]+%s*[_%-%.%s]%s*第%s*(%d+[%.v]?%d*)%s*[话集回]",
format = function(name, season, episode)
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)
return clean_name(name) .. " S" .. chinese_to_number(season) .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*第([一二三四五六七八九十]+)[季部]+%s*[_%-%.%s]%s*[eEpP]+[_%-%.%s]?(%d+[%.v]?%d*)",
format = function(name, season, episode)
return clean_name(name) .. " S" .. chinese_to_number(season) .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)[_%.%s]%d%d[_%.%s]%d%d%s*[_%.%s]?(.-)%s*[_%.%s]%d+[pPkKxXbBfF]",
format = function(name, year, subtitle)
local title = clean_name(name)
if subtitle then
title = title .. ": " .. subtitle:gsub("%.", " "):gsub("^%s*(.-)%s*$", "%1")
end
return title .. " (" .. year .. ")"
end
},
{
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)%s*[_%.%s]%s*[sS](%d+)[%.%-%s:]?[eE](%d+%.?%d*)",
format = function(name, year, season, episode)
return clean_name(name) .. " (" .. year .. ") S" .. season .. "E" .. episode
end
},
{
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)%s*[_%.%s]%s*[eEpP]+[_%-%.%s]?(%d+%.?%d*)",
format = function(name, year, episode)
return clean_name(name) .. " (" .. year .. ") E" .. episode
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*[sS](%d+)[%.%-%s:]?[eE](%d+[%.v]?%d*)%s*[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
format = function(name, season, episode, year)
return clean_name(name) .. " (" .. year .. ") S" .. season .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*[sS](%d+)[%.%-%s:]?[eE](%d+%.?%d*)",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode
end
},
{
regex = "^(.-)%s*[_%.%s]%s*(%d+)[nrdsth]+[_%.%s]%s*[sS]eason[_%.%s]%s*%[(%d+[%.v]?%d*)%]",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[^dD][eEpP]+[_%-%.%s]?(%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*)",
format = function(name, episode)
return clean_name(name) .. " E" .. episode
end
},
{
regex = "^(.-)%s*第%s*(%d+[%.v]?%d*)%s*[话集回]",
format = function(name, episode)
return clean_name(name) .. " E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*%[(%d+[%.v]?%d*)%]",
format = function(name, episode)
return clean_name(name) .. " E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*%[(%d+[%.v]?%d*)%(%a+%)%]",
format = function(name, episode)
return clean_name(name) .. " E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[%-#]%s*(%d+%.?%d*)%s*",
format = function(name, episode)
return clean_name(name) .. " E" .. episode
end
},
{
regex = "^(.-)%s*[%[%(]([OVADSPs]+)[%]%)]",
format = function(name, sp)
return clean_name(name) .. " [" .. sp .. "]"
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*(%d?%d)x(%d%d?%d?%d?)[^%dhHxXvVpPkKxXbBfF]",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode
end
},
{
regex = "^%((%d%d%d%d)%.?%d?%d?%.?%d?%d?%)%s*(.-)%s*[%(%[]",
format = function(year, name)
return clean_name(name) .. " (" .. year .. ")"
end
},
{
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
format = function(name, year)
return clean_name(name) .. " (" .. year .. ")"
end
},
{
regex = "^%[.-%]%s*%[?(.-)%]?%s*[%(%[]",
format = function(name)
return clean_name(name)
end
},
}
-- Format filename based on regex patterns
function format_filename(title)
for _, formatter in ipairs(formatters) do
local matches = {title:match(formatter.regex)}
if #matches > 0 then
title = formatter.format(unpack(matches))
return title
end
end
end
+192
View File
@@ -0,0 +1,192 @@
--[[
sha256 -- public domain Lua SHA-256 implementation
no warranty implied; use at your own risk
author: dyphire
COMPATIBILITY
Lua 5.1+, LuaJIT
LICENSE: MIT License
--]]
local unpack = unpack or table.unpack
local function band(a,b)
local res = 0
local bit = 1
for i = 0,31 do
local aa = a % 2
local bb = b % 2
if aa == 1 and bb == 1 then
res = res + bit
end
a = (a - aa) / 2
b = (b - bb) / 2
bit = bit * 2
end
return res
end
local function bor(a,b)
local res = 0
local bit = 1
for i = 0,31 do
local aa = a % 2
local bb = b % 2
if aa == 1 or bb == 1 then
res = res + bit
end
a = (a - aa) / 2
b = (b - bb) / 2
bit = bit * 2
end
return res
end
local function bxor(a,b)
local res = 0
local bit = 1
for i = 0,31 do
local aa = a % 2
local bb = b % 2
if (aa + bb) == 1 then
res = res + bit
end
a = (a - aa) / 2
b = (b - bb) / 2
bit = bit * 2
end
return res
end
local function bnot(a)
return 0xFFFFFFFF - a
end
local function lshift(a,n)
return (a * 2^n) % 2^32
end
local function rshift(a,n)
return math.floor(a / 2^n) % 2^32
end
local function bit_ror(x, n)
return bor(rshift(x, n), lshift(x, 32 - n))
end
local function sha256(message)
local k = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
}
local function preprocess(msg)
local len = #msg
local bitLen = len * 8
msg = msg .. "\128"
local zeroPad = 64 - ((len + 9) % 64)
if zeroPad ~= 64 then
msg = msg .. string.rep("\0", zeroPad)
end
msg = msg .. string.char(
rshift(bitLen, 56) % 256,
rshift(bitLen, 48) % 256,
rshift(bitLen, 40) % 256,
rshift(bitLen, 32) % 256,
rshift(bitLen, 24) % 256,
rshift(bitLen, 16) % 256,
rshift(bitLen, 8) % 256,
bitLen % 256
)
return msg
end
local function chunkify(msg)
local chunks = {}
for i = 1, #msg, 64 do
table.insert(chunks, msg:sub(i, i + 63))
end
return chunks
end
local function processChunk(chunk, hash)
local w = {}
for i = 1, 64 do
if i <= 16 then
w[i] = lshift(string.byte(chunk, (i - 1) * 4 + 1), 24) +
lshift(string.byte(chunk, (i - 1) * 4 + 2), 16) +
lshift(string.byte(chunk, (i - 1) * 4 + 3), 8) +
string.byte(chunk, (i - 1) * 4 + 4)
else
local s0 = bxor(bxor(bit_ror(w[i - 15], 7), bit_ror(w[i - 15], 18)), rshift(w[i - 15], 3))
local s1 = bxor(bxor(bit_ror(w[i - 2], 17), bit_ror(w[i - 2], 19)), rshift(w[i - 2], 10))
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) % 2^32
end
end
local a, b, c, d, e, f, g, h = unpack(hash)
for i = 1, 64 do
local s1 = bxor(bxor(bit_ror(e, 6), bit_ror(e, 11)), bit_ror(e, 25))
local ch = bxor(band(e, f), band(bnot(e), g))
local temp1 = (h + s1 + ch + k[i] + w[i]) % 2^32
local s0 = bxor(bxor(bit_ror(a, 2), bit_ror(a, 13)), bit_ror(a, 22))
local maj = bxor(bxor(band(a, b), band(a, c)), band(b, c))
local temp2 = (s0 + maj) % 2^32
h = g
g = f
f = e
e = (d + temp1) % 2^32
d = c
c = b
b = a
a = (temp1 + temp2) % 2^32
end
return
(hash[1] + a) % 2^32,
(hash[2] + b) % 2^32,
(hash[3] + c) % 2^32,
(hash[4] + d) % 2^32,
(hash[5] + e) % 2^32,
(hash[6] + f) % 2^32,
(hash[7] + g) % 2^32,
(hash[8] + h) % 2^32
end
message = preprocess(message)
local chunks = chunkify(message)
local hash = {
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
}
for _, chunk in ipairs(chunks) do
hash = {processChunk(chunk, hash)}
end
local result = ""
for _, h in ipairs(hash) do
result = result .. string.format("%08x", h)
end
return result
end
return sha256
+164
View File
@@ -0,0 +1,164 @@
-- taken from https://github.com/rkscv/danmaku/blob/main/danmaku.lua
-- modified from https://bitop.luajit.org/download.html (LuaBitOp-1.0.2 / md5test.lua)
-- and https://github.com/kikito/md5.lua/blob/master/md5.lua
-- SPDX-License-Identifier:MIT
local byte, char, sub, rep = string.byte, string.char, string.sub, string.rep
local tobit, tohex, bnot, bor, band, bxor, lshift, rshift, rol, bswap
if _G.bit then --LuaJIT
tobit, tohex = _G.bit.tobit or _G.bit.cast, _G.bit.tohex
bnot, bor, band, bxor, lshift, rshift = _G.bit.bnot, _G.bit.bor, _G.bit.band, _G.bit.bxor, _G.bit.lshift, _G.bit.rshift
rol, bswap = _G.bit.rol, _G.bit.bswap
elseif _G.bit32 then --Lua 5.2
local bit32_bnot = _G.bit32.bnot
tobit = function(a) return a <= 0x7fffffff and a or -(_G.bit32.bnot(a) + 1) end
bnot = function(a) return tobit(bit32_bnot(tobit(a))) end
bor, band, bxor, lshift, rshift, rol = _G.bit32.bor, _G.bit32.band, _G.bit32.bxor, _G.bit32.lshift, _G.bit32.rshift, _G.bit32.lrotate
else
return nil
end
if not tohex then
tohex = function(a) return string.sub(string.format('%08x', a), -8) end
end
if not bswap then
bswap = function(a)
return bor(rshift(a, 24), band(rshift(a, 8), 0xff00), lshift(band(a, 0xff00), 8), lshift(a, 24))
end
end
local function tr_f(a, b, c, d, x, s) return rol(bxor(d, band(b, bxor(c, d))) + a + x, s) + b end
local function tr_g(a, b, c, d, x, s) return rol(bxor(c, band(d, bxor(b, c))) + a + x, s) + b end
local function tr_h(a, b, c, d, x, s) return rol(bxor(b, c, d) + a + x, s) + b end
local function tr_i(a, b, c, d, x, s) return rol(bxor(c, bor(b, bnot(d))) + a + x, s) + b end
local function transform(x, a1, b1, c1, d1)
local a, b, c, d = a1, b1, c1, d1
a = tr_f(a, b, c, d, x[1] + 0xd76aa478, 7)
d = tr_f(d, a, b, c, x[2] + 0xe8c7b756, 12)
c = tr_f(c, d, a, b, x[3] + 0x242070db, 17)
b = tr_f(b, c, d, a, x[4] + 0xc1bdceee, 22)
a = tr_f(a, b, c, d, x[5] + 0xf57c0faf, 7)
d = tr_f(d, a, b, c, x[6] + 0x4787c62a, 12)
c = tr_f(c, d, a, b, x[7] + 0xa8304613, 17)
b = tr_f(b, c, d, a, x[8] + 0xfd469501, 22)
a = tr_f(a, b, c, d, x[9] + 0x698098d8, 7)
d = tr_f(d, a, b, c, x[10] + 0x8b44f7af, 12)
c = tr_f(c, d, a, b, x[11] + 0xffff5bb1, 17)
b = tr_f(b, c, d, a, x[12] + 0x895cd7be, 22)
a = tr_f(a, b, c, d, x[13] + 0x6b901122, 7)
d = tr_f(d, a, b, c, x[14] + 0xfd987193, 12)
c = tr_f(c, d, a, b, x[15] + 0xa679438e, 17)
b = tr_f(b, c, d, a, x[16] + 0x49b40821, 22)
a = tr_g(a, b, c, d, x[2] + 0xf61e2562, 5)
d = tr_g(d, a, b, c, x[7] + 0xc040b340, 9)
c = tr_g(c, d, a, b, x[12] + 0x265e5a51, 14)
b = tr_g(b, c, d, a, x[1] + 0xe9b6c7aa, 20)
a = tr_g(a, b, c, d, x[6] + 0xd62f105d, 5)
d = tr_g(d, a, b, c, x[11] + 0x02441453, 9)
c = tr_g(c, d, a, b, x[16] + 0xd8a1e681, 14)
b = tr_g(b, c, d, a, x[5] + 0xe7d3fbc8, 20)
a = tr_g(a, b, c, d, x[10] + 0x21e1cde6, 5)
d = tr_g(d, a, b, c, x[15] + 0xc33707d6, 9)
c = tr_g(c, d, a, b, x[4] + 0xf4d50d87, 14)
b = tr_g(b, c, d, a, x[9] + 0x455a14ed, 20)
a = tr_g(a, b, c, d, x[14] + 0xa9e3e905, 5)
d = tr_g(d, a, b, c, x[3] + 0xfcefa3f8, 9)
c = tr_g(c, d, a, b, x[8] + 0x676f02d9, 14)
b = tr_g(b, c, d, a, x[13] + 0x8d2a4c8a, 20)
a = tr_h(a, b, c, d, x[6] + 0xfffa3942, 4)
d = tr_h(d, a, b, c, x[9] + 0x8771f681, 11)
c = tr_h(c, d, a, b, x[12] + 0x6d9d6122, 16)
b = tr_h(b, c, d, a, x[15] + 0xfde5380c, 23)
a = tr_h(a, b, c, d, x[2] + 0xa4beea44, 4)
d = tr_h(d, a, b, c, x[5] + 0x4bdecfa9, 11)
c = tr_h(c, d, a, b, x[8] + 0xf6bb4b60, 16)
b = tr_h(b, c, d, a, x[11] + 0xbebfbc70, 23)
a = tr_h(a, b, c, d, x[14] + 0x289b7ec6, 4)
d = tr_h(d, a, b, c, x[1] + 0xeaa127fa, 11)
c = tr_h(c, d, a, b, x[4] + 0xd4ef3085, 16)
b = tr_h(b, c, d, a, x[7] + 0x04881d05, 23)
a = tr_h(a, b, c, d, x[10] + 0xd9d4d039, 4)
d = tr_h(d, a, b, c, x[13] + 0xe6db99e5, 11)
c = tr_h(c, d, a, b, x[16] + 0x1fa27cf8, 16)
b = tr_h(b, c, d, a, x[3] + 0xc4ac5665, 23)
a = tr_i(a, b, c, d, x[1] + 0xf4292244, 6)
d = tr_i(d, a, b, c, x[8] + 0x432aff97, 10)
c = tr_i(c, d, a, b, x[15] + 0xab9423a7, 15)
b = tr_i(b, c, d, a, x[6] + 0xfc93a039, 21)
a = tr_i(a, b, c, d, x[13] + 0x655b59c3, 6)
d = tr_i(d, a, b, c, x[4] + 0x8f0ccc92, 10)
c = tr_i(c, d, a, b, x[11] + 0xffeff47d, 15)
b = tr_i(b, c, d, a, x[2] + 0x85845dd1, 21)
a = tr_i(a, b, c, d, x[9] + 0x6fa87e4f, 6)
d = tr_i(d, a, b, c, x[16] + 0xfe2ce6e0, 10)
c = tr_i(c, d, a, b, x[7] + 0xa3014314, 15)
b = tr_i(b, c, d, a, x[14] + 0x4e0811a1, 21)
a = tr_i(a, b, c, d, x[5] + 0xf7537e82, 6)
d = tr_i(d, a, b, c, x[12] + 0xbd3af235, 10)
c = tr_i(c, d, a, b, x[3] + 0x2ad7d2bb, 15)
b = tr_i(b, c, d, a, x[10] + 0xeb86d391, 21)
return tobit(a + a1), tobit(b + b1), tobit(c + c1), tobit(d + d1)
end
local function md5_update(self, s)
local m, len = s, #s
if len % 4 ~= 0 then
m = m .. '\128' .. rep('\0', 63 - band(len + 8, 63)) ..
char(band(lshift(len, 3), 255), band(rshift(len, 5), 255), band(rshift(len, 13), 255),
band(rshift(len, 21), 255)) .. '\0\0\0\0'
end
local a, b, c, d = self.a, self.b, self.c, self.d
local x, k = self.x, self.k
for i = 1, #m, 4 do
local m0, m1, m2, m3 = byte(m, i, i + 3)
x[k] = bor(m0, lshift(m1, 8), lshift(m2, 16), lshift(m3, 24))
if k == 16 then
a, b, c, d = transform(x, a, b, c, d)
k = 1
else
k = k + 1
end
end
self.a, self.b, self.c, self.d, self.k = a, b, c, d, k
self.len = self.len + len
return self
end
local function md5_finish(self)
local len = self.len
if len % 4 == 0 then
local s = '\128' .. rep('\0', 63 - band(len + 8, 63)) ..
char(band(lshift(len, 3), 255), band(rshift(len, 5), 255), band(rshift(len, 13), 255),
band(rshift(len, 21), 255)) .. '\0\0\0\0'
md5_update(self, s)
end
return tohex(bswap(self.a)) .. tohex(bswap(self.b)) .. tohex(bswap(self.c)) .. tohex(bswap(self.d))
end
local md5 = {}
function md5.new()
return {
a = 0x67452301,
b = 0xefcdab89,
c = 0x98badcfe,
d = 0x10325476,
x = {},
k = 1,
len = 0,
update = md5_update,
finish = md5_finish,
}
end
function md5.sum(s)
return md5.new():update(s):finish()
end
return md5
+806
View File
@@ -0,0 +1,806 @@
local msg = require('mp.msg')
local utils = require("mp.utils")
input_loaded, input = pcall(require, "mp.input")
uosc_available = false
-- 打开番剧数据匹配菜单
function get_animes(query)
local encoded_query = url_encode(query)
local url = options.api_server .. "/api/v2/search/anime"
local params = "keyword=" .. encoded_query
local full_url = url .. "?" .. params
local items = {}
local message = "加载数据中..."
local menu_type = "menu_anime"
local menu_title = "在此处输入番剧名称"
local footnote = "使用enter或ctrl+enter进行搜索"
local menu_cmd = { "script-message-to", mp.get_script_name(), "search-anime-event" }
if uosc_available then
update_menu_uosc(menu_type, menu_title, message, footnote, menu_cmd, query)
else
show_message(message, 30)
end
msg.verbose("尝试获取番剧数据:" .. full_url)
local args = make_danmaku_request_args("GET", full_url)
if args == nil then
return
end
local res = mp.command_native({ name = 'subprocess', capture_stdout = true, capture_stderr = true, args = args })
if not res.status or res.status ~= 0 then
local message = "获取数据失败"
if uosc_available then
update_menu_uosc(menu_type, menu_title, message, footnote, menu_cmd, query)
else
show_message(message, 3)
end
msg.error("HTTP 请求失败:" .. res.stderr)
end
local response = utils.parse_json(res.stdout)
if not response or not response.animes then
local message = "无结果"
if uosc_available then
update_menu_uosc(menu_type, menu_title, message, footnote, menu_cmd, query)
else
show_message(message, 3)
end
msg.info("无结果")
return
end
for _, anime in ipairs(response.animes) do
table.insert(items, {
title = anime.animeTitle,
hint = anime.typeDescription,
value = {
"script-message-to",
mp.get_script_name(),
"search-episodes-event",
anime.animeTitle, anime.bangumiId,
},
})
end
if uosc_available then
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()
open_menu_select(items)
end)
end
end
function get_episodes(animeTitle, bangumiId)
local url = options.api_server .. "/api/v2/bangumi/" .. bangumiId
local items = {}
local message = "加载数据中..."
local menu_type = "menu_episodes"
local menu_title = "剧集信息"
local footnote = "使用 / 打开筛选"
if uosc_available then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 30)
end
local args = make_danmaku_request_args("GET", url)
if args == nil then
return
end
local res = mp.command_native({ name = 'subprocess', capture_stdout = true, capture_stderr = true, args = args })
if not res.status or res.status ~= 0 then
local message = "获取数据失败"
if uosc_available then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 3)
end
msg.error("HTTP 请求失败:" .. res.stderr)
end
local response = utils.parse_json(res.stdout)
if not response or not response.bangumi or not response.bangumi.episodes then
local message = "无结果"
if uosc_available then
update_menu_uosc(menu_type, menu_title, message, footnote)
else
show_message(message, 3)
end
msg.info("无结果")
return
end
for _, episode in ipairs(response.bangumi.episodes) do
table.insert(items, {
title = episode.episodeTitle,
hint = episode.episodeNumber,
value = { "script-message-to", mp.get_script_name(), "load-danmaku",
animeTitle, episode.episodeTitle, episode.episodeId },
keep_open = false,
selectable = true,
})
end
if uosc_available then
update_menu_uosc(menu_type, menu_title, items, footnote)
elseif input_loaded then
mp.add_timeout(0.1, function()
open_menu_select(items)
end)
end
end
function update_menu_uosc(menu_type, menu_title, menu_item, menu_footnote, menu_cmd, query)
local items = {}
if type(menu_item) == "string" then
table.insert(items, {
title = menu_item,
value = "",
italic = true,
keep_open = true,
selectable = false,
align = "center",
})
else
items = menu_item
end
local menu_props = {
type = menu_type,
title = menu_title,
search_style = menu_cmd and "palette" or "on_demand",
search_debounce = menu_cmd and "submit" or 0,
on_search = menu_cmd,
footnote = menu_footnote,
search_suggestion = query,
items = items,
}
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
end
function open_menu_select(menu_items, is_time)
local item_titles, item_values = {}, {}
for i, v in ipairs(menu_items) do
item_titles[i] = is_time and "[" .. v.hint .. "] " .. v.title or
(v.hint and v.title .. " (" .. v.hint .. ")" or v.title)
item_values[i] = v.value
end
mp.commandv('script-message-to', 'console', 'disable')
input.select({
prompt = '筛选:',
items = item_titles,
submit = function(id)
mp.commandv(unpack(item_values[id]))
end,
})
end
-- 打开弹幕输入搜索菜单
function open_input_menu_get()
mp.commandv('script-message-to', 'console', 'disable')
local title = parse_title()
input.get({
prompt = '番剧名称:',
default_text = title,
cursor_position = title and #title + 1,
submit = function(text)
input.terminate()
mp.commandv("script-message-to", mp.get_script_name(), "search-anime-event", text)
end
})
end
function open_input_menu_uosc()
local items = {}
if DANMAKU.anime and DANMAKU.episode then
local episode = DANMAKU.episode:gsub("%s.-$","")
episode = episode:match("^(第.*[话回集]+)%s*") or episode
items[#items + 1] = {
title = string.format("已关联弹幕:%s-%s", DANMAKU.anime, episode),
bold = true,
italic = true,
keep_open = true,
selectable = false,
}
end
items[#items + 1] = {
hint = " 追加|ds或|dy或|dm可搜索电视剧|电影|国漫",
keep_open = true,
selectable = false,
}
local menu_props = {
type = "menu_danmaku",
title = "在此处输入番剧名称",
search_style = "palette",
search_debounce = "submit",
search_suggestion = parse_title(),
on_search = { "script-message-to", mp.get_script_name(), "search-anime-event" },
footnote = "使用enter或ctrl+enter进行搜索",
items = items
}
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
end
function open_input_menu()
if uosc_available then
open_input_menu_uosc()
elseif input_loaded then
open_input_menu_get()
end
end
-- 打开弹幕源添加管理菜单
function open_add_menu_get()
mp.commandv('script-message-to', 'console', 'disable')
input.get({
prompt = 'Input url:',
submit = function(text)
input.terminate()
mp.commandv("script-message-to", mp.get_script_name(), "add-source-event", text)
end
})
end
function open_add_menu_uosc()
local sources = {}
for url, source in pairs(DANMAKU.sources) do
if source.fname 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"},}
else
item.hint = "来源:弹幕服务器(未屏蔽)"
item.actions = {{icon = "not_interested", name = "block"},}
end
else
item.hint = "来源:用户添加"
item.actions = {{icon = "delete", name = "delete"},}
end
table.insert(sources, item)
end
end
local menu_props = {
type = "menu_source",
title = "在此输入源地址url",
search_style = "palette",
search_debounce = "submit",
on_search = { "script-message-to", mp.get_script_name(), "add-source-event" },
footnote = "使用enter或ctrl+enter进行添加",
items = sources,
item_actions_place = "outside",
callback = {mp.get_script_name(), 'setup-danmaku-source'},
}
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
end
function open_add_menu()
if uosc_available then
open_add_menu_uosc()
elseif input_loaded then
open_add_menu_get()
end
end
-- 打开弹幕内容菜单
function open_content_menu(pos)
local items = {}
local time_pos = pos or mp.get_property_native("time-pos")
local duration = mp.get_property_number("duration", 0)
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
if text and text ~= "" and start_time >= 0 and start_time <= duration then
table.insert(items, {
title = abbr_str(text, 60),
hint = seconds_to_time(start_time),
value = { "seek", start_time, "absolute" },
active = time_pos >= start_time and time_pos <= end_time,
})
end
end
end
local menu_props = {
type = "menu_content",
title = "弹幕内容",
footnote = "使用 / 打开搜索",
items = items
}
local json_props = utils.format_json(menu_props)
if uosc_available then
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
elseif input_loaded then
open_menu_select(items, true)
end
end
local menu_items_config = {
bold = { title = "粗体", hint = options.bold, original = options.bold,
footnote = "true / false", },
fontsize = { title = "大小", hint = options.fontsize, original = options.fontsize,
scope = { min = 0, max = math.huge }, footnote = "请输入整数(>=0)", },
outline = { title = "描边", hint = options.outline, original = options.outline,
scope = { min = 0.0, max = 4.0 }, footnote = "输入范围:(0.0-4.0)" },
shadow = { title = "阴影", hint = options.shadow, original = options.shadow,
scope = { min = 0, max = math.huge }, footnote = "请输入整数(>=0)", },
scrolltime = { title = "速度", hint = options.scrolltime, original = options.scrolltime,
scope = { min = 1, max = math.huge }, footnote = "请输入整数(>=1)", },
opacity = { title = "透明度", hint = options.opacity, original = options.opacity,
scope = { min = 0, max = 1 }, footnote = "输入范围0完全透明到1不透明", },
displayarea = { title = "弹幕显示范围", hint = options.displayarea, original = options.displayarea,
scope = { min = 0.0, max = 1.0 }, footnote = "显示范围(0.0-1.0)", },
}
-- 创建一个包含键顺序的表,这是样式菜单的排布顺序
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
end
local items = {}
for _, key in ipairs(ordered_keys) do
local config = menu_items_config[key]
local item_config = {
title = config.title,
hint = "目前:" .. tostring(config.hint),
active = key == actived,
keep_open = true,
selectable = true,
}
if config.hint ~= config.original then
local original_str = tostring(config.original)
item_config.actions = {{icon = "refresh", name = key, label = "恢复默认配置 < " .. original_str .. " >"}}
end
table.insert(items, item_config)
end
local menu_props = {
type = "menu_style",
title = "弹幕样式",
search_style = "disabled",
footnote = "样式更改仅在本次播放生效",
item_actions_place = "outside",
items = items,
callback = { mp.get_script_name(), 'setup-danmaku-style'},
}
local actions = "open-menu"
if status ~= nil then
-- msg.info(status)
if status == "updata" then
-- "updata" 模式会保留输入框文字
menu_props.title = " " .. menu_items_config[actived]["footnote"]
actions = "update-menu"
elseif status == "refresh" then
-- "refresh" 模式会清除输入框文字
menu_props.title = " " .. menu_items_config[actived]["footnote"]
elseif status == "error" then
menu_props.title = "输入非数字字符或范围出错"
-- 创建一个定时器在1秒后触发回调函数删除搜索栏错误信息
mp.add_timeout(1.0, function() add_danmaku_setup(actived, "updata") end)
end
menu_props.search_style = "palette"
menu_props.search_debounce = "submit"
menu_props.footnote = menu_items_config[actived]["footnote"] or ""
menu_props.on_search = { "script-message-to", mp.get_script_name(), "setup-danmaku-style", actived }
end
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", actions, json_props)
end
-- 设置弹幕源延迟菜单
function danmaku_delay_setup(source_url)
if not uosc_available then
show_message("无uosc UI框架不支持使用该功能", 2)
return
end
local sources = {}
for url, source in pairs(DANMAKU.sources) do
if source.fname and not source.blocked then
local delay = 0
if source.delay_segments then
for _, seg in ipairs(source.delay_segments) do
if seg.start == 0 then
delay = seg.delay or 0
break
end
end
end
local item = {title = url, value = url, keep_open = true,}
item.hint = "当前弹幕源延迟:" .. string.format("%.1f", delay + 1e-10) .. ""
item.active = url == source_url
table.insert(sources, item)
end
end
local menu_props = {
type = "menu_delay",
title = "弹幕源延迟设置",
search_style = "disabled",
items = sources,
callback = {mp.get_script_name(), 'setup-source-delay'},
}
if source_url ~= nil then
menu_props.title = "请输入数字,单位(秒)/ 或者按照形如\"14m15s\"的格式输入分钟数加秒数"
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 }
end
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
end
-- 总集合弹幕菜单
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.-$","")
episode = episode:match("^(第.*[话回集]+)%s*") or episode
items[#items + 1] = {
title = string.format("已关联弹幕:%s-%s", DANMAKU.anime, episode),
bold = true,
italic = true,
keep_open = true,
selectable = false,
}
end
for _, config in ipairs(total_menu_items_config) do
table.insert(items, {
title = config.title,
value = { "script-message-to", mp.get_script_name(), config.action },
keep_open = false,
selectable = true,
})
end
local menu_props = {
type = "menu_total",
title = "弹幕设置",
search_style = "disabled",
items = items,
}
local json_props = utils.format_json(menu_props)
mp.commandv("script-message-to", "uosc", "open-menu", json_props)
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 }
end
mp.commandv('script-message-to', 'console', 'disable')
input.select({
prompt = '选择:',
items = item_titles,
submit = function(id)
mp.commandv(unpack(item_values[id]))
end,
})
end
function open_add_total_menu()
if uosc_available then
open_add_total_menu_uosc()
elseif input_loaded then
open_add_total_menu_select()
end
end
-- 添加 uosc 菜单栏按钮
mp.commandv(
"script-message-to",
"uosc",
"set-button",
"danmaku",
utils.format_json({
icon = "search",
tooltip = "弹幕搜索",
command = "script-message open_search_danmaku_menu",
})
)
mp.commandv(
"script-message-to",
"uosc",
"set-button",
"danmaku_source",
utils.format_json({
icon = "add_box",
tooltip = "从源添加弹幕",
command = "script-message open_add_source_menu",
})
)
mp.commandv(
"script-message-to",
"uosc",
"set-button",
"danmaku_styles",
utils.format_json({
icon = "palette",
tooltip = "弹幕样式",
command = "script-message open_setup_danmaku_menu",
})
)
mp.commandv(
"script-message-to",
"uosc",
"set-button",
"danmaku_delay",
utils.format_json({
icon = "more_time",
tooltip = "弹幕源延迟设置",
command = "script-message open_source_delay_menu",
})
)
mp.commandv(
"script-message-to",
"uosc",
"set-button",
"danmaku_menu",
utils.format_json({
icon = "grid_view",
tooltip = "弹幕设置",
command = "script-message open_add_total_menu",
})
)
mp.register_script_message('uosc-version', function()
uosc_available = true
end)
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "off")
mp.register_script_message("set", function(prop, value)
if prop ~= "show_danmaku" then
return
end
if value == "on" then
ENABLED = true
set_danmaku_visibility(true)
if COMMENTS == nil then
local path = mp.get_property("path")
init(path)
else
show_loaded()
show_danmaku_func()
end
else
show_message("关闭弹幕", 2)
ENABLED = false
set_danmaku_visibility(false)
hide_danmaku_func()
end
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", value)
end)
-- 注册函数给 uosc 按钮使用
mp.register_script_message("search-anime-event", function(query)
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_danmaku")
end
local name, class = query:match("^(.-)%s*|%s*(.-)%s*$")
if name and class then
query_extra(name, class)
else
get_animes(query)
end
end)
mp.register_script_message("search-episodes-event", function(animeTitle, bangumiId)
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_anime")
end
get_episodes(animeTitle, bangumiId)
end)
-- Register script message to show the input menu
mp.register_script_message("load-danmaku", function(animeTitle, episodeTitle, episodeId)
ENABLED = true
DANMAKU.anime = animeTitle
DANMAKU.episode = episodeTitle
set_episode_id(episodeId, true)
end)
mp.register_script_message("add-source-event", function(query)
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_source")
end
ENABLED = true
add_danmaku_source(query, true)
end)
mp.register_script_message("open_setup_danmaku_menu", function()
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_total")
end
add_danmaku_setup()
end)
mp.register_script_message("open_content_danmaku_menu", function()
if uosc_available then
mp.commandv("script-message-to", "uosc", "close-menu", "menu_total")
end
open_content_menu()
end)
mp.register_script_message("setup-danmaku-style", function(query, text)
local event = utils.parse_json(query)
if event ~= nil then
-- item点击 或 图标点击
if event.type == "activate" then
if not event.action then
if ordered_keys[event.index] == "bold" then
options.bold = not options.bold
menu_items_config.bold.hint = options.bold and "true" or "false"
end
-- "updata" 模式会保留输入框文字
add_danmaku_setup(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")
if event.action == "fontsize" or event.action == "scrolltime" then
load_danmaku(true)
end
end
end
else
-- 数值输入
if text == nil or text == "" then
return
end
local newText, _ = text:gsub("%s", "") -- 移除所有空白字符
if tonumber(newText) ~= nil and menu_items_config[query]["scope"] ~= nil then
local num = tonumber(newText)
local min_num = menu_items_config[query]["scope"]["min"]
local max_num = menu_items_config[query]["scope"]["max"]
if num and min_num <= num and num <= max_num then
if string.match(menu_items_config[query]["footnote"], "整数") then
-- 输入范围为整数时向下取整
num = tostring(math.floor(num))
end
options[query] = tostring(num)
menu_items_config[query]["hint"] = options[query]
-- "refresh" 模式会清除输入框文字
add_danmaku_setup(query, "refresh")
if query == "fontsize" or query == "scrolltime" then
load_danmaku(true, true)
end
return
end
end
add_danmaku_setup(query, "error")
end
end)
mp.register_script_message('setup-danmaku-source', function(json)
local event = utils.parse_json(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()
load_danmaku(true)
end
if event.action == "block" then
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()
load_danmaku(true)
end
if event.action == "unblock" then
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()
load_danmaku(true)
end
end
end)
mp.register_script_message("setup-source-delay", function(query, text)
local event = utils.parse_json(query)
if event ~= nil then
-- item点击
if event.type == "activate" then
danmaku_delay_setup(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
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
end)
+76
View File
@@ -0,0 +1,76 @@
local opt = require("mp.options")
-- 选项
options = {
-- 指定弹幕服务器地址,自定义服务需兼容 dandanplay 的 api
api_server = "https://api.dandanplay.net",
-- 指定 b 站和爱腾优的弹幕获取的兜底服务器地址,主要用于获取非动画弹幕
-- 服务器可以自托管https://github.com/lyz05/danmaku
fallback_server = "https://fc.lyz05.cn",
-- 设置 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 = "",
-- 使用 fps 视频滤镜,大幅提升弹幕平滑度。默认禁用
vf_fps = false,
-- 设置要使用的 fps 滤镜参数
fps = "60/1.001",
-- 指定合并重复弹幕的时间间隔的容差值,单位为秒。默认值: -1表示禁用
merge_tolerance = -1,
-- 指定弹幕关联历史记录文件的路径,支持绝对路径和相对路径
history_path = "~~/danmaku-history.json",
open_search_danmaku_menu_key = "Ctrl+d",
show_danmaku_keyboard_key = "j",
-- 中文简繁转换。0-不转换1-转换为简体2-转换为繁体
chConvert = 0,
--滚动弹幕的显示时间
scrolltime = 15,
--固定弹幕的显示时间
fixtime = 5,
--字体
fontname = "sans-serif",
--字体大小
fontsize = 50,
--字体阴影
shadow = 0,
--字体粗体
bold = true,
-- 透明度0完全透明到 1不透明
opacity = 0.7,
--全部弹幕的显示范围(0.0-1.0)
displayarea = 0.85,
--描边 0-4
outline = 1.0,
-- 限制屏幕中同时显示的最大弹幕数量0 表示不限制
max_screen_danmaku = 0,
--指定弹幕屏蔽词文件路径(black.txt),支持绝对路径和相对路径。文件内容以换行分隔
--支持 lua 的正则表达式写法
blacklist_path = "",
--指定脚本相关消息显示的消息的对齐方式
message_anlignment = 7,
--指定脚本相关消息显示的消息的x轴坐标
message_x = 30,
--指定脚本相关消息显示的消息的y轴坐标
message_y = 30,
-- 自定义标题解析中的额外替换规则,内容格式为 JSON 字符串,替换模式为 lua 的 string.gsub 函数
--! 注意:由于 mpv 的 lua 版本限制,自定义规则只支持形如 %n 的捕获组写法,即示例用法,不支持直接替换字符的写法
title_replace = [[
[{
"rules": [{ "^(.-)": "%1"},{ "^.*《(.-)》": "%1" }],
}]
]],
-- 指定哈希匹配中需忽略的共享盘(挂载盘)的路径/目录。支持绝对路径和相对路径,多个路径用逗号分隔
-- 示例:["X:", "Z:", "F:/Download/", "Download"]
excluded_path = [[
[]
]],
}
opt.read_options(options, mp.get_script_name(), function() end)
+655
View File
@@ -0,0 +1,655 @@
local msg = require 'mp.msg'
local utils = require 'mp.utils'
local s2t = require("dicts/s2t_chars")
local t2s = require("dicts/t2s_chars")
local function ass_escape(text)
return text:gsub("\\", "\\\\")
:gsub("{", "\\{")
:gsub("}", "\\}")
:gsub("\n", "\\N")
end
local function xml_unescape(str)
return str:gsub("&quot;", "\"")
:gsub("&apos;", "'")
:gsub("&gt;", ">")
:gsub("&lt;", "<")
:gsub("&amp;", "&")
end
local function decode_html_entities(text)
return text:gsub("&#x([%x]+);", function(hex)
local codepoint = tonumber(hex, 16)
return unicode_to_utf8(codepoint)
end):gsub("&#(%d+);", function(dec)
local codepoint = tonumber(dec, 10)
return unicode_to_utf8(codepoint)
end)
end
-- 加载黑名单模式
local function load_blacklist_patterns(filepath)
local patterns = {}
if not file_exists(filepath) then
return patterns
end
local file = io.open(filepath, "r")
if not file then
msg.error("无法打开黑名单文件: " .. filepath)
return patterns
end
for line in file:lines() do
line = line:match("^%s*(.-)%s*$")
if line ~= "" then
table.insert(patterns, line)
end
end
file:close()
return patterns
end
local blacklist_file = mp.command_native({ "expand-path", options.blacklist_path })
local black_patterns = load_blacklist_patterns(blacklist_file)
-- 检查字符串是否在黑名单中
function is_blacklisted(str, patterns)
for _, pattern in ipairs(patterns) do
local ok, result = pcall(function()
return str:match(pattern)
end)
if ok and result then
return true, pattern
elseif not ok then
-- msg.debug("黑名单规则错误,跳过: " .. pattern .. ",错误信息:" .. result)
end
end
return false
end
-- 简繁转换
local function convert(text, dict)
return text:gsub("[%z\1-\127\194-\244][\128-\191]*", function(c)
return dict[c] or c
end)
end
local function ch_convert(str)
if options.chConvert == 1 then
return convert(str, t2s)
elseif options.chConvert == 2 then
return convert(str, s2t)
end
return str
end
local ch_convert_cache = {}
local ch_cache_keys = {}
local ch_cache_max = 5000
local function ch_convert_cached(text)
if type(text) ~= "string" or text == "" then return text end
local cached = ch_convert_cache[text]
if cached ~= nil then return cached end
local converted = ch_convert(text)
ch_convert_cache[text] = converted
ch_cache_keys[#ch_cache_keys+1] = text
if #ch_cache_keys > ch_cache_max then
local old_key = table.remove(ch_cache_keys, 1)
ch_convert_cache[old_key] = nil
end
return converted
end
-- 合并重复弹幕
local function merge_duplicate_danmaku(danmakus, threshold)
if not threshold or tonumber(threshold) < 0 then return danmakus end
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)
end
local merged = {}
for _, group in pairs(groups) 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
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
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,
}
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
table.sort(merged, function(a, b) return a.time < b.time end)
return merged
end
-- 限制每屏弹幕条数
local function limit_danmaku(danmakus, limit)
if not limit or limit <= 0 then
return danmakus
end
local window = {}
for _, d in ipairs(danmakus) do
for i = #window, 1, -1 do
if window[i].end_time <= d.start_time then
table.remove(window, i)
end
end
if #window < limit then
table.insert(window, d)
else
local max_idx = 1
for i = 2, #window do
if window[i].end_time > window[max_idx].end_time then
max_idx = i
end
end
if window[max_idx].end_time > d.end_time then
window[max_idx].drop = true
window[max_idx] = d
else
d.drop = true
end
end
end
local result = {}
for _, d in ipairs(danmakus) do
if not d.drop then
table.insert(result, d)
end
end
return result
end
-- 解析 XML 弹幕
local function parse_xml_danmaku(xml_string, delay_segments)
local danmakus = {}
for p_attr, text in xml_string:gmatch('<d p="([^"]+)">([^<]+)</d>') do
local params = {}
local i = 1
for val in p_attr:gmatch("([^,]+)") do
params[i] = tonumber(val)
i = i + 1
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,
type = params[2] or 1,
size = params[3] or 25,
color = params[4] or 0xFFFFFF,
text = xml_unescape(text)
})
end
end
table.sort(danmakus, function(a, b) return a.time < b.time end)
return danmakus
end
-- 解析 JSON 弹幕
local function parse_json_danmaku(json_string, delay_segments)
local danmakus = {}
if json_string:sub(1, 3) == "\239\187\191" then
json_string = json_string:sub(4)
end
local json = utils.parse_json(json_string)
if not json or type(json) ~= "table" then
msg.info("JSON 解析失败")
return danmakus
end
for _, entry in ipairs(json) do
local c = entry.c
local text = entry.m or ""
if type(c) == "string" then
local params = {}
local i = 1
for val in c:gmatch("([^,]+)") do
params[i] = tonumber(val)
i = i + 1
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,
color = params[2] or 0xFFFFFF,
type = params[3] or 1,
size = params[4] or 25,
text = text
})
end
end
end
table.sort(danmakus, function(a, b) return a.time < b.time end)
return danmakus
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
local all_danmaku = {}
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)
end
else
msg.info("文件不存在: " .. DANMAKU_PATH)
end
end
if #all_danmaku == 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
end
--# 弹幕数组与布局算法 (Danmaku Array & Layout Algorithms)
local DanmakuArray = {}
DanmakuArray.__index = DanmakuArray
function DanmakuArray:new(res_x, res_y, font_size)
local obj = {
solution_y = res_y,
font_size = font_size,
rows = math.floor(res_y / font_size),
time_length_array = {}
}
for i = 1, obj.rows do
obj.time_length_array[i] = { time = -1, length = 0 }
end
setmetatable(obj, self)
return obj
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 }
end
end
function DanmakuArray:get_time(row)
if row > 0 and row <= self.rows then
return self.time_length_array[row].time
end
return -1
end
function DanmakuArray:get_length(row)
if row > 0 and row <= self.rows then
return self.time_length_array[row].length
end
return 0
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
array:set_time_length(i, appear_time, text_length)
return 1 + (i - 1) * font_size
end
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
if delta_x >= 0 then
if delta_velocity <= 0 then
array:set_time_length(i, appear_time, text_length)
return 1 + (i - 1) * font_size
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
-- 追及发生在屏幕之外,允许放置
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
-- 所有行都被占用,放弃渲染
return nil
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
else
row_start, row_end, row_step = array.rows, 1, -1
end
for i = row_start, row_end, row_step do
local previous_appear_time = array:get_time(i)
if previous_appear_time < 0 then
array:set_time_length(i, appear_time, 0)
return (i - 1) * font_size + 1
else
local delta_time = appear_time - previous_appear_time
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
-- 所有行都被占用,放弃渲染
return nil
end
-- 将弹幕转换为 ASS 格式
function convert_danmaku_to_ass(all_danmaku, danmaku_file)
if #all_danmaku == 0 then
msg.info("弹幕文件为空或解析失败")
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"
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
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
local time = d.type == 1 and math.floor(d.time + 0.5) or d.time
local appear_time = time
local danmaku_type = d.type
local end_time = nil
if danmaku_type >= 1 and danmaku_type <= 3 then
end_time = appear_time + scrolltime
elseif danmaku_type == 5 or danmaku_type == 4 then
end_time = appear_time + fixtime
end
if end_time then
table.insert(pre_events, {start_time = appear_time, end_time = end_time, danmaku = d})
end
end
if options.max_screen_danmaku > 0 then
pre_events = limit_danmaku(pre_events, options.max_screen_danmaku)
end
local ass_events = {}
for _, ev in ipairs(pre_events) do
local d = ev.danmaku
local appear_time = ev.start_time
local danmaku_type = d.type
local text = ass_escape(decode_html_entities(d.text))
:gsub("x(%d+)$", "{\\b1\\i1}x%1")
-- 颜色从十进制转为 BGR Hex
local color = math.max(0, math.min(d.color or 0xFFFFFF, 0xFFFFFF))
local color_hex = string.format("%06X", color)
local r = string.sub(color_hex, 1, 2)
local g = string.sub(color_hex, 3, 4)
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
-- 滚动弹幕 (类型 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
local x2 = -text_length / 2
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)
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)
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)
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)
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
+296
View File
@@ -0,0 +1,296 @@
-- modified from https://github.com/rkscv/danmaku/blob/main/danmaku.lua
local msg = require('mp.msg')
local utils = require("mp.utils")
local INTERVAL = options.vf_fps and 0.01 or 0.001
local osd_width, osd_height, pause = 0, 0, true
-- 提取 \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%.]+).*%)")
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)
else
return string.format("{\\an7}%s", event.text)
end
end
-- 计算移动的时间范围
local duration = event.end_time - event.start_time --mean: options.scrolltime
local progress = (pos - event.start_time - delay) / duration -- 移动进度 [0, 1]
-- 计算当前坐标
local current_x = tonumber(x1 + (x2 - x1) * progress)
local current_y = tonumber(y1 + (y2 - y1) * progress)
-- 移除 \move 标签并应用当前坐标
local clean_text = event.text:gsub("\\move%(.-%)", "")
if current_y > displayarea then return end
if event.style ~= "SP" and event.style ~= "MSG" then
return string.format("{\\pos(%.1f,%.1f)\\an8}%s", current_x, current_y, clean_text)
else
return string.format("{\\pos(%.1f,%.1f)\\an7}%s", current_x, current_y, clean_text)
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 文件")
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 width, height = 1920, 1080
local ratio = osd_width / osd_height
if width / height < ratio then
height = width / ratio
fontsize = options.fontsize - ratio * 2
end
local ass_events = {}
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)
if text then
text = text:gsub("&#%d+;","")
end
if text and text:match("\\fs%d+") then
text = text:gsub("\\fs(%d+)", function(size)
return string.format("\\fs%d", size * 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)
table.insert(ass_events, ass_text)
end
end
overlay.res_x = width
overlay.res_y = height
overlay.data = table.concat(ass_events, '\n')
overlay:update()
end
local timer = mp.add_periodic_timer(INTERVAL, render, true)
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
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()
end
end)
end
local function filter_state(label, name)
local filters = mp.get_property_native("vf")
for _, filter in pairs(filters) do
if filter.label == label or filter.name == name
or filter.params[name] ~= nil then
return true
end
end
return false
end
function show_danmaku_func()
render()
if not pause then
timer:resume()
end
if options.vf_fps then
local display_fps = mp.get_property_number('display-fps')
local video_fps = mp.get_property_number('estimated-vf-fps')
if (display_fps and display_fps < 58) or (video_fps and video_fps > 58) then
return
end
if not filter_state("danmaku", "fps") then
mp.commandv("vf", "append", string.format("@danmaku:fps=fps=%s", options.fps))
end
end
end
function hide_danmaku_func()
timer:kill()
overlay:remove()
if filter_state("danmaku") then
mp.commandv("vf", "remove", "@danmaku")
end
end
local message_overlay = mp.create_osd_overlay('ass-events')
local message_timer = mp.add_timeout(3, function()
message_overlay:remove()
end, true)
function show_message(text, time)
message_timer.timeout = time or 3
message_timer:kill()
message_overlay:remove()
local message = string.format("{\\an%d\\pos(%d,%d)}%s", options.message_anlignment,
options.message_x, options.message_y, text)
local width, height = 1920, 1080
local ratio = osd_width / osd_height
if width / height < ratio then
height = width / ratio
end
message_overlay.res_x = width
message_overlay.res_y = height
message_overlay.data = message
message_overlay:update()
message_timer:resume()
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()
elseif COMMENTS ~= nil then
timer:resume()
end
end
end)
mp.register_event('playback-restart', function(event)
if event.error then
return msg.error(event.error)
end
if ENABLED and COMMENTS ~= nil then
render()
end
end)
mp.add_hook("on_unload", 50, function()
COMMENTS, DELAY = nil, 0
timer:kill()
overlay:remove()
mp.set_property_native(DELAY_PROPERTY, 0)
if filter_state("danmaku") then
mp.commandv("vf", "remove", "@danmaku")
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")
}
if options.save_danmaku and file_exists(files_to_remove.file2) then
save_danmaku(true)
end
for _, file in pairs(files_to_remove) do
if file_exists(file) then
os.remove(file)
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)
+53
View File
@@ -0,0 +1,53 @@
local msg = require('mp.msg')
local utils = require("mp.utils")
local repo = "Tony15246/uosc_danmaku"
local local_version = VERSION or "0.0.0"
local function version_greater(v1, v2)
local function parse(ver)
local a, b, c = ver:match("v?(%d+)%.(%d+)%.(%d+)")
return tonumber(a), tonumber(b), tonumber(c)
end
local a1, a2, a3 = parse(v1)
local b1, b2, b3 = parse(v2)
if a1 ~= b1 then return a1 > b1 end
if a2 ~= b2 then return a2 > b2 end
return a3 > b3
end
local function get_latest_release(repo)
local url = "https://api.github.com/repos/" .. repo .. "/releases/latest"
local cmd = { "curl", "-sL", 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 return nil end
local tag = res.stdout:match([["tag_name"%s*:%s*"([^"]+)"]])
return tag
end
-- 仅检查并提示新版本,不自动下载/覆盖(避免 rm -rf 破坏配置)
function check_for_update()
local latest_version = get_latest_release(repo)
if not latest_version 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 .. ")")
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
+664
View File
@@ -0,0 +1,664 @@
local utils = require("mp.utils")
-- from http://lua-users.org/wiki/LuaUnicode
local UTF8_PATTERN = '[%z\1-\127\194-\244][\128-\191]*'
-- return a substring based on utf8 characters
-- like string.sub, but negative index is not supported
function utf8_sub(s, i, j)
if i > j then
return s
end
local t, idx = {}, 1
for char in s:gmatch(UTF8_PATTERN) do
if idx >= i and idx <= j then
t[#t + 1] = char
end
idx = idx + 1
end
return table.concat(t)
end
function utf8_len(s)
local count = 0
for _ in s:gmatch(UTF8_PATTERN) do
count = count + 1
end
return count
end
function utf8_iter(s)
local iter = s:gmatch(UTF8_PATTERN)
return function()
return iter()
end
end
function utf8_to_table(s)
local t = {}
for ch in utf8_iter(s) do
t[#t + 1] = ch
end
return t
end
-- abbreviate string if it's too long
function abbr_str(str, length)
if not str or str == '' then return '' end
local str_clip = utf8_sub(str, 1, length)
if str ~= str_clip then
return str_clip .. '...'
end
return str
end
function get_str_width(text, font_size)
local width = 0
for i = 1, #text do
local byte = string.byte(text, i)
if byte > 127 then
width = width + 2
else
width = width + 1
end
end
local unicode_width = 0
local i = 1
while i <= #text do
local byte = string.byte(text, i)
local char_len
if byte < 128 then char_len = 1; unicode_width = unicode_width + 1
elseif byte >= 192 and byte < 224 then char_len = 2; unicode_width = unicode_width + 2
elseif byte >= 224 and byte < 240 then char_len = 3; unicode_width = unicode_width + 2
elseif byte >= 240 and byte < 248 then char_len = 4; unicode_width = unicode_width + 2
else char_len = 1; unicode_width = unicode_width + 1
end
i = i + char_len
end
return unicode_width * (font_size / 2)
end
function unicode_to_utf8(unicode)
if unicode < 0x80 then
return string.char(unicode)
else
local byte_count
if unicode < 0x800 then
byte_count = 2
elseif unicode < 0x10000 then
byte_count = 3
elseif unicode < 0x110000 then
byte_count = 4
else
return
end
local res = {}
local shift = 2 ^ 6
local after_shift = unicode
for _ = byte_count, 2, -1 do
local before_shift = after_shift
after_shift = math.floor(before_shift / shift)
table.insert(res, 1, before_shift - after_shift * shift + 0x80)
end
shift = 2 ^ (8 - byte_count)
table.insert(res, 1, after_shift + math.floor(0xFF / shift) * shift)
---@diagnostic disable-next-line: deprecated
return string.char(unpack(res))
end
end
function jaro(s1, s2)
local match_window = math.floor(math.max(#s1, #s2) / 2.0) - 1
local matches1 = {}
local matches2 = {}
local m = 0;
local t = 0;
for i = 0, #s1, 1 do
local start = math.max(0, i - match_window)
local final = math.min(i + match_window + 1, #s2)
for k = start, final, 1 do
if not (matches2[k] or s1[i] ~= s2[k]) then
matches1[i] = true
matches2[k] = true
m = m + 1
break
end
end
end
if m == 0 then
return 0.0
end
local k = 0
for i = 0, #s1, 1 do
if matches1[i] then
while not matches2[k] do
k = k + 1
end
if s1[i] ~= s2[k] then
t = t + 1
end
k = k + 1
end
end
t = t / 2.0
return (m / #s1 + m / #s2 + (m - t) / m) / 3.0
end
function jaro_winkler(s1, s2)
if #s1 + #s2 == 0 then
return 0.0
end
if s1 == s2 then
return 1.0
end
s1 = utf8_to_table(s1)
s2 = utf8_to_table(s2)
local d = jaro(s1, s2)
local p = 0.1
local l = 0;
while (s1[l] == s2[l] and l < 4) do
l = l + 1
end
return d + l * p * (1 - d)
end
-- 从时间字符串转换为秒数
function time_to_seconds(time_str)
local h, m, s = time_str:match("(%d+):(%d+):([%d%.]+)")
return tonumber(h) * 3600 + tonumber(m) * 60 + tonumber(s)
end
-- 从秒数转换为时间字符串
function seconds_to_time(seconds)
local hours = math.floor(seconds / 3600)
local minutes = math.floor((seconds % 3600) / 60)
local secs = math.floor(seconds % 60)
local centiseconds = math.floor((seconds - math.floor(seconds)) * 100)
return string.format("%d:%02d:%02d.%02d", hours, minutes, secs, centiseconds)
end
function is_chinese(str)
return string.match(str, "[\228-\233][\128-\191]") ~= nil
end
function is_protocol(path)
return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
end
function hex_to_bin(hexstr)
return (hexstr:gsub('..', function (cc)
return string.char(tonumber(cc, 16))
end))
end
function hex_to_char(x)
return string.char(tonumber(x, 16))
end
-- url编码转换
function url_encode(str)
-- 将非安全字符转换为百分号编码
if str then
str = str:gsub("([^%w%-%.%_%~])", function(c)
return string.format("%%%02X", string.byte(c))
end)
end
return str
end
-- url解码转换
function url_decode(str)
if str ~= nil then
str = str:gsub('^%a[%a%d-_]+://', '')
:gsub('^%a[%a%d-_]+:\\?', '')
:gsub('%%(%x%x)', hex_to_char)
if str:find('://localhost:?') then
str = str:gsub('^.*/', '')
end
str = str:gsub('%?.+', '')
:gsub('%+', ' ')
return str
end
end
-- Utility function to split a string by a delimiter
function split(str, delim)
local result = {}
for match in (str .. delim):gmatch("(.-)" .. delim) do
table.insert(result, match)
end
return result
end
function table_to_zero_indexed(tbl)
for i = #tbl, 1, -1 do
tbl[i - 1] = tbl[i]
end
tbl[#tbl] = nil
return tbl
end
function itable_index_of(itable, value)
for index = 1, #itable do
if itable[index] == value then
return index
end
end
end
function is_nested_table(t)
if type(t) ~= "table" then
return false
end
for _, v in pairs(t) do
if type(v) == "table" then
return true
end
end
return false
end
function shallow_copy(original)
if type(original) ~= "table" then
return original
end
local copy = {}
for k, v in pairs(original) do
copy[k] = v
end
return copy
end
function deep_copy(obj, seen)
if type(obj) ~= "table" then
return obj
end
if seen and seen[obj] then
return seen[obj]
end
local s = seen or {}
local copy = {}
s[obj] = copy
for k, v in pairs(obj) do
copy[deep_copy(k, s)] = deep_copy(v, s)
end
setmetatable(copy, getmetatable(obj))
return copy
end
function remove_query(url)
local qpos = string.find(url, "?", 1, true)
if qpos then
return string.sub(url, 1, qpos - 1)
else
return url
end
end
function file_exists(path)
if path then
local meta = utils.file_info(path)
return meta and meta.is_file
end
return false
end
function is_writable(path)
local file = io.open(path, "w")
if file then
file:close()
os.remove(path)
return true
end
return false
end
function contains_any(tab, val)
for _, element in pairs(tab) do
if string.find(val, element) then
return true
end
end
return false
end
--读history 和 写history
function read_file(file_path)
local file = io.open(file_path, "r") -- 打开文件,"r"表示只读模式
if not file then
return nil
end
local content = file:read("*all") -- 读取文件所有内容
file:close() -- 关闭文件
return content
end
-- 应用额外的自定义标题替换规则
function title_replace(title)
local title_replace = utils.parse_json(options.title_replace)
if not title_replace then
return title
end
for _, v in pairs(title_replace) do
for _, indexrules in pairs(v['rules']) do
for rule, override in pairs(indexrules) do
title = title:gsub(rule, override)
:gsub("[_%.]", " ")
:gsub("^%s*(.-)%s*$", "%1")
:gsub("[@#%.%+%-%%&*_=,/~`]+$", "")
end
end
end
return title
end
function write_json_file(file_path, data)
local file = io.open(file_path, "w")
if not file then
return
end
file:write(utils.format_json(data)) -- 将 Lua 表转换为 JSON 并写入
file:close()
end
-- 拆分字符串中的字符和数字
local function split_by_numbers(filename)
local parts = {}
local pattern = "([^%d]*)(%d+)([^%d]*)"
for pre, num, post in string.gmatch(filename, pattern) do
table.insert(parts, {pre = pre, num = tonumber(num), post = post})
end
return parts
end
-- 识别并匹配前后剧集
local function compare_filenames(fname1, fname2)
local parts1 = split_by_numbers(fname1)
local parts2 = split_by_numbers(fname2)
local min_len = math.min(#parts1, #parts2)
-- 逐个部分进行比较
for i = 1, min_len do
local part1 = parts1[i]
local part2 = parts2[i]
-- 比较数字前的字符是否相同
if part1.pre ~= part2.pre then
return false
end
-- 比较数字部分
if part1.num ~= part2.num then
return part1.num, part2.num
end
-- 比较数字后的字符是否相同
if part1.post ~= part2.post then
return false
end
end
return false
end
-- 规范化路径
function normalize(path)
if normalize_path ~= nil then
if normalize_path then
path = mp.command_native({"normalize-path", path})
else
local directory = mp.get_property("working-directory", "")
path = utils.join_path(directory, path:gsub('^%.[\\/]',''))
if PLATFORM == "windows" then path = path:gsub("\\", "/") end
end
return path
end
normalize_path = false
local commands = mp.get_property_native("command-list", {})
for _, command in ipairs(commands) do
if command.name == "normalize-path" then
normalize_path = true
break
end
end
return normalize(path)
end
-- 获取父目录路径
function get_parent_directory(path)
local dir = nil
if path and not is_protocol(path) then
path = normalize(path)
dir = utils.split_path(path)
end
return dir
end
-- 获取播放文件标题信息
function parse_title()
local path = mp.get_property("path")
local filename = mp.get_property("filename/no-ext")
if not filename then
return
end
local thin_space = string.char(0xE2, 0x80, 0x89)
filename = filename:gsub(thin_space, " ")
local media_title, season, episode = nil, nil, nil
if path and not is_protocol(path) then
local title = format_filename(filename)
if title then
media_title, season, episode = title:match("^(.-)%s*[sS](%d+)[eE](%d+)")
if season then
return title_replace(media_title), season, episode
else
media_title, episode = title:match("^(.-)%s*[eE](%d+)")
if episode then
return title_replace(media_title), season, episode
end
end
return title_replace(title)
end
local directory = get_parent_directory(path)
local dir, title = utils.split_path(directory:sub(1, -2))
if title:lower():match("^%s*seasons?%s*%d+%s*$") or title:lower():match("^%s*specials?%s*$") or title:match("^%s*SPs?%s*$")
or title:match("^%s*O[VAD]+s?%s*$") or title:match("^%s*第.-[季部]+%s*$") then
directory, title = utils.split_path(dir:sub(1, -2))
end
title = title
:gsub(thin_space, " ")
:gsub("%[.-%]", "")
:gsub("^%s*%(%d+.?%d*.?%d*%)", "")
:gsub("%(%d+.?%d*.?%d*%)%s*$", "")
:gsub("[%._]", " ")
:gsub("^%s*(.-)%s*$", "%1")
return title_replace(title)
end
local title = mp.get_property("media-title")
if title then
title = title:gsub(thin_space, " ")
local ftitle = url_decode(title)
local name, class = ftitle:match("^(.-)%s*|%s*(.-)%s*$")
if name then ftitle = name end
local format_title = format_filename(ftitle)
if format_title then
media_title, season, episode = format_title:match("^(.-)%s*[sS](%d+)[eE](%d+)")
if season then
title = media_title
else
media_title, episode = format_title:match("^(.-)%s*[eE](%d+)")
if episode then
season = 1
title = media_title
else
title = format_title
end
end
end
end
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,
[""] = 10, [""] = 100, [""] = 1000, [""] = 10000,
}
function chinese_to_number(cn)
local total = 0
local num = 0
local unit = 1
local chars = {}
for uchar in cn:gmatch(UTF8_PATTERN) do
table.insert(chars, 1, uchar)
end
for _, char in ipairs(chars) do
local val = CHINESE_NUM_MAP[char]
if val then
if val >= 10 then
if num == 0 then
num = 1
end
unit = val
else
total = total + val * unit
unit = 1
num = 0
end
end
end
if unit > 1 then
total = total + num * unit
end
if total > 0 then
return total
else
return num
end
end
local CHINESE_NUM = {"", "", "", "", "", "", "", "", "", ""}
local CHINESE_UNIT = {"", "", "", ""}
local CHINESE_BIG_UNIT = {"", "", "亿"}
function number_to_chinese(num)
if num == 0 then return "" end
local str = tostring(num)
local len = #str
local result = ""
local zero_flag = false
for i = 1, len do
local digit = tonumber(str:sub(i, i))
local pos = len - i + 1
local small_unit_index = (pos - 1) % 4 + 1
local small_unit = CHINESE_UNIT[small_unit_index]
if digit == 0 then
zero_flag = true
else
if zero_flag then
result = result .. ""
zero_flag = false
end
if digit == 1 and small_unit_index == 2 and i == 1 then
result = result .. small_unit
else
result = result .. CHINESE_NUM[digit + 1] .. small_unit
end
end
if pos % 4 == 1 and pos > 1 then
local big_unit_index = math.floor((pos - 1) / 4)
result = result .. CHINESE_BIG_UNIT[big_unit_index + 1]
end
end
result = result:gsub("零+$", "")
return result
end
-- 异步执行命令
-- 同时返回 abort 函数,用于取消异步命令
function call_cmd_async(args, callback)
async_running = true
local abort_signal = mp.command_native_async({
name = 'subprocess',
capture_stderr = true,
capture_stdout = true,
playback_only = false,
args = args,
}, function(success, result, error)
if not success or not result or result.status ~= 0 then
local exit_code = (result and result.status or 'unknown')
local message = error or (result and result.stdout .. result.stderr) or ''
callback('Calling failed. Exit code: ' .. exit_code .. ' Error: ' .. message, {})
return
end
local json = result and type(result.stdout) == 'string' and result.stdout or ''
return callback(nil, json)
end)
return function()
mp.abort_async_command(abort_signal)
end
end