init
This commit is contained in:
@@ -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
@@ -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 api(url)+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
|
||||
@@ -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
@@ -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)
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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.
|
||||
------------------------------------------------------------------------------
|
||||
--]]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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(""", "\"")
|
||||
:gsub("'", "'")
|
||||
:gsub(">", ">")
|
||||
:gsub("<", "<")
|
||||
:gsub("&", "&")
|
||||
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("&", "&")
|
||||
:gsub("<", "<")
|
||||
:gsub(">", ">")
|
||||
:gsub("\"", """)
|
||||
:gsub("'", "'")
|
||||
|
||||
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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user