init (again)

This commit is contained in:
2026-05-26 15:18:17 +02:00
commit 22246060e6
270 changed files with 117698 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 吴南李
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
File diff suppressed because it is too large Load Diff
+613
View File
@@ -0,0 +1,613 @@
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"
local api_server = options.api_server
for url, source in pairs(DANMAKU.sources) do
if source.from == "api_server" then
if not source.from_history then
DANMAKU.sources[url] = nil
else
DANMAKU.sources[url]["data"] = nil
api_server = source.api_server or options.api_server
end
end
end
local episodeId = tonumber(input)
local main_url = api_server .. "/api/v2/comment/" .. episodeId .. "?withRelated=true&chConvert=0"
add_source_to_history(main_url, { from = "api_server", api_server = api_server })
write_history(episodeId, api_server)
set_danmaku_button()
fetch_danmaku(episodeId, from_menu, api_server)
end
-- 回退使用额外的弹幕获取方式
function get_danmaku_fallback(query)
local url = options.fallback_server .. "/?ac=dm&url=" .. query
msg.verbose("尝试获取弹幕:" .. url)
local args = make_danmaku_request_args("GET", url)
if not args then return end
fetch_danmaku_data(args, function(data)
if not data or not data["comments"] or data["count"] <= 1 then
msg.info("备用服务器无数据或返回格式不正确")
show_message("备用服务器无数据或返回格式不正确", 3)
return
end
save_danmaku_data(data["comments"], query, "user_custom")
load_danmaku(true)
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
if options.proxy ~= "" then
table.insert(args, '-x')
table.insert(args, options.proxy)
end
table.insert(args, url)
return args
end
local function normalize_danmaku_response(d)
if not d then return d end
-- 已经是 comments/count 格式则直接返回
if d.comments or d.count then return d end
if d.danmuku and type(d.danmuku) == "table" then
local out = {}
for _, item in ipairs(d.danmuku) do
-- item 预期为数组,索引: 1=time, 2=pos(right/top/bottom), 3=color(hex), 5=content
local time = tonumber(item[1]) or 0
local pos = item[2] or "right"
local color = item[3] or ""
local content = item[5] or item[4] or ""
local mode = 1
if pos == "right" then
mode = 1
elseif pos == "top" then
mode = 4
elseif pos == "bottom" then
mode = 5
end
local colorDec = 16777215
if type(color) == "number" then
colorDec = color
elseif type(color) == "string" then
colorDec = hex_to_int_color(color)
end
local p = string.format("%.2f,%d,%d", time, mode, colorDec)
table.insert(out, { p = p, m = content })
end
return { comments = out, count = tonumber(d.danum) or #out }
end
return d
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
local animeTitle = tostring(anime.animeTitle or "")
animeTitle = animeTitle:gsub("^%s*(.-)%s*$", "%1")
:gsub("%s*%(.-%)%s*$", "")
:gsub("%s*【.-】.*$", "")
if animeTitle:match("第一[季部]") and tonumber(season_num) == 1 then
target_title = title .. " 第一季"
end
local score = jaro_winkler(target_title, animeTitle)
msg.debug(("候选: %s -> 相似度 %.3f"):format(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 "a1b2c3d4e5f67890abcd1234ef567890",
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 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)
data = normalize_danmaku_response(data)
callback(data)
end)
end
-- 保存弹幕数据
function save_danmaku_data(comments, query, danmaku_source)
local danmaku_list = save_danmaku_to_list(comments)
if DANMAKU.sources[query] ~= nil then
DANMAKU.sources[query]["data"] = danmaku_list
else
DANMAKU.sources[query] = {from = danmaku_source, data = danmaku_list}
end
end
function save_danmaku_downloaded(url, downloaded_file)
local danmaku_list = parse_danmaku_file(downloaded_file)
if file_exists(downloaded_file) then
os.remove(downloaded_file)
end
if DANMAKU.sources[url] ~= nil then
DANMAKU.sources[url]["data"] = danmaku_list
else
DANMAKU.sources[url] = {from = "user_custom", data = danmaku_list}
end
end
-- 处理弹幕数据
function handle_danmaku_data(query, data, from_menu)
-- 如果没有数据,进行重试
if not data or not data["comments"] or data["count"] <= 1 then
show_message("服务器无缓存数据,再次尝试请求", 10)
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"] <= 1 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(data["comments"], query, "user_custom")
load_danmaku(from_menu)
end
end
-- 处理获取到的数据
function handle_fetched_danmaku(data, url, from_menu)
if data and data["comments"] then
if data["count"] == 0 then
if DANMAKU.sources[url] == nil then
DANMAKU.sources[url] = {from = "api_server"}
end
show_message("该集弹幕内容为空,结束加载", 3)
msg.verbose("该集弹幕内容为空,结束加载")
return
end
save_danmaku_data(data["comments"], url, "api_server")
load_danmaku(from_menu)
else
show_message("无数据", 3)
msg.info("无数据")
end
end
-- 匹配弹幕库 comment, 仅匹配dandan本身弹幕库
-- 通过danmaku apiurl+id获取弹幕
function fetch_danmaku(episodeId, from_menu, api_server)
local url = (api_server or 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 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$")) then
msg.warn("仅支持弹幕文件")
return
end
if DANMAKU.sources[query] ~= nil then
DANMAKU.sources[query]["from"] = "user_local"
DANMAKU.sources[query]["data"] = parse_danmaku_file(path)
else
DANMAKU.sources[query] = {from = "user_local", data = parse_danmaku_file(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)
handle_danmaku_data(query, data, from_menu)
end)
end
-- 将弹幕转换为 Lua table
function save_danmaku_to_list(comments)
local danmaku_list = {}
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 time = tonumber(fields[1])
local type = tonumber(fields[2])
local color = tonumber(fields[3]) or 0xFFFFFF
local size = 25
local m_value = comment["m"]
:gsub("[%z\1-\31]", "")
:gsub("\\", "")
:gsub("\"", "")
table.insert(danmaku_list, {
time = time,
type = type,
size = size,
color = color,
text = m_value
})
end
end
return danmaku_list
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
+435
View File
@@ -0,0 +1,435 @@
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).*")
elseif url:match("^https?://v%.youku%.com/") and url:match("[?&]vid=") then
-- 转换 youku 的短链接形式 video?vid=... 到真实播放页 v_show/id_*.html
local vid = url:match("[?&]vid=([^&]+)")
if vid then
play_url = "https://v.youku.com/v_show/id_" .. vid .. ".html"
else
play_url = url:gsub("%?bsource=360ogvys$",""):gsub("&.*$","")
end
else
play_url = url:gsub("%?bsource=360ogvys$",""):gsub("&.*$","")
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.tmdb.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
local function get_episodes_v2(cat, id, site)
local s_param = string.format('[{"cat_id":"%s","ent_id":"%s","site":"%s"}]', tostring(cat), tostring(id), tostring(site))
local url = string.format("https://api.so.360kan.com/episodesv2?v_ap=1&s=%s", url_encode(s_param))
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 episodesv2: " .. (res.stderr or "unknown error"))
return nil
end
local data_text = res.stdout or ""
-- 兼容 JSONP 和 纯 JSON提取最外层括号内 JSON
local json_payload = data_text
local first_paren = data_text:find('%(')
local last_paren = data_text:match('.*()%)')
if first_paren and last_paren and last_paren > first_paren then
json_payload = data_text:sub(first_paren + 1, last_paren - 1)
end
local parsed = utils.parse_json(json_payload)
if not parsed then
msg.error("episodesv2: 解析返回失败: " .. (res.stdout or ""))
return nil
end
local episodes = {}
if parsed.code == 0 and parsed.data and #parsed.data > 0 then
local seriesHTML = parsed.data[1] and parsed.data[1].seriesHTML
if seriesHTML and seriesHTML.seriesPlaylinks then
for i, ep in ipairs(seriesHTML.seriesPlaylinks) do
local episode_url = nil
if type(ep) == 'string' then
episode_url = ep
elseif type(ep) == 'table' and ep.url then
episode_url = ep.url
end
if episode_url and episode_url ~= '' then
table.insert(episodes, { index = i, url = episode_url })
end
end
end
end
if #episodes == 0 then
return nil
end
return episodes
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
local items = {}
local episodes = nil
if cat == 2 or cat == 4 then
episodes = get_episodes_v2(cat, id, site)
end
-- 统一构建 episode_rows优先使用 episodesv2 返回的数据,否则使用 v1/detail
local episode_rows = nil
if episodes then
episode_rows = {}
for _, ep in ipairs(episodes) do
table.insert(episode_rows, { index = tostring(ep.index), url = ep.url })
end
else
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)
if result and result.data and result.data.allepidetail and result.data.allepidetail[site] then
episode_rows = {}
for _, it in ipairs(result.data.allepidetail[site]) do
table.insert(episode_rows, { index = tostring(it.playlink_num), url = it.url })
end
end
end
if episode_rows and #episode_rows > 0 then
if episodenum then
for _, ep in ipairs(episode_rows) do
if tonumber(ep.index) == tonumber(episodenum) then
load_extra_danmaku(ep.url, ep.index, number, class, id, site, title, year)
return
end
end
end
table.insert(items, {
title = "← 返回搜索结果",
value = { "script-message-to", "uosc", "open-menu", latest_menu_anime },
keep_open = false,
selectable = true,
})
for _, ep in ipairs(episode_rows) do
table.insert(items, {
title = "" .. ep.index .. "",
hint = ep.index,
value = {
"script-message-to",
mp.get_script_name(),
"add-extra-event",
ep.url, ep.index, tostring(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
latest_menu_anime = 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
+940
View File
@@ -0,0 +1,940 @@
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}
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)
HAS_DANMAKU = string.format("user-data/%s/has-danmaku", mp.get_script_name())
mp.set_property_bool(HAS_DANMAKU, false)
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)()
local rebuild_convert_timer = nil
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 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
local segs = {}
for i = 1, #delay_segments do segs[i] = delay_segments[i] end
table.sort(segs, function(a, b) return a.start < b.start end)
local applied_delay = 0
for i = 1, #segs do
local seg = segs[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
function parse_delay_input(text)
if not text then return nil end
local s = tostring(text):gsub("%s+", "")
if s == "" then return nil end
-- XmYs 格式,允许负号在分钟部分
local m, sec = string.match(s, "^(%-?%d+)m(%d+)s$")
if m and sec then
m = tonumber(m)
sec = tonumber(sec)
if not m or not sec then return nil end
if m < 0 then sec = -sec end
return m * 60 + sec
end
-- 普通数字(整数或小数),支持负数
local n = tonumber(s)
if n ~= nil then return n end
return nil
end
local function set_danmaku_delay(dly, time, specific_source)
if specific_source then
local source = DANMAKU.sources[specific_source]
if source and source.data 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
source.delay_segments = merge_delay_segments(source.delay_segments)
add_source_to_history(specific_source, source)
end
else
for url, source in pairs(DANMAKU.sources) do
if source.data 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
source.delay_segments = merge_delay_segments(source.delay_segments)
add_source_to_history(url, source)
end
end
end
if dly == 0 then
DELAY = 0
else
DELAY = DELAY + dly
end
if ENABLED and COMMENTS ~= nil then
render()
end
-- 防抖:批量重建 ASS 事件并渲染,避免频繁变更导致重复重建
if rebuild_convert_timer then
rebuild_convert_timer:kill()
rebuild_convert_timer = nil
end
rebuild_convert_timer = mp.add_timeout(0.1, function()
if convert_danmaku_to_ass_events then
convert_danmaku_to_ass_events(true)
end
render()
rebuild_convert_timer = nil
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
DANMAKU.sources[url] = nil
end
end
load_danmaku(false)
show_message("已重置当前视频所有弹幕源更改", 3)
msg.verbose("已重置当前视频所有弹幕源更改")
end
function write_history(episodeid, api_server)
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
if api_server then
history[dir].api_server = api_server
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
if record.from == "api_server" then
record.api_server = add_source.api_server or options.api_server
else
record.api_server = nil
end
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,
api_server = data.api_server,
}
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,
api_server = record.api_server,
}
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
-- 视频播放时保存弹幕
function save_danmaku(not_forced)
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_out)
end
end
end
-- 加载弹幕
function load_danmaku(from_menu, no_osd)
if not ENABLED then return end
convert_danmaku_to_ass_events()
render_danmaku(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,
}
if options.cookie_file and options.cookie_file ~= "" then
table.insert(arg, '-b')
table.insert(arg, mp.command_native({"expand-path", options.cookie_file}))
end
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
if options.cookie_file and options.cookie_file ~= "" then
table.insert(arg, '-b')
table.insert(arg, mp.command_native({"expand-path", options.cookie_file}))
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)
os.remove(danmaku_json)
local comments = utils.parse_json(comments_json)
if not comments then
return
end
local output_table = {}
for _, comment in ipairs(comments) do
local color = hex_to_int_color(comment["color"])
local mode = get_type_from_position(comment["position"])
local time = tonumber(comment["time"]) / 10
local c_param = string.format("%s,%s,%s,25,,,", time, color, mode)
table.insert(output_table, {
c = c_param,
m = comment["text"]
})
end
local final_json_str = utils.format_json(output_table)
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(final_json_str)
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 source_arg = commands[3]
local dly = parse_delay_input(delay_str)
local time = time_str and tonumber(time_str)
if type(dly) ~= "number" then
show_message("参数错误:缺少有效的延迟秒数", 3)
return
end
if source_arg and source_arg ~= "nil" then
set_danmaku_delay(dly, time, source_arg)
else
set_danmaku_delay(dly, time)
end
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")
if COMMENTS == nil then
show_message("加载弹幕初始化...", 3)
set_danmaku_visibility(true)
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")
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", open_delay_menu)
mp.register_script_message("open_search_danmaku_menu", open_input_menu)
mp.register_script_message("open_add_source_menu", open_add_menu)
mp.register_script_message("open_add_total_menu", open_add_total_menu)
+491
View File
@@ -0,0 +1,491 @@
-- modified from https://github.com/idiomic/Lua_AES
--[[
Copyright 2019 Tyler Richard Hoyer
Copyright 2025 dyphire
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]
local unpack = unpack or table.unpack
local GF8x2 = {
[0]=0x00,0x02,0x04,0x06,0x08,0x0a,0x0c,0x0e,0x10,0x12,0x14,0x16,0x18,0x1a,0x1c,0x1e,
0x20,0x22,0x24,0x26,0x28,0x2a,0x2c,0x2e,0x30,0x32,0x34,0x36,0x38,0x3a,0x3c,0x3e,
0x40,0x42,0x44,0x46,0x48,0x4a,0x4c,0x4e,0x50,0x52,0x54,0x56,0x58,0x5a,0x5c,0x5e,
0x60,0x62,0x64,0x66,0x68,0x6a,0x6c,0x6e,0x70,0x72,0x74,0x76,0x78,0x7a,0x7c,0x7e,
0x80,0x82,0x84,0x86,0x88,0x8a,0x8c,0x8e,0x90,0x92,0x94,0x96,0x98,0x9a,0x9c,0x9e,
0xa0,0xa2,0xa4,0xa6,0xa8,0xaa,0xac,0xae,0xb0,0xb2,0xb4,0xb6,0xb8,0xba,0xbc,0xbe,
0xc0,0xc2,0xc4,0xc6,0xc8,0xca,0xcc,0xce,0xd0,0xd2,0xd4,0xd6,0xd8,0xda,0xdc,0xde,
0xe0,0xe2,0xe4,0xe6,0xe8,0xea,0xec,0xee,0xf0,0xf2,0xf4,0xf6,0xf8,0xfa,0xfc,0xfe,
0x1b,0x19,0x1f,0x1d,0x13,0x11,0x17,0x15,0x0b,0x09,0x0f,0x0d,0x03,0x01,0x07,0x05,
0x3b,0x39,0x3f,0x3d,0x33,0x31,0x37,0x35,0x2b,0x29,0x2f,0x2d,0x23,0x21,0x27,0x25,
0x5b,0x59,0x5f,0x5d,0x53,0x51,0x57,0x55,0x4b,0x49,0x4f,0x4d,0x43,0x41,0x47,0x45,
0x7b,0x79,0x7f,0x7d,0x73,0x71,0x77,0x75,0x6b,0x69,0x6f,0x6d,0x63,0x61,0x67,0x65,
0x9b,0x99,0x9f,0x9d,0x93,0x91,0x97,0x95,0x8b,0x89,0x8f,0x8d,0x83,0x81,0x87,0x85,
0xbb,0xb9,0xbf,0xbd,0xb3,0xb1,0xb7,0xb5,0xab,0xa9,0xaf,0xad,0xa3,0xa1,0xa7,0xa5,
0xdb,0xd9,0xdf,0xdd,0xd3,0xd1,0xd7,0xd5,0xcb,0xc9,0xcf,0xcd,0xc3,0xc1,0xc7,0xc5,
0xfb,0xf9,0xff,0xfd,0xf3,0xf1,0xf7,0xf5,0xeb,0xe9,0xef,0xed,0xe3,0xe1,0xe7,0xe5
}
local GF8x3 = {
[0]=0x00,0x03,0x06,0x05,0x0c,0x0f,0x0a,0x09,0x18,0x1b,0x1e,0x1d,0x14,0x17,0x12,0x11,
0x30,0x33,0x36,0x35,0x3c,0x3f,0x3a,0x39,0x28,0x2b,0x2e,0x2d,0x24,0x27,0x22,0x21,
0x60,0x63,0x66,0x65,0x6c,0x6f,0x6a,0x69,0x78,0x7b,0x7e,0x7d,0x74,0x77,0x72,0x71,
0x50,0x53,0x56,0x55,0x5c,0x5f,0x5a,0x59,0x48,0x4b,0x4e,0x4d,0x44,0x47,0x42,0x41,
0xc0,0xc3,0xc6,0xc5,0xcc,0xcf,0xca,0xc9,0xd8,0xdb,0xde,0xdd,0xd4,0xd7,0xd2,0xd1,
0xf0,0xf3,0xf6,0xf5,0xfc,0xff,0xfa,0xf9,0xe8,0xeb,0xee,0xed,0xe4,0xe7,0xe2,0xe1,
0xa0,0xa3,0xa6,0xa5,0xac,0xaf,0xaa,0xa9,0xb8,0xbb,0xbe,0xbd,0xb4,0xb7,0xb2,0xb1,
0x90,0x93,0x96,0x95,0x9c,0x9f,0x9a,0x99,0x88,0x8b,0x8e,0x8d,0x84,0x87,0x82,0x81,
0x9b,0x98,0x9d,0x9e,0x97,0x94,0x91,0x92,0x83,0x80,0x85,0x86,0x8f,0x8c,0x89,0x8a,
0xab,0xa8,0xad,0xae,0xa7,0xa4,0xa1,0xa2,0xb3,0xb0,0xb5,0xb6,0xbf,0xbc,0xb9,0xba,
0xfb,0xf8,0xfd,0xfe,0xf7,0xf4,0xf1,0xf2,0xe3,0xe0,0xe5,0xe6,0xef,0xec,0xe9,0xea,
0xcb,0xc8,0xcd,0xce,0xc7,0xc4,0xc1,0xc2,0xd3,0xd0,0xd5,0xd6,0xdf,0xdc,0xd9,0xda,
0x5b,0x58,0x5d,0x5e,0x57,0x54,0x51,0x52,0x43,0x40,0x45,0x46,0x4f,0x4c,0x49,0x4a,
0x6b,0x68,0x6d,0x6e,0x67,0x64,0x61,0x62,0x73,0x70,0x75,0x76,0x7f,0x7c,0x79,0x7a,
0x3b,0x38,0x3d,0x3e,0x37,0x34,0x31,0x32,0x23,0x20,0x25,0x26,0x2f,0x2c,0x29,0x2a,
0x0b,0x08,0x0d,0x0e,0x07,0x04,0x01,0x02,0x13,0x10,0x15,0x16,0x1f,0x1c,0x19,0x1a
}
local GF8x9 = {
[0]=0x00,0x09,0x12,0x1b,0x24,0x2d,0x36,0x3f,0x48,0x41,0x5a,0x53,0x6c,0x65,0x7e,0x77,
0x90,0x99,0x82,0x8b,0xb4,0xbd,0xa6,0xaf,0xd8,0xd1,0xca,0xc3,0xfc,0xf5,0xee,0xe7,
0x3b,0x32,0x29,0x20,0x1f,0x16,0x0d,0x04,0x73,0x7a,0x61,0x68,0x57,0x5e,0x45,0x4c,
0xab,0xa2,0xb9,0xb0,0x8f,0x86,0x9d,0x94,0xe3,0xea,0xf1,0xf8,0xc7,0xce,0xd5,0xdc,
0x76,0x7f,0x64,0x6d,0x52,0x5b,0x40,0x49,0x3e,0x37,0x2c,0x25,0x1a,0x13,0x08,0x01,
0xe6,0xef,0xf4,0xfd,0xc2,0xcb,0xd0,0xd9,0xae,0xa7,0xbc,0xb5,0x8a,0x83,0x98,0x91,
0x4d,0x44,0x5f,0x56,0x69,0x60,0x7b,0x72,0x05,0x0c,0x17,0x1e,0x21,0x28,0x33,0x3a,
0xdd,0xd4,0xcf,0xc6,0xf9,0xf0,0xeb,0xe2,0x95,0x9c,0x87,0x8e,0xb1,0xb8,0xa3,0xaa,
0xec,0xe5,0xfe,0xf7,0xc8,0xc1,0xda,0xd3,0xa4,0xad,0xb6,0xbf,0x80,0x89,0x92,0x9b,
0x7c,0x75,0x6e,0x67,0x58,0x51,0x4a,0x43,0x34,0x3d,0x26,0x2f,0x10,0x19,0x02,0x0b,
0xd7,0xde,0xc5,0xcc,0xf3,0xfa,0xe1,0xe8,0x9f,0x96,0x8d,0x84,0xbb,0xb2,0xa9,0xa0,
0x47,0x4e,0x55,0x5c,0x63,0x6a,0x71,0x78,0x0f,0x06,0x1d,0x14,0x2b,0x22,0x39,0x30,
0x9a,0x93,0x88,0x81,0xbe,0xb7,0xac,0xa5,0xd2,0xdb,0xc0,0xc9,0xf6,0xff,0xe4,0xed,
0x0a,0x03,0x18,0x11,0x2e,0x27,0x3c,0x35,0x42,0x4b,0x50,0x59,0x66,0x6f,0x74,0x7d,
0xa1,0xa8,0xb3,0xba,0x85,0x8c,0x97,0x9e,0xe9,0xe0,0xfb,0xf2,0xcd,0xc4,0xdf,0xd6,
0x31,0x38,0x23,0x2a,0x15,0x1c,0x07,0x0e,0x79,0x70,0x6b,0x62,0x5d,0x54,0x4f,0x46
}
local GF8x11 = {
[0]=0x00,0x0b,0x16,0x1d,0x2c,0x27,0x3a,0x31,0x58,0x53,0x4e,0x45,0x74,0x7f,0x62,0x69,
0xb0,0xbb,0xa6,0xad,0x9c,0x97,0x8a,0x81,0xe8,0xe3,0xfe,0xf5,0xc4,0xcf,0xd2,0xd9,
0x7b,0x70,0x6d,0x66,0x57,0x5c,0x41,0x4a,0x23,0x28,0x35,0x3e,0x0f,0x04,0x19,0x12,
0xcb,0xc0,0xdd,0xd6,0xe7,0xec,0xf1,0xfa,0x93,0x98,0x85,0x8e,0xbf,0xb4,0xa9,0xa2,
0xf6,0xfd,0xe0,0xeb,0xda,0xd1,0xcc,0xc7,0xae,0xa5,0xb8,0xb3,0x82,0x89,0x94,0x9f,
0x46,0x4d,0x50,0x5b,0x6a,0x61,0x7c,0x77,0x1e,0x15,0x08,0x03,0x32,0x39,0x24,0x2f,
0x8d,0x86,0x9b,0x90,0xa1,0xaa,0xb7,0xbc,0xd5,0xde,0xc3,0xc8,0xf9,0xf2,0xef,0xe4,
0x3d,0x36,0x2b,0x20,0x11,0x1a,0x07,0x0c,0x65,0x6e,0x73,0x78,0x49,0x42,0x5f,0x54,
0xf7,0xfc,0xe1,0xea,0xdb,0xd0,0xcd,0xc6,0xaf,0xa4,0xb9,0xb2,0x83,0x88,0x95,0x9e,
0x47,0x4c,0x51,0x5a,0x6b,0x60,0x7d,0x76,0x1f,0x14,0x09,0x02,0x33,0x38,0x25,0x2e,
0x8c,0x87,0x9a,0x91,0xa0,0xab,0xb6,0xbd,0xd4,0xdf,0xc2,0xc9,0xf8,0xf3,0xee,0xe5,
0x3c,0x37,0x2a,0x21,0x10,0x1b,0x06,0x0d,0x64,0x6f,0x72,0x79,0x48,0x43,0x5e,0x55,
0x01,0x0a,0x17,0x1c,0x2d,0x26,0x3b,0x30,0x59,0x52,0x4f,0x44,0x75,0x7e,0x63,0x68,
0xb1,0xba,0xa7,0xac,0x9d,0x96,0x8b,0x80,0xe9,0xe2,0xff,0xf4,0xc5,0xce,0xd3,0xd8,
0x7a,0x71,0x6c,0x67,0x56,0x5d,0x40,0x4b,0x22,0x29,0x34,0x3f,0x0e,0x05,0x18,0x13,
0xca,0xc1,0xdc,0xd7,0xe6,0xed,0xf0,0xfb,0x92,0x99,0x84,0x8f,0xbe,0xb5,0xa8,0xa3
}
local GF8x13 = {
[0]=0x00,0x0d,0x1a,0x17,0x34,0x39,0x2e,0x23,0x68,0x65,0x72,0x7f,0x5c,0x51,0x46,0x4b,
0xd0,0xdd,0xca,0xc7,0xe4,0xe9,0xfe,0xf3,0xb8,0xb5,0xa2,0xaf,0x8c,0x81,0x96,0x9b,
0xbb,0xb6,0xa1,0xac,0x8f,0x82,0x95,0x98,0xd3,0xde,0xc9,0xc4,0xe7,0xea,0xfd,0xf0,
0x6b,0x66,0x71,0x7c,0x5f,0x52,0x45,0x48,0x03,0x0e,0x19,0x14,0x37,0x3a,0x2d,0x20,
0x6d,0x60,0x77,0x7a,0x59,0x54,0x43,0x4e,0x05,0x08,0x1f,0x12,0x31,0x3c,0x2b,0x26,
0xbd,0xb0,0xa7,0xaa,0x89,0x84,0x93,0x9e,0xd5,0xd8,0xcf,0xc2,0xe1,0xec,0xfb,0xf6,
0xd6,0xdb,0xcc,0xc1,0xe2,0xef,0xf8,0xf5,0xbe,0xb3,0xa4,0xa9,0x8a,0x87,0x90,0x9d,
0x06,0x0b,0x1c,0x11,0x32,0x3f,0x28,0x25,0x6e,0x63,0x74,0x79,0x5a,0x57,0x40,0x4d,
0xda,0xd7,0xc0,0xcd,0xee,0xe3,0xf4,0xf9,0xb2,0xbf,0xa8,0xa5,0x86,0x8b,0x9c,0x91,
0x0a,0x07,0x10,0x1d,0x3e,0x33,0x24,0x29,0x62,0x6f,0x78,0x75,0x56,0x5b,0x4c,0x41,
0x61,0x6c,0x7b,0x76,0x55,0x58,0x4f,0x42,0x09,0x04,0x13,0x1e,0x3d,0x30,0x27,0x2a,
0xb1,0xbc,0xab,0xa6,0x85,0x88,0x9f,0x92,0xd9,0xd4,0xc3,0xce,0xed,0xe0,0xf7,0xfa,
0xb7,0xba,0xad,0xa0,0x83,0x8e,0x99,0x94,0xdf,0xd2,0xc5,0xc8,0xeb,0xe6,0xf1,0xfc,
0x67,0x6a,0x7d,0x70,0x53,0x5e,0x49,0x44,0x0f,0x02,0x15,0x18,0x3b,0x36,0x21,0x2c,
0x0c,0x01,0x16,0x1b,0x38,0x35,0x22,0x2f,0x64,0x69,0x7e,0x73,0x50,0x5d,0x4a,0x47,
0xdc,0xd1,0xc6,0xcb,0xe8,0xe5,0xf2,0xff,0xb4,0xb9,0xae,0xa3,0x80,0x8d,0x9a,0x97
}
local GF8x14 = {
[0]=0x00,0x0e,0x1c,0x12,0x38,0x36,0x24,0x2a,0x70,0x7e,0x6c,0x62,0x48,0x46,0x54,0x5a,
0xe0,0xee,0xfc,0xf2,0xd8,0xd6,0xc4,0xca,0x90,0x9e,0x8c,0x82,0xa8,0xa6,0xb4,0xba,
0xdb,0xd5,0xc7,0xc9,0xe3,0xed,0xff,0xf1,0xab,0xa5,0xb7,0xb9,0x93,0x9d,0x8f,0x81,
0x3b,0x35,0x27,0x29,0x03,0x0d,0x1f,0x11,0x4b,0x45,0x57,0x59,0x73,0x7d,0x6f,0x61,
0xad,0xa3,0xb1,0xbf,0x95,0x9b,0x89,0x87,0xdd,0xd3,0xc1,0xcf,0xe5,0xeb,0xf9,0xf7,
0x4d,0x43,0x51,0x5f,0x75,0x7b,0x69,0x67,0x3d,0x33,0x21,0x2f,0x05,0x0b,0x19,0x17,
0x76,0x78,0x6a,0x64,0x4e,0x40,0x52,0x5c,0x06,0x08,0x1a,0x14,0x3e,0x30,0x22,0x2c,
0x96,0x98,0x8a,0x84,0xae,0xa0,0xb2,0xbc,0xe6,0xe8,0xfa,0xf4,0xde,0xd0,0xc2,0xcc,
0x41,0x4f,0x5d,0x53,0x79,0x77,0x65,0x6b,0x31,0x3f,0x2d,0x23,0x09,0x07,0x15,0x1b,
0xa1,0xaf,0xbd,0xb3,0x99,0x97,0x85,0x8b,0xd1,0xdf,0xcd,0xc3,0xe9,0xe7,0xf5,0xfb,
0x9a,0x94,0x86,0x88,0xa2,0xac,0xbe,0xb0,0xea,0xe4,0xf6,0xf8,0xd2,0xdc,0xce,0xc0,
0x7a,0x74,0x66,0x68,0x42,0x4c,0x5e,0x50,0x0a,0x04,0x16,0x18,0x32,0x3c,0x2e,0x20,
0xec,0xe2,0xf0,0xfe,0xd4,0xda,0xc8,0xc6,0x9c,0x92,0x80,0x8e,0xa4,0xaa,0xb8,0xb6,
0x0c,0x02,0x10,0x1e,0x34,0x3a,0x28,0x26,0x7c,0x72,0x60,0x6e,0x44,0x4a,0x58,0x56,
0x37,0x39,0x2b,0x25,0x0f,0x01,0x13,0x1d,0x47,0x49,0x5b,0x55,0x7f,0x71,0x63,0x6d,
0xd7,0xd9,0xcb,0xc5,0xef,0xe1,0xf3,0xfd,0xa7,0xa9,0xbb,0xb5,0x9f,0x91,0x83,0x8d
}
local s = {
[0]=0x63,0x7C,0x77,0x7B,0xF2,0x6B,0x6F,0xC5,0x30,0x01,0x67,0x2B,0xFE,0xD7,0xAB,0x76,
0xCA,0x82,0xC9,0x7D,0xFA,0x59,0x47,0xF0,0xAD,0xD4,0xA2,0xAF,0x9C,0xA4,0x72,0xC0,
0xB7,0xFD,0x93,0x26,0x36,0x3F,0xF7,0xCC,0x34,0xA5,0xE5,0xF1,0x71,0xD8,0x31,0x15,
0x04,0xC7,0x23,0xC3,0x18,0x96,0x05,0x9A,0x07,0x12,0x80,0xE2,0xEB,0x27,0xB2,0x75,
0x09,0x83,0x2C,0x1A,0x1B,0x6E,0x5A,0xA0,0x52,0x3B,0xD6,0xB3,0x29,0xE3,0x2F,0x84,
0x53,0xD1,0x00,0xED,0x20,0xFC,0xB1,0x5B,0x6A,0xCB,0xBE,0x39,0x4A,0x4C,0x58,0xCF,
0xD0,0xEF,0xAA,0xFB,0x43,0x4D,0x33,0x85,0x45,0xF9,0x02,0x7F,0x50,0x3C,0x9F,0xA8,
0x51,0xA3,0x40,0x8F,0x92,0x9D,0x38,0xF5,0xBC,0xB6,0xDA,0x21,0x10,0xFF,0xF3,0xD2,
0xCD,0x0C,0x13,0xEC,0x5F,0x97,0x44,0x17,0xC4,0xA7,0x7E,0x3D,0x64,0x5D,0x19,0x73,
0x60,0x81,0x4F,0xDC,0x22,0x2A,0x90,0x88,0x46,0xEE,0xB8,0x14,0xDE,0x5E,0x0B,0xDB,
0xE0,0x32,0x3A,0x0A,0x49,0x06,0x24,0x5C,0xC2,0xD3,0xAC,0x62,0x91,0x95,0xE4,0x79,
0xE7,0xC8,0x37,0x6D,0x8D,0xD5,0x4E,0xA9,0x6C,0x56,0xF4,0xEA,0x65,0x7A,0xAE,0x08,
0xBA,0x78,0x25,0x2E,0x1C,0xA6,0xB4,0xC6,0xE8,0xDD,0x74,0x1F,0x4B,0xBD,0x8B,0x8A,
0x70,0x3E,0xB5,0x66,0x48,0x03,0xF6,0x0E,0x61,0x35,0x57,0xB9,0x86,0xC1,0x1D,0x9E,
0xE1,0xF8,0x98,0x11,0x69,0xD9,0x8E,0x94,0x9B,0x1E,0x87,0xE9,0xCE,0x55,0x28,0xDF,
0x8C,0xA1,0x89,0x0D,0xBF,0xE6,0x42,0x68,0x41,0x99,0x2D,0x0F,0xB0,0x54,0xBB,0x16
}
local si = {
[0]=0x52,0x09,0x6A,0xD5,0x30,0x36,0xA5,0x38,0xBF,0x40,0xA3,0x9E,0x81,0xF3,0xD7,0xFB,
0x7C,0xE3,0x39,0x82,0x9B,0x2F,0xFF,0x87,0x34,0x8E,0x43,0x44,0xC4,0xDE,0xE9,0xCB,
0x54,0x7B,0x94,0x32,0xA6,0xC2,0x23,0x3D,0xEE,0x4C,0x95,0x0B,0x42,0xFA,0xC3,0x4E,
0x08,0x2E,0xA1,0x66,0x28,0xD9,0x24,0xB2,0x76,0x5B,0xA2,0x49,0x6D,0x8B,0xD1,0x25,
0x72,0xF8,0xF6,0x64,0x86,0x68,0x98,0x16,0xD4,0xA4,0x5C,0xCC,0x5D,0x65,0xB6,0x92,
0x6C,0x70,0x48,0x50,0xFD,0xED,0xB9,0xDA,0x5E,0x15,0x46,0x57,0xA7,0x8D,0x9D,0x84,
0x90,0xD8,0xAB,0x00,0x8C,0xBC,0xD3,0x0A,0xF7,0xE4,0x58,0x05,0xB8,0xB3,0x45,0x06,
0xD0,0x2C,0x1E,0x8F,0xCA,0x3F,0x0F,0x02,0xC1,0xAF,0xBD,0x03,0x01,0x13,0x8A,0x6B,
0x3A,0x91,0x11,0x41,0x4F,0x67,0xDC,0xEA,0x97,0xF2,0xCF,0xCE,0xF0,0xB4,0xE6,0x73,
0x96,0xAC,0x74,0x22,0xE7,0xAD,0x35,0x85,0xE2,0xF9,0x37,0xE8,0x1C,0x75,0xDF,0x6E,
0x47,0xF1,0x1A,0x71,0x1D,0x29,0xC5,0x89,0x6F,0xB7,0x62,0x0E,0xAA,0x18,0xBE,0x1B,
0xFC,0x56,0x3E,0x4B,0xC6,0xD2,0x79,0x20,0x9A,0xDB,0xC0,0xFE,0x78,0xCD,0x5A,0xF4,
0x1F,0xDD,0xA8,0x33,0x88,0x07,0xC7,0x31,0xB1,0x12,0x10,0x59,0x27,0x80,0xEC,0x5F,
0x60,0x51,0x7F,0xA9,0x19,0xB5,0x4A,0x0D,0x2D,0xE5,0x7A,0x9F,0x93,0xC9,0x9C,0xEF,
0xA0,0xE0,0x3B,0x4D,0xAE,0x2A,0xF5,0xB0,0xC8,0xEB,0xBB,0x3C,0x83,0x53,0x99,0x61,
0x17,0x2B,0x04,0x7E,0xBA,0x77,0xD6,0x26,0xE1,0x69,0x14,0x63,0x55,0x21,0x0C,0x7D
}
local rcon = {
0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,
0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,
0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,
0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,
0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,
0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,
0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,
0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,0x6a,0xd4,0xb3,
0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,0x25,0x4a,0x94,
0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,0x08,0x10,0x20,
0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,0x97,0x35,
0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,0x61,0xc2,0x9f,
0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d,0x01,0x02,0x04,
0x08,0x10,0x20,0x40,0x80,0x1b,0x36,0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,
0xc6,0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91,0x39,0x72,0xe4,0xd3,0xbd,
0x61,0xc2,0x9f,0x25,0x4a,0x94,0x33,0x66,0xcc,0x83,0x1d,0x3a,0x74,0xe8,0xcb,0x8d
}
local xor4 = {
[0]=0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,
1,0,3,2,5,4,7,6,9,8,11,10,13,12,15,14,
2,3,0,1,6,7,4,5,10,11,8,9,14,15,12,13,
3,2,1,0,7,6,5,4,11,10,9,8,15,14,13,12,
4,5,6,7,0,1,2,3,12,13,14,15,8,9,10,11,
5,4,7,6,1,0,3,2,13,12,15,14,9,8,11,10,
6,7,4,5,2,3,0,1,14,15,12,13,10,11,8,9,
7,6,5,4,3,2,1,0,15,14,13,12,11,10,9,8,
8,9,10,11,12,13,14,15,0,1,2,3,4,5,6,7,
9,8,11,10,13,12,15,14,1,0,3,2,5,4,7,6,
10,11,8,9,14,15,12,13,2,3,0,1,6,7,4,5,
11,10,9,8,15,14,13,12,3,2,1,0,7,6,5,4,
12,13,14,15,8,9,10,11,4,5,6,7,0,1,2,3,
13,12,15,14,9,8,11,10,5,4,7,6,1,0,3,2,
14,15,12,13,10,11,8,9,6,7,4,5,2,3,0,1,
15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0,
}
local function xor8(a, b)
local al = a % 16
local bl = b % 16
return 16 * xor4[a - al + (b - bl) / 16] + xor4[16 * al + bl]
end
local function xor_blocks_8(a, b)
local res = {}
for i = 1, 16 do
res[i] = xor8(a[i], b[i])
end
return res
end
local function addRoundKey(state, key)
for i, byte in next, state do
state[i] = xor8(byte, key[i])
end
end
local function subBytes(state, s_box)
for i, byte in next, state do
state[i] = s_box[byte]
end
end
local function shiftRows(state)
state[2], state[6], state[10], state[14] =
state[6], state[10], state[14], state[2]
state[3], state[7], state[11], state[15] =
state[11], state[15], state[3], state[7]
state[4], state[8], state[12], state[16] =
state[16], state[4], state[8], state[12]
end
local function inv_shiftRows(state)
state[2], state[6], state[10], state[14] =
state[14], state[2], state[6], state[10]
state[3], state[7], state[11], state[15] =
state[11], state[15], state[3], state[7]
state[4], state[8], state[12], state[16] =
state[8], state[12], state[16], state[4]
end
local function mixColumns(state)
for i = 0, 3 do
local cur = i*4+1
local a, b, c, d = state[cur], state[cur + 1], state[cur + 2], state[cur + 3]
state[cur + 0] = xor8(xor8(xor8(GF8x2[a], GF8x3[b]), c), d)
state[cur + 1] = xor8(xor8(xor8(a, GF8x2[b]), GF8x3[c]), d)
state[cur + 2] = xor8(xor8(xor8(a, b), GF8x2[c]), GF8x3[d])
state[cur + 3] = xor8(xor8(xor8(GF8x3[a], b), c), GF8x2[d])
end
end
local function inv_mixColumns(state) -- TODO: fix
for i = 0, 3 do
local cur = i*4+1
local a, b, c, d = state[cur], state[cur + 1], state[cur + 2], state[cur + 3]
state[cur + 0] = xor8(xor8(xor8(GF8x14[a], GF8x11[b]), GF8x13[c]), GF8x9[d])
state[cur + 1] = xor8(xor8(xor8(GF8x9[a], GF8x14[b]), GF8x11[c]), GF8x13[d])
state[cur + 2] = xor8(xor8(xor8(GF8x13[a], GF8x9[b]), GF8x14[c]), GF8x11[d])
state[cur + 3] = xor8(xor8(xor8(GF8x11[a], GF8x13[b]), GF8x9[c]), GF8x14[d])
end
end
-- 256-bit key constants
local n = 32 -- number of bytes in the 256-bit encryption key
local b = 240 -- number of bytes in 15 128-bit round keys
local function schedule256(key)
local expanded = {}
for c = 0, n-1 do
expanded[c] = key[c]
end
local i = 1
local c = n
local t1, t2, t3, t4 --t
while c < b do
t1 = expanded[c-4]
t2 = expanded[c-3]
t3 = expanded[c-2]
t4 = expanded[c-1]
if (c % n == 0) then
t1, t2, t3, t4 = xor8(rcon[i+1], s[t2]), s[t3], s[t4], s[t1]
i = i + 1
end
if (c % n == 16) then
t1 = s[t1]
t2 = s[t2]
t3 = s[t3]
t4 = s[t4]
end
t1 = xor8(t1, expanded[c - n])
expanded[c] = t1
c = c + 1
t2 = xor8(t2, expanded[c - n])
expanded[c] = t2
c = c + 1
t3 = xor8(t3, expanded[c - n])
expanded[c] = t3
c = c + 1
t4 = xor8(t4, expanded[c - n])
expanded[c] = t4
c = c + 1
end
local roundKeys = {}
for round = 0, 14 do
local roundKey = {}
for byte = 0, 15 do
roundKey[byte+1] = expanded[round * 16 + byte]
end
roundKeys[round] = roundKey
end
return roundKeys
end
local function chunks(text, i)
local first = i * 16 + 1
if first > #text then
return
end
i = i + 1
local chunk = {text:byte(first, first + 15)}
for j = #chunk + 1, 16 do
chunk[j] = 0
end
return i, chunk
end
local function pkcs7_unpad(str)
local len = #str
if len == 0 then return str end
local pad_len = string.byte(str, len)
if pad_len < 1 or pad_len > 16 then
return nil
end
for i = len - pad_len + 1, len do
if string.byte(str, i) ~= pad_len then
return nil
end
end
return string.sub(str, 1, len - pad_len)
end
local function zero_unpad(str)
local len = #str
while len > 0 and string.byte(str, len) == 0 do
len = len - 1
end
return string.sub(str, 1, len)
end
local function unpad(str)
local unpadded = pkcs7_unpad(str)
if unpadded then
return unpadded
else
return zero_unpad(str)
end
end
local function encrypt(state, roundKeys)
addRoundKey(state, roundKeys[0])
for round = 1, 13 do
subBytes(state, s)
shiftRows(state)
mixColumns(state)
addRoundKey(state, roundKeys[round])
end
subBytes(state, s)
shiftRows(state)
addRoundKey(state, roundKeys[14])
end
local function decrypt(state, roundKeys)
addRoundKey(state, roundKeys[14])
inv_shiftRows(state)
subBytes(state, si)
for round = 13, 1, -1 do
addRoundKey(state, roundKeys[round])
inv_mixColumns(state)
inv_shiftRows(state)
subBytes(state, si)
end
addRoundKey(state, roundKeys[0])
end
local function ECB_encrypt(key, originaltext)
local text = {}
local roundKeys = schedule256(key)
local i = 0
while true do
i, state = chunks(originaltext, i)
if not state then break end
encrypt(state, roundKeys)
text[i] = string.char(unpack(state))
end
return table.concat(text)
end
local function ECB_decrypt(key, ciphertext)
local text = {}
local roundKeys = schedule256(key)
local i = 0
while true do
i, state = chunks(ciphertext, i)
if not state then break end
decrypt(state, roundKeys)
text[i] = string.char(unpack(state))
end
return unpad(table.concat(text))
end
local function CBC_encrypt(key, iv, originaltext)
local roundKeys = schedule256(key)
local text = {}
local prev_block = {unpack(iv)}
local i = 0
while true do
i, block = chunks(originaltext, i)
if not block then break end
local xored = xor_blocks_8(block, prev_block)
encrypt(xored, roundKeys)
text[i] = string.char(unpack(xored))
prev_block = xored
end
return table.concat(text)
end
local function CBC_decrypt(key, iv, ciphertext)
local roundKeys = schedule256(key)
local text = {}
local prev_block = {unpack(iv)}
local i = 0
while true do
i, block = chunks(ciphertext, i)
if not block then break end
local decrypted = {unpack(block)}
decrypt(decrypted, roundKeys)
local plain_block = xor_blocks_8(decrypted, prev_block)
text[i] = string.char(unpack(plain_block))
prev_block = block
end
local result = table.concat(text)
return unpad(result)
end
return {
ECB = {
encrypt = ECB_encrypt;
decrypt = ECB_decrypt;
};
CBC = {
encrypt = CBC_encrypt;
decrypt = CBC_decrypt;
};
}
+203
View File
@@ -0,0 +1,203 @@
--[[
base64 -- v1.5.3 public domain Lua base64 encoder/decoder
no warranty implied; use at your own risk
Needs bit32.extract function. If not present it's implemented using BitOp
or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua
implementation inspired by Rici Lake's post:
http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html
author: Ilya Kolbin (iskolbin@gmail.com)
url: github.com/iskolbin/lbase64
COMPATIBILITY
Lua 5.1+, LuaJIT
LICENSE
See end of file for license information.
--]]
local base64 = {}
local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode
if not extract then
if _G.bit then -- LuaJIT
local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band
extract = function( v, from, width )
return band( shr( v, from ), shl( 1, width ) - 1 )
end
elseif _G._VERSION == "Lua 5.1" then
extract = function( v, from, width )
local w = 0
local flag = 2^from
for i = 0, width-1 do
local flag2 = flag + flag
if v % flag2 >= flag then
w = w + 2^i
end
flag = flag2
end
return w
end
else -- Lua 5.3+
extract = load[[return function( v, from, width )
return ( v >> from ) & ((1 << width) - 1)
end]]()
end
end
function base64.makeencoder( s62, s63, spad )
local encoder = {}
for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J',
'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y',
'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n',
'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2',
'3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do
encoder[b64code] = char:byte()
end
return encoder
end
function base64.makedecoder( s62, s63, spad )
local decoder = {}
for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do
decoder[charcode] = b64code
end
return decoder
end
local DEFAULT_ENCODER = base64.makeencoder()
local DEFAULT_DECODER = base64.makedecoder()
local char, concat = string.char, table.concat
function base64.encode( str, encoder, usecaching )
encoder = encoder or DEFAULT_ENCODER
local t, k, n = {}, 1, #str
local lastn = n % 3
local cache = {}
for i = 1, n-lastn, 3 do
local a, b, c = str:byte( i, i+2 )
local v = a*0x10000 + b*0x100 + c
local s
if usecaching then
s = cache[v]
if not s then
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
cache[v] = s
end
else
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
end
t[k] = s
k = k + 1
end
if lastn == 2 then
local a, b = str:byte( n-1, n )
local v = a*0x10000 + b*0x100
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64])
elseif lastn == 1 then
local v = str:byte( n )*0x10000
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64])
end
return concat( t )
end
function base64.decode( b64, decoder, usecaching, schar1pos, schar2pos )
decoder = decoder or DEFAULT_DECODER
schar1pos = schar1pos or 62
schar2pos = schar2pos or 63
local pattern = '[^%w%+%/%=]'
if decoder then
local s62, s63
for charcode, b64code in pairs( decoder ) do
if b64code == schar1pos then s62 = charcode
elseif b64code == schar2pos then s63 = charcode
end
end
pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) )
end
b64 = b64:gsub( pattern, '' )
local cache = usecaching and {}
local t, k = {}, 1
local n = #b64
local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0
for i = 1, padding > 0 and n-4 or n, 4 do
local a, b, c, d = b64:byte( i, i+3 )
local s
if usecaching then
local v0 = a*0x1000000 + b*0x10000 + c*0x100 + d
s = cache[v0]
if not s then
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
cache[v0] = s
end
else
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
end
t[k] = s
k = k + 1
end
if padding == 1 then
local a, b, c = b64:byte( n-3, n-1 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
t[k] = char( extract(v,16,8), extract(v,8,8))
elseif padding == 2 then
local a, b = b64:byte( n-3, n-2 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000
t[k] = char( extract(v,16,8))
end
return concat( t )
end
return base64
--[[
------------------------------------------------------------------------------
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2018 Ilya Kolbin
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------------------------
--]]
+159
View File
@@ -0,0 +1,159 @@
local unpack = unpack or table.unpack
-- 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*[_%-%.%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*(%d+)%s*[季部]+%s*[_%-%.%s]%s*[^%ddD][eEpP]+(%d+[%.v]?%d*)",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*第([一二三四五六七八九十]+)[季部]+%s*[_%-%.%s]%s*[^%ddD][eEpP]+(%d+[%.v]?%d*)",
format = function(name, season, episode)
return clean_name(name) .. " S" .. chinese_to_number(season) .. "E" .. episode:gsub("v%d+$","")
end
},
{
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*[^%ddD][eEpP]+(%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*[^%ddD][eEpP]+(%d+[%.v]?%d*)[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
format = function(name, episode, year)
return clean_name(name) .. " (" .. year .. ") E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[^%ddD][eEpP]+(%d+%.?%d*)",
format = function(name, episode)
return clean_name(name) .. " E" .. episode
end
},
{
regex = "^(.-)%s*第%s*(%d+[%.v]?%d*)%s*[话集回]",
format = function(name, episode)
return clean_name(name) .. " E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*%[(%d+[%.v]?%d*)%]",
format = function(name, episode)
return clean_name(name) .. " E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*%[(%d+[%.v]?%d*)%(%a+%)%]",
format = function(name, episode)
return clean_name(name) .. " E" .. episode:gsub("v%d+$","")
end
},
{
regex = "^(.-)%s*[%-#]%s*(%d+%.?%d*)%s*",
format = function(name, episode)
return clean_name(name) .. " E" .. episode
end
},
{
regex = "^(.-)%s*[%[%(]([OVADSPs]+)[%]%)]",
format = function(name, sp)
return clean_name(name) .. " [" .. sp .. "]"
end
},
{
regex = "^(.-)%s*[_%-%.%s]%s*(%d?%d)x(%d%d?%d?%d?)[^%dhHxXvVpPkKxXbBfF]",
format = function(name, season, episode)
return clean_name(name) .. " S" .. season .. "E" .. episode
end
},
{
regex = "^%((%d%d%d%d)%.?%d?%d?%.?%d?%d?%)%s*(.-)%s*[%(%[]",
format = function(year, name)
return clean_name(name) .. " (" .. year .. ")"
end
},
{
regex = "^(.-)%s*[_%.%s]%s*(%d%d%d%d)[^%dhHxXvVpPkKxXbBfF]",
format = function(name, year)
return clean_name(name) .. " (" .. year .. ")"
end
},
{
regex = "^%[.-%]%s*%[?(.-)%]?%s*[%(%[]",
format = function(name)
return clean_name(name)
end
},
}
-- Format filename based on regex patterns
function format_filename(title)
for _, formatter in ipairs(formatters) do
local matches = {title:match(formatter.regex)}
if #matches > 0 then
title = formatter.format(unpack(matches))
return title
end
end
end
+192
View File
@@ -0,0 +1,192 @@
--[[
sha256 -- public domain Lua SHA-256 implementation
no warranty implied; use at your own risk
author: dyphire
COMPATIBILITY
Lua 5.1+, LuaJIT
LICENSE: MIT License
--]]
local unpack = unpack or table.unpack
local function band(a,b)
local res = 0
local bit = 1
for i = 0,31 do
local aa = a % 2
local bb = b % 2
if aa == 1 and bb == 1 then
res = res + bit
end
a = (a - aa) / 2
b = (b - bb) / 2
bit = bit * 2
end
return res
end
local function bor(a,b)
local res = 0
local bit = 1
for i = 0,31 do
local aa = a % 2
local bb = b % 2
if aa == 1 or bb == 1 then
res = res + bit
end
a = (a - aa) / 2
b = (b - bb) / 2
bit = bit * 2
end
return res
end
local function bxor(a,b)
local res = 0
local bit = 1
for i = 0,31 do
local aa = a % 2
local bb = b % 2
if (aa + bb) == 1 then
res = res + bit
end
a = (a - aa) / 2
b = (b - bb) / 2
bit = bit * 2
end
return res
end
local function bnot(a)
return 0xFFFFFFFF - a
end
local function lshift(a,n)
return (a * 2^n) % 2^32
end
local function rshift(a,n)
return math.floor(a / 2^n) % 2^32
end
local function bit_ror(x, n)
return bor(rshift(x, n), lshift(x, 32 - n))
end
local function sha256(message)
local k = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
}
local function preprocess(msg)
local len = #msg
local bitLen = len * 8
msg = msg .. "\128"
local zeroPad = 64 - ((len + 9) % 64)
if zeroPad ~= 64 then
msg = msg .. string.rep("\0", zeroPad)
end
msg = msg .. string.char(
rshift(bitLen, 56) % 256,
rshift(bitLen, 48) % 256,
rshift(bitLen, 40) % 256,
rshift(bitLen, 32) % 256,
rshift(bitLen, 24) % 256,
rshift(bitLen, 16) % 256,
rshift(bitLen, 8) % 256,
bitLen % 256
)
return msg
end
local function chunkify(msg)
local chunks = {}
for i = 1, #msg, 64 do
table.insert(chunks, msg:sub(i, i + 63))
end
return chunks
end
local function processChunk(chunk, hash)
local w = {}
for i = 1, 64 do
if i <= 16 then
w[i] = lshift(string.byte(chunk, (i - 1) * 4 + 1), 24) +
lshift(string.byte(chunk, (i - 1) * 4 + 2), 16) +
lshift(string.byte(chunk, (i - 1) * 4 + 3), 8) +
string.byte(chunk, (i - 1) * 4 + 4)
else
local s0 = bxor(bxor(bit_ror(w[i - 15], 7), bit_ror(w[i - 15], 18)), rshift(w[i - 15], 3))
local s1 = bxor(bxor(bit_ror(w[i - 2], 17), bit_ror(w[i - 2], 19)), rshift(w[i - 2], 10))
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) % 2^32
end
end
local a, b, c, d, e, f, g, h = unpack(hash)
for i = 1, 64 do
local s1 = bxor(bxor(bit_ror(e, 6), bit_ror(e, 11)), bit_ror(e, 25))
local ch = bxor(band(e, f), band(bnot(e), g))
local temp1 = (h + s1 + ch + k[i] + w[i]) % 2^32
local s0 = bxor(bxor(bit_ror(a, 2), bit_ror(a, 13)), bit_ror(a, 22))
local maj = bxor(bxor(band(a, b), band(a, c)), band(b, c))
local temp2 = (s0 + maj) % 2^32
h = g
g = f
f = e
e = (d + temp1) % 2^32
d = c
c = b
b = a
a = (temp1 + temp2) % 2^32
end
return
(hash[1] + a) % 2^32,
(hash[2] + b) % 2^32,
(hash[3] + c) % 2^32,
(hash[4] + d) % 2^32,
(hash[5] + e) % 2^32,
(hash[6] + f) % 2^32,
(hash[7] + g) % 2^32,
(hash[8] + h) % 2^32
end
message = preprocess(message)
local chunks = chunkify(message)
local hash = {
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
}
for _, chunk in ipairs(chunks) do
hash = {processChunk(chunk, hash)}
end
local result = ""
for _, h in ipairs(hash) do
result = result .. string.format("%08x", h)
end
return result
end
return sha256
+164
View File
@@ -0,0 +1,164 @@
-- taken from https://github.com/rkscv/danmaku/blob/main/danmaku.lua
-- modified from https://bitop.luajit.org/download.html (LuaBitOp-1.0.2 / md5test.lua)
-- and https://github.com/kikito/md5.lua/blob/master/md5.lua
-- SPDX-License-Identifier:MIT
local byte, char, sub, rep = string.byte, string.char, string.sub, string.rep
local tobit, tohex, bnot, bor, band, bxor, lshift, rshift, rol, bswap
if _G.bit then --LuaJIT
tobit, tohex = _G.bit.tobit or _G.bit.cast, _G.bit.tohex
bnot, bor, band, bxor, lshift, rshift = _G.bit.bnot, _G.bit.bor, _G.bit.band, _G.bit.bxor, _G.bit.lshift, _G.bit.rshift
rol, bswap = _G.bit.rol, _G.bit.bswap
elseif _G.bit32 then --Lua 5.2
local bit32_bnot = _G.bit32.bnot
tobit = function(a) return a <= 0x7fffffff and a or -(_G.bit32.bnot(a) + 1) end
bnot = function(a) return tobit(bit32_bnot(tobit(a))) end
bor, band, bxor, lshift, rshift, rol = _G.bit32.bor, _G.bit32.band, _G.bit32.bxor, _G.bit32.lshift, _G.bit32.rshift, _G.bit32.lrotate
else
return nil
end
if not tohex then
tohex = function(a) return string.sub(string.format('%08x', a), -8) end
end
if not bswap then
bswap = function(a)
return bor(rshift(a, 24), band(rshift(a, 8), 0xff00), lshift(band(a, 0xff00), 8), lshift(a, 24))
end
end
local function tr_f(a, b, c, d, x, s) return rol(bxor(d, band(b, bxor(c, d))) + a + x, s) + b end
local function tr_g(a, b, c, d, x, s) return rol(bxor(c, band(d, bxor(b, c))) + a + x, s) + b end
local function tr_h(a, b, c, d, x, s) return rol(bxor(b, c, d) + a + x, s) + b end
local function tr_i(a, b, c, d, x, s) return rol(bxor(c, bor(b, bnot(d))) + a + x, s) + b end
local function transform(x, a1, b1, c1, d1)
local a, b, c, d = a1, b1, c1, d1
a = tr_f(a, b, c, d, x[1] + 0xd76aa478, 7)
d = tr_f(d, a, b, c, x[2] + 0xe8c7b756, 12)
c = tr_f(c, d, a, b, x[3] + 0x242070db, 17)
b = tr_f(b, c, d, a, x[4] + 0xc1bdceee, 22)
a = tr_f(a, b, c, d, x[5] + 0xf57c0faf, 7)
d = tr_f(d, a, b, c, x[6] + 0x4787c62a, 12)
c = tr_f(c, d, a, b, x[7] + 0xa8304613, 17)
b = tr_f(b, c, d, a, x[8] + 0xfd469501, 22)
a = tr_f(a, b, c, d, x[9] + 0x698098d8, 7)
d = tr_f(d, a, b, c, x[10] + 0x8b44f7af, 12)
c = tr_f(c, d, a, b, x[11] + 0xffff5bb1, 17)
b = tr_f(b, c, d, a, x[12] + 0x895cd7be, 22)
a = tr_f(a, b, c, d, x[13] + 0x6b901122, 7)
d = tr_f(d, a, b, c, x[14] + 0xfd987193, 12)
c = tr_f(c, d, a, b, x[15] + 0xa679438e, 17)
b = tr_f(b, c, d, a, x[16] + 0x49b40821, 22)
a = tr_g(a, b, c, d, x[2] + 0xf61e2562, 5)
d = tr_g(d, a, b, c, x[7] + 0xc040b340, 9)
c = tr_g(c, d, a, b, x[12] + 0x265e5a51, 14)
b = tr_g(b, c, d, a, x[1] + 0xe9b6c7aa, 20)
a = tr_g(a, b, c, d, x[6] + 0xd62f105d, 5)
d = tr_g(d, a, b, c, x[11] + 0x02441453, 9)
c = tr_g(c, d, a, b, x[16] + 0xd8a1e681, 14)
b = tr_g(b, c, d, a, x[5] + 0xe7d3fbc8, 20)
a = tr_g(a, b, c, d, x[10] + 0x21e1cde6, 5)
d = tr_g(d, a, b, c, x[15] + 0xc33707d6, 9)
c = tr_g(c, d, a, b, x[4] + 0xf4d50d87, 14)
b = tr_g(b, c, d, a, x[9] + 0x455a14ed, 20)
a = tr_g(a, b, c, d, x[14] + 0xa9e3e905, 5)
d = tr_g(d, a, b, c, x[3] + 0xfcefa3f8, 9)
c = tr_g(c, d, a, b, x[8] + 0x676f02d9, 14)
b = tr_g(b, c, d, a, x[13] + 0x8d2a4c8a, 20)
a = tr_h(a, b, c, d, x[6] + 0xfffa3942, 4)
d = tr_h(d, a, b, c, x[9] + 0x8771f681, 11)
c = tr_h(c, d, a, b, x[12] + 0x6d9d6122, 16)
b = tr_h(b, c, d, a, x[15] + 0xfde5380c, 23)
a = tr_h(a, b, c, d, x[2] + 0xa4beea44, 4)
d = tr_h(d, a, b, c, x[5] + 0x4bdecfa9, 11)
c = tr_h(c, d, a, b, x[8] + 0xf6bb4b60, 16)
b = tr_h(b, c, d, a, x[11] + 0xbebfbc70, 23)
a = tr_h(a, b, c, d, x[14] + 0x289b7ec6, 4)
d = tr_h(d, a, b, c, x[1] + 0xeaa127fa, 11)
c = tr_h(c, d, a, b, x[4] + 0xd4ef3085, 16)
b = tr_h(b, c, d, a, x[7] + 0x04881d05, 23)
a = tr_h(a, b, c, d, x[10] + 0xd9d4d039, 4)
d = tr_h(d, a, b, c, x[13] + 0xe6db99e5, 11)
c = tr_h(c, d, a, b, x[16] + 0x1fa27cf8, 16)
b = tr_h(b, c, d, a, x[3] + 0xc4ac5665, 23)
a = tr_i(a, b, c, d, x[1] + 0xf4292244, 6)
d = tr_i(d, a, b, c, x[8] + 0x432aff97, 10)
c = tr_i(c, d, a, b, x[15] + 0xab9423a7, 15)
b = tr_i(b, c, d, a, x[6] + 0xfc93a039, 21)
a = tr_i(a, b, c, d, x[13] + 0x655b59c3, 6)
d = tr_i(d, a, b, c, x[4] + 0x8f0ccc92, 10)
c = tr_i(c, d, a, b, x[11] + 0xffeff47d, 15)
b = tr_i(b, c, d, a, x[2] + 0x85845dd1, 21)
a = tr_i(a, b, c, d, x[9] + 0x6fa87e4f, 6)
d = tr_i(d, a, b, c, x[16] + 0xfe2ce6e0, 10)
c = tr_i(c, d, a, b, x[7] + 0xa3014314, 15)
b = tr_i(b, c, d, a, x[14] + 0x4e0811a1, 21)
a = tr_i(a, b, c, d, x[5] + 0xf7537e82, 6)
d = tr_i(d, a, b, c, x[12] + 0xbd3af235, 10)
c = tr_i(c, d, a, b, x[3] + 0x2ad7d2bb, 15)
b = tr_i(b, c, d, a, x[10] + 0xeb86d391, 21)
return tobit(a + a1), tobit(b + b1), tobit(c + c1), tobit(d + d1)
end
local function md5_update(self, s)
local m, len = s, #s
if len % 4 ~= 0 then
m = m .. '\128' .. rep('\0', 63 - band(len + 8, 63)) ..
char(band(lshift(len, 3), 255), band(rshift(len, 5), 255), band(rshift(len, 13), 255),
band(rshift(len, 21), 255)) .. '\0\0\0\0'
end
local a, b, c, d = self.a, self.b, self.c, self.d
local x, k = self.x, self.k
for i = 1, #m, 4 do
local m0, m1, m2, m3 = byte(m, i, i + 3)
x[k] = bor(m0, lshift(m1, 8), lshift(m2, 16), lshift(m3, 24))
if k == 16 then
a, b, c, d = transform(x, a, b, c, d)
k = 1
else
k = k + 1
end
end
self.a, self.b, self.c, self.d, self.k = a, b, c, d, k
self.len = self.len + len
return self
end
local function md5_finish(self)
local len = self.len
if len % 4 == 0 then
local s = '\128' .. rep('\0', 63 - band(len + 8, 63)) ..
char(band(lshift(len, 3), 255), band(rshift(len, 5), 255), band(rshift(len, 13), 255),
band(rshift(len, 21), 255)) .. '\0\0\0\0'
md5_update(self, s)
end
return tohex(bswap(self.a)) .. tohex(bswap(self.b)) .. tohex(bswap(self.c)) .. tohex(bswap(self.d))
end
local md5 = {}
function md5.new()
return {
a = 0x67452301,
b = 0xefcdab89,
c = 0x98badcfe,
d = 0x10325476,
x = {},
k = 1,
len = 0,
update = md5_update,
finish = md5_finish,
}
end
function md5.sum(s)
return md5.new():update(s):finish()
end
return md5
File diff suppressed because it is too large Load Diff
+77
View File
@@ -0,0 +1,77 @@
local opt = require("mp.options")
-- 选项
options = {
-- 指定弹幕服务器地址,自定义服务需兼容 dandanplay 的 api
api_server = "https://api.dandanplay.net",
-- 指定 b 站和爱腾优的弹幕获取的兜底服务器地址,主要用于获取非动画弹幕
-- 可用: https://api.danmu.icuhttps://dmku.hls.one
fallback_server = "https://api.danmu.icu",
-- 设置 tmdb 的 API Key用于获取非动画条目的中文信息(当搜索内容非中文时)
-- 可以在 https://www.themoviedb.org 注册后去个人账号设置界面获取
-- 注意:自定义此参数时还需要对获取到的 API Key 进行 base64 编码
tmdb_api_key = "NmJmYjIxOTZkNzIyN2UyMTIzMGM3Y2YzZjQ4MDNkZGM=",
auto_load = false,
autoload_local_danmaku = false,
autoload_for_url = false,
save_danmaku = false,
user_agent = "mpv_danmaku/1.0",
proxy = "",
-- 可选:向 HTTP 请求传递 cookie.txt 文件路径
cookie_file = "",
-- 使用 fps 视频滤镜,大幅提升弹幕平滑度。默认禁用
vf_fps = false,
-- 设置要使用的 fps 滤镜参数
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)
+720
View File
@@ -0,0 +1,720 @@
local msg = require 'mp.msg'
local utils = require 'mp.utils'
local s2t = require("dicts/s2t_chars")
local t2s = require("dicts/t2s_chars")
local function ass_escape(text)
return text:gsub("\\", "\\\\")
:gsub("{", "\\{")
:gsub("}", "\\}")
:gsub("\n", "\\N")
end
local function xml_unescape(str)
return str:gsub("&quot;", "\"")
:gsub("&apos;", "'")
:gsub("&gt;", ">")
:gsub("&lt;", "<")
:gsub("&amp;", "&")
end
local function decode_html_entities(text)
return text:gsub("&#x([%x]+);", function(hex)
local codepoint = tonumber(hex, 16)
return unicode_to_utf8(codepoint)
end):gsub("&#(%d+);", function(dec)
local codepoint = tonumber(dec, 10)
return unicode_to_utf8(codepoint)
end)
end
-- 加载黑名单模式
local function load_blacklist_patterns(filepath)
local patterns = {}
if not file_exists(filepath) then
return patterns
end
local file = io.open(filepath, "r")
if not file then
msg.error("无法打开黑名单文件: " .. filepath)
return patterns
end
if string.match(filepath, "%.xml$") then
-- xml文件格式示例
--<?xml version="1.0" encoding="utf-8"?>
--<filters>
-- <item enabled="true">t=卡在</item>
-- <item enabled="true">t=进度条</item>
--</filters>
print("加载黑名单文件: " .. filepath)
for line in file:lines() do
local pattern = line:match('<item%s+enabled="true">t=(.-)</item>')
if pattern then
print("加载黑名单模式: " .. pattern)
table.insert(patterns, pattern)
end
end
end
if string.match(filepath, "%.json$") then
-- json文件格式示例
-- [{"type":0,"filter":"开门","opened":true,"id":15628936}
-- ,{"type":0,"filter":"tony","opened":true,"id":15628939}
-- ,{"type":1,"filter":"0+.1","opened":true,"id":15628951}]
local content = read_file(filepath)
if content then
local json = utils.parse_json(content)
if json and type(json) == "table" then
for _, entry in ipairs(json) do
if entry.opened and entry.filter and entry.type == 0 then
table.insert(patterns, entry.filter)
end
end
end
end
end
if string.match(filepath, "%.txt$") then
-- 文本文件格式示例
-- 卡在
-- 进度条
for line in file:lines() do
line = line:match("^%s*(.-)%s*$")
if line ~= "" then
table.insert(patterns, line)
end
end
end
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 tkey = tostring(d.type or "")
local ckey = tostring(d.color or "")
local text = d.text or ""
groups[tkey] = groups[tkey] or {}
groups[tkey][ckey] = groups[tkey][ckey] or {}
groups[tkey][ckey][text] = groups[tkey][ckey][text] or {}
table.insert(groups[tkey][ckey][text], d)
end
local merged = {}
local abs = math.abs
for _, bytype in pairs(groups) do
for _, bycolor in pairs(bytype) do
for _, group in pairs(bycolor) do
table.sort(group, function(a, b) return a.time < b.time end)
local i = 1
while i <= #group do
local base = group[i]
local times = { base.time }
local count = 1
local j = i + 1
while j <= #group and abs(group[j].time - base.time) <= threshold do
times[#times+1] = group[j].time
count = count + 1
j = j + 1
end
local same_time = true
for k = 2, #times do
if times[k] ~= times[1] then
same_time = false
break
end
end
local danmaku = {
time = base.time,
type = base.type,
size = base.size,
color = base.color,
text = base.text,
source = base.source,
orig_time = base.orig_time,
}
if count > 2 or not same_time then
danmaku.text = danmaku.text .. string.format("x%d", count)
end
table.insert(merged, danmaku)
i = j
end
end
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)
local danmakus = {}
-- [^>]* 匹配其他 attributes
-- %f[^%s] 确保 p= 前面是空白字符
for p_attr, text in xml_string:gmatch('<d%s+[^>]*%f[^%s]p="([^"]+)"[^>]*>([^<]+)</d>') do
local params = {}
local i = 1
for val in p_attr:gmatch("([^,]+)") do
params[i] = tonumber(val)
i = i + 1
end
if params[1] and params[2] and params[3] and params[4] then
table.insert(danmakus, {
time = params[1],
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)
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
table.insert(danmakus, {
time = params[1],
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_file(danmaku_input)
local danmakus = {}
if file_exists(danmaku_input) then
local content = read_file(danmaku_input)
if content then
local parsed = {}
if danmaku_input:match("%.xml$") then
parsed = parse_xml_danmaku(content)
elseif danmaku_input:match("%.json$") then
parsed = parse_json_danmaku(content)
end
for _, d in ipairs(parsed) do
table.insert(danmakus, d)
end
else
msg.info("无法读取文件内容: " .. danmaku_input)
end
else
msg.info("文件不存在: " .. danmaku_input)
end
for _, d in ipairs(danmakus) do
if d.orig_time == nil then d.orig_time = d.time end
end
if #danmakus == 0 then
msg.info("未能解析任何弹幕")
return nil
end
return danmakus
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 = 0, length = 0, empty = true }
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, empty = false }
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
function DanmakuArray:is_empty(row)
if row > 0 and row <= self.rows then
return self.time_length_array[row].empty
end
return false
end
-- 滚动弹幕 Y 坐标算法
function get_position_y(font_size, appear_time, text_length, resolution_x, roll_time, array)
local velocity = (text_length + resolution_x) / roll_time
for i = 1, array.rows do
local previous_appear_time = array:get_time(i)
if array:is_empty(i) 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
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
if delta_time >= roll_time then
array:set_time_length(i, appear_time, text_length)
return 1 + (i - 1) * font_size
end
end
end
-- 所有行都被占用,放弃渲染
return nil
end
-- 固定弹幕 Y 坐标算法
function get_fixed_y(font_size, appear_time, fixtime, array, from_top)
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 array:is_empty(i) 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
end
end
end
-- 所有行都被占用,放弃渲染
return nil
end
-- 将弹幕转换为 XML 格式
function convert_danmaku_to_xml(danmaku_out)
local danmakus = {}
for _, source in pairs(DANMAKU.sources) do
if not source.blocked and source.data then
for _, d in ipairs(source.data) do
table.insert(danmakus, d)
end
end
end
if #danmakus == 0 then
show_message("弹幕内容为空,无法保存", 3)
msg.verbose("弹幕内容为空,无法保存")
COMMENTS = {}
return false
end
-- 拼接为 XML 内容
local xml = { '<?xml version="1.0" encoding="UTF-8"?><i>\n' }
for _, d in ipairs(danmakus) do
local time = d.time
local type = d.type or 1
local size = d.size or 25
local color = d.color or 0xFFFFFF
local text = d.text or ""
text = text:gsub("&", "&amp;")
:gsub("<", "&lt;")
:gsub(">", "&gt;")
:gsub("\"", "&quot;")
:gsub("'", "&apos;")
table.insert(xml, string.format('<d p="%s,%s,%s,%s">%s</d>\n', time, type, size, color, text))
end
table.insert(xml, '</i>')
-- 写入 XML 文件
local file = io.open(danmaku_out, "w")
if not file then
show_message("无法写入目标 XML 文件", 3)
msg.info("无法写入目标 XML 文件: " .. danmaku_out)
return false
end
file:write(table.concat(xml))
file:close()
show_message("转换 XML 弹幕成功: " .. danmaku_out, 3)
msg.info("转换 XML 弹幕成功: " .. danmaku_out)
return true
end
function convert_danmaku_to_ass_events(force)
local per_source_lists = {}
for url, source in pairs(DANMAKU.sources) do
if not source.blocked and source.data then
local segments = nil
local prefix = nil
if source.delay_segments and #source.delay_segments > 0 then
segments = {}
for i, v in ipairs(source.delay_segments) do segments[i] = v end
table.sort(segments, function(a, b) return (a.start or 0) < (b.start or 0) end)
prefix = {}
local s = 0
for i, v in ipairs(segments) do
s = s + (v.delay or 0)
prefix[i] = s
end
end
local function get_cached_delay(t)
local segs = segments or {}
local pre = prefix or {}
if #segs == 0 then return 0 end
local idx = binary_search(segs, t, function(s) return (s and s.start) or 0 end)
local target = idx - 1
if target < 1 then return 0 end
return pre[target] or 0
end
local list = {}
for _, d in ipairs(source.data) do
local base_time = d.orig_time or d.time
if d.orig_time == nil then d.orig_time = base_time end
local adjusted_time = base_time + get_cached_delay(base_time)
local entry = {
orig_time = d.orig_time,
time = adjusted_time,
type = d.type,
size = d.size,
color = d.color,
text = d.text,
source = url,
}
if not is_blacklisted(d.text, black_patterns) then
table.insert(list, entry)
end
end
if #list > 0 then
table.sort(list, function(a, b) return a.time < b.time end)
per_source_lists[#per_source_lists + 1] = list
end
end
end
local danmakus = {}
local heap = new_min_heap()
for li = 1, #per_source_lists do
local lst = per_source_lists[li]
if lst and #lst > 0 then
heap.push({ time = lst[1].time, list_idx = li, pos = 1, entry = lst[1] })
end
end
while true do
local node = heap.pop()
if not node then break end
table.insert(danmakus, node.entry)
local li = node.list_idx
local next_pos = node.pos + 1
local lst = per_source_lists[li]
if lst and lst[next_pos] then
heap.push({ time = lst[next_pos].time, list_idx = li, pos = next_pos, entry = lst[next_pos] })
end
end
if options.max_screen_danmaku > 0 and options.merge_tolerance <= 0 then
options.merge_tolerance = options.scrolltime
end
danmakus = merge_duplicate_danmaku(danmakus, options.merge_tolerance)
if #danmakus == 0 then
if not force then
show_message("该集弹幕内容为空,结束加载", 3)
msg.verbose("该集弹幕内容为空,结束加载")
end
COMMENTS = {}
return
end
if not force then
msg.info("已解析 " .. #danmakus .. " 条弹幕")
end
local fontsize = tonumber(options.fontsize) or 50
local scrolltime = tonumber(options.scrolltime) or 15
local fixtime = tonumber(options.fixtime) or 5
local 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 pre_events = {}
for _, d in ipairs(danmakus) do
local time = d.type == 1 and math.floor(d.time + 0.5) or d.time
local orig_time = d.type == 1 and math.floor(d.orig_time + 0.5) or d.orig_time
local appear_time = time
local danmaku_type = d.type
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, {orig_time = orig_time, 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 clean_text = ch_convert_cached(decode_html_entities(d.text))
local text = ass_escape(clean_text)
:gsub("x(%d+)$", "{\\b1\\i1}x%1")
-- 颜色从十进制转为 BGR Hex
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 style, effect
local pos, move = nil, nil
-- 滚动弹幕 (类型 1, 2, 3)
if danmaku_type >= 1 and danmaku_type <= 3 then
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)
move = {x1, y, x2, y}
end
-- 顶部弹幕 (类型 5)
elseif danmaku_type == 5 then
style = "TOP"
local x = res_x / 2
local y = get_fixed_y(fontsize, appear_time, fixtime, top_array, true)
if y then
effect = string.format("{\\pos(%d, %d)}", x, y)
pos = {x, y}
end
-- 底部弹幕 (类型 4)
elseif danmaku_type == 4 then
style = "BTM"
local x = res_x / 2
local y = get_fixed_y(fontsize, appear_time, fixtime, top_array, false)
if y then
effect = string.format("{\\pos(%d, %d)}", x, y)
pos = {x, y}
end
end
if style and effect then
text = effect .. color_text .. text
local event = {
orig_time = ev.orig_time,
start_time = ev.start_time,
end_time = ev.end_time,
delay = ev.start_time - (ev.orig_time or ev.start_time),
style = style,
text = text,
clean_text = clean_text,
pos = pos,
move = move,
source = d.source,
}
table.insert(ass_events, event)
COMMENTS = ass_events
end
end
end
+259
View File
@@ -0,0 +1,259 @@
-- modified from https://github.com/rkscv/danmaku/blob/main/danmaku.lua
local msg = require('mp.msg')
local utils = require("mp.utils")
local unpack = unpack or table.unpack
local osd_width, osd_height, pause = 0, 0, true
local time_pos_observer_active = false
local overlay = mp.create_osd_overlay('ass-events')
local function realtime_position_text(event, pos, displayarea)
if not event.move then
local _, current_y = unpack(event.pos)
if not current_y or tonumber(current_y) > displayarea then return end
if event.style ~= "SP" and event.style ~= "MSG" then
return string.format("{\\an8}%s", event.text)
else
return string.format("{\\an7}%s", event.text)
end
end
local x1, y1, x2, y2 = unpack(event.move)
-- 计算移动的时间范围
local duration = event.end_time - event.start_time --mean: options.scrolltime
local progress = (pos - event.start_time) / 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
function render(pos_arg)
if COMMENTS == nil then return end
local pos, err
if pos_arg == nil then
pos, err = mp.get_property_number('time-pos')
if err ~= nil then
return msg.error(err)
end
else
pos = pos_arg
end
if not pos then
overlay:remove()
return
end
local fontname = options.fontname
local fontsize = options.fontsize
local opacity = tonumber(options.opacity)
local alpha = string.format("%02X", (1 - (opacity or 0)) * 255)
local width, height = 1920, 1080
local ratio = osd_width / osd_height
if width / height < ratio then
height = width / ratio
fontsize = options.fontsize - ratio * 2
end
local ass_events = {}
local max_display = math.max(options.scrolltime, options.fixtime)
local window_start = pos - max_display
-- 跳过已结束的弹幕
local lo = binary_search(COMMENTS, window_start, function(item) return item.start_time end)
local re_entity = "&#%d+;"
local re_fs = "\\fs(%d+)"
local ass_prefix = string.format("{\\rDefault\\fn%s\\fs%d\\c&HFFFFFF&\\alpha&H%s\\bord%s\\shad%s\\b%s\\q2}",
fontname, fontsize, alpha, options.outline, options.shadow, options.bold and "1" or "0")
for i = lo, #COMMENTS do
local event = COMMENTS[i]
if not event then break end
if event.start_time > pos then break end -- 后续弹幕提前退出
if event.end_time >= pos then
local text = realtime_position_text(event, pos, height * options.displayarea)
if text then
text = text:gsub(re_entity, "")
end
if text and text:match(re_fs) then
text = text:gsub(re_fs, function(size)
local n = tonumber(size) or 0
return string.format("\\fs%d", math.floor(n * 1.5))
end)
end
-- 构建 ASS 字符串
local ass_text = text and (ass_prefix .. 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 function time_pos_callback(_, time_pos)
if time_pos then
render(time_pos)
else
overlay:remove()
end
end
local function start_time_observer()
if not time_pos_observer_active then
mp.observe_property('time-pos', 'number', time_pos_callback)
time_pos_observer_active = true
end
end
local function stop_time_observer()
if time_pos_observer_active then
mp.unobserve_property(time_pos_callback)
time_pos_observer_active = false
end
end
function render_danmaku(from_menu, no_osd)
if ENABLED and (from_menu or get_danmaku_visibility()) then
if not no_osd then
show_loaded(true)
end
mp.commandv("script-message-to", "uosc", "set", "show_danmaku", "on")
show_danmaku_func()
else
show_message("")
hide_danmaku_func()
end
end
local function filter_state(label, name)
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()
mp.set_property_bool(HAS_DANMAKU, true)
set_danmaku_visibility(true)
render()
if not pause then
start_time_observer()
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()
stop_time_observer()
mp.set_property_bool(HAS_DANMAKU, false)
set_danmaku_visibility(false)
overlay:remove()
if filter_state("danmaku") then
mp.commandv("vf", "remove", "@danmaku")
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('pause', 'bool', function(_, value)
if value ~= nil then
pause = value
end
if ENABLED then
if pause then
stop_time_observer()
elseif COMMENTS ~= nil then
start_time_observer()
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
stop_time_observer()
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, "temp-" .. PID .. ".mp4"),
}
if options.save_danmaku then
save_danmaku(true)
end
for _, file in pairs(files_to_remove) do
if file_exists(file) then
os.remove(file)
end
end
DANMAKU = {sources = {}, count = 1}
end)
+167
View File
@@ -0,0 +1,167 @@
local msg = require('mp.msg')
local utils = require("mp.utils")
local repo = "Tony15246/uosc_danmaku"
local zip_file = utils.join_path(os.getenv("TEMP") or "/tmp/", "uosc_danmaku.zip")
local local_version = VERSION or "0.0.0"
local platform = mp.get_property("platform")
local function version_greater(v1, v2)
local function parse(ver)
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*"([^"]+)"]])
local zip_url = res.stdout:match([["browser_download_url"%s*:%s*"([^"]+%.zip)"]])
return tag, zip_url
end
local function escape_ps(str)
return tostring(str):gsub("'", "''")
end
local function unzip_overwrite(zip_file)
local outpath = mp.get_script_directory()
-- 定义临时目录路径,用于安全更新
local tmpdir = utils.join_path(
(platform == "windows" and (os.getenv("TEMP") or "C:\\Windows\\Temp") or "/tmp"),
"uosc_update_" .. tostring(os.time())
)
local cmd_unzip = {}
msg.info("创建临时目录并解压: " .. tmpdir)
if platform == "windows" then
-- PowerShell: Expand-Archive (会自动创建目标目录)
local ps_script = string.format(
"Expand-Archive -LiteralPath '%s' -DestinationPath '%s' -Force",
escape_ps(zip_file),
escape_ps(tmpdir)
)
cmd_unzip = { "powershell", "-NoProfile", "-Command", ps_script }
else
-- Unix: unzip
cmd_unzip = { "unzip", "-o", zip_file, "-d", tmpdir }
end
local res = mp.command_native({
name = "subprocess",
args = cmd_unzip,
capture_stdout = true,
capture_stderr = true,
playback_only = false,
})
if not res or res.status ~= 0 then
msg.error("❌ 解压失败:\n" .. (res and (res.stdout .. res.stderr) or "未知错误"))
-- 清理残留的临时目录
if platform == "windows" then
mp.command_native({
name = "subprocess",
args = {"powershell", "-NoProfile", "-Command", "Remove-Item -LiteralPath '"..escape_ps(tmpdir).."' -Recurse -Force"}
})
else
os.execute("rm -rf \"" .. tmpdir .. "\"")
end
return false
end
msg.info("解压成功,准备替换旧目录...")
local cmd_swap = {}
if platform == "windows" then
-- Windows: 在一个 PowerShell 实例中执行删除和移动
local ps_swap = string.format(
"Remove-Item -LiteralPath '%s' -Recurse -Force -ErrorAction SilentlyContinue; Move-Item -LiteralPath '%s' -Destination '%s' -Force",
escape_ps(outpath),
escape_ps(tmpdir),
escape_ps(outpath)
)
cmd_swap = { "powershell", "-NoProfile", "-Command", ps_swap }
else
-- Unix: rm && mv
cmd_swap = { "sh", "-c", string.format("rm -rf \"%s\" && mv \"%s\" \"%s\"", outpath, tmpdir, outpath) }
end
local res_swap = mp.command_native({
name = "subprocess",
args = cmd_swap,
capture_stdout = true,
capture_stderr = true,
playback_only = false,
})
if not res_swap or res_swap.status ~= 0 then
msg.error("❌ 替换目录失败:\n" .. (res_swap and (res_swap.stdout .. res_swap.stderr) or ""))
return false
end
msg.info("更新完成")
return true
end
function check_for_update()
local latest_version, download_url = get_latest_release(repo)
if not latest_version or not download_url then
show_message("❌ 无法获取最新版本信息")
msg.warn("❌ 无法获取最新版本信息")
return
end
if not version_greater(latest_version, local_version) then
show_message("✅ 已是最新版本 ("..local_version..")")
msg.info("✅ 已是最新版本")
return
end
show_message("⬇️ 发现新版本: " .. latest_version .. ",正在下载...")
msg.info("⬇️ 发现新版本: " .. latest_version .. ",地址: " .. download_url)
local cmd = { "curl", "-L", "-o", zip_file, download_url }
local res = mp.command_native({
name = "subprocess",
args = cmd,
capture_stdout = true,
capture_stderr = true,
playback_only = false,
})
if not res or res.status ~= 0 then
show_message("❌ 下载失败!")
msg.warn("❌ 下载失败!")
return
end
show_message("📦 下载完成,开始解压覆盖...")
msg.info("📦 下载完成,开始解压覆盖...")
if unzip_overwrite(zip_file) then
os.remove(zip_file)
show_message("✅ 更新成功!请重启 mpv 以应用更新,当前版本为:" .. latest_version)
msg.info("✅ 更新成功,当前版本为:" .. latest_version)
else
os.remove(zip_file)
show_message("❌ 解压失败!请查看控制台日志")
msg.warn("❌ 解压失败!")
end
end
+755
View File
@@ -0,0 +1,755 @@
local utils = require("mp.utils")
local unpack = unpack or table.unpack
-- from http://lua-users.org/wiki/LuaUnicode
local UTF8_PATTERN = '[%z\1-\127\194-\244][\128-\191]*'
-- 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
function hex_to_int_color(hex_color)
-- 移除颜色代码中的'#'字符
hex_color = hex_color:sub(2) -- 只保留颜色代码部分
-- 提取R, G, B的十六进制值并转为整数
local r = tonumber(hex_color:sub(1, 2), 16)
local g = tonumber(hex_color:sub(3, 4), 16)
local b = tonumber(hex_color:sub(5, 6), 16)
-- 计算32位整数值
local color_int = (r * 256 * 256) + (g * 256) + b
return color_int
end
local function get_type_from_position(position)
if position == 0 then
return 1
end
if position == 1 then
return 4
end
return 5
end
-- url编码转换
function url_encode(str)
-- 将非安全字符转换为百分号编码
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 binary_search(tbl, target, key)
if not tbl or #tbl == 0 then return 1 end
key = key or function(x) return x end
local lo, hi = 1, #tbl
local res = #tbl + 1
while lo <= hi do
local mid = math.floor((lo + hi) / 2)
local v = tbl[mid]
local val = key(v)
if val >= target then
res = mid
hi = mid - 1
else
lo = mid + 1
end
end
return res
end
function new_min_heap()
local h = {}
local function swap(i, j)
h[i], h[j] = h[j], h[i]
end
local function up(i)
while i > 1 do
local p = math.floor(i/2)
if h[p].time <= h[i].time then break end
swap(p, i)
i = p
end
end
local function down(i)
local n = #h
while true do
local l = i * 2
local r = l + 1
local smallest = i
if l <= n and h[l].time < h[smallest].time then smallest = l end
if r <= n and h[r].time < h[smallest].time then smallest = r end
if smallest == i then break end
swap(i, smallest)
i = smallest
end
end
local function push(node)
h[#h + 1] = node
up(#h)
end
local function pop()
if #h == 0 then return nil end
local root = h[1]
if #h == 1 then h[1] = nil; return root end
h[1] = h[#h]
h[#h] = nil
down(1)
return root
end
return { push = push, pop = pop, size = function() return #h end }
end
function is_writable(path)
local file = io.open(path, "w")
if file then
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 get_series_episodes(fname1, fname2)
local parts1 = split_by_numbers(fname1)
local parts2 = split_by_numbers(fname2)
local title1 = format_filename(fname1)
local title2 = format_filename(fname2)
if title1 and title2 then
local media_title1, season1, episode1 = title1:match("^(.-)%s*[sS](%d+)[eE](%d+)")
local media_title2, season2, episode2 = title2:match("^(.-)%s*[sS](%d+)[eE](%d+)")
if season1 and season2 and season1 ~= season2 then
return nil, nil
end
end
local min_len = math.min(#parts1, #parts2)
-- 逐个部分进行比较
for i = 1, min_len do
local part1 = parts1[i]
local part2 = parts2[i]
-- 比较数字前的字符是否相同
if part1.pre ~= part2.pre then
return nil, nil
end
-- 比较数字部分
if part1.num ~= part2.num then
return part1.num, part2.num
end
-- 比较数字后的字符是否相同
if part1.post ~= part2.post then
return nil, nil
end
end
return nil, nil
end
-- 获取当前文件名所包含的集数
function get_episode_number(filename, fname)
-- 尝试对比记录文件名来获取当前集数
if fname then
return get_series_episodes(fname, filename)
end
local thin_space = string.char(0xE2, 0x80, 0x89)
filename = filename:gsub(thin_space, " ")
local title = format_filename(filename)
if title then
local media_title, season, episode = title:match("^(.-)%s*[sS](%d+)[eE](%d+)")
if season then
return tonumber(episode)
else
local media_title, episode = title:match("^(.-)%s*[eE](%d+)")
if episode then
return tonumber(episode)
end
end
end
return nil
end
-- 规范化路径
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
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 = true,
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